-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathelections.js
More file actions
121 lines (111 loc) · 3.62 KB
/
elections.js
File metadata and controls
121 lines (111 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// MIT License
// Copyright (c) 2026 Open2b
// See the LICENSE file for full text.
import { getTime } from './utils.js'
// Elections maintain leader elections. Each candidate should have its own
// instance.
class Elections {
#id
#state
#onElection
#timeoutID
// constructor returns an instance of Elections for a candidate identified
// by id. state implements methods for reading and writing the election
// state. During each election, the onElection function is invoked with a
// boolean argument indicating whether the candidate emerged as the leader.
constructor(id, state, onElection) {
this.#id = id
this.#state = state
this.#onElection = onElection
this.#timeoutID = setTimeout(this.#keep.bind(this))
}
// close closes elections.
close() {
clearTimeout(this.#timeoutID)
this.#timeoutID = null
}
// resign resigns as leader.
resign() {
this.#getOwner(1, (id) => {
if (id === this.#id) {
this.#state.write(this.#id, 1, '', () => this.#onElection(false))
}
})
}
// keep keeps the elections. It is initially invoked by the constructor and
// then recursively calls itself.
#keep() {
// While debugging tests in Deno, there have been instances where #keep
// is invoked even after the timeout has been canceled.
if (this.#timeoutID == null) {
console.warn('elections.#keep called after closure')
return
}
this.#timeoutID = null
this.#tryElection((isLeader, expiration) => {
const interval = expiration - getTime()
// The next election occurs 1.2 seconds later. The leader retries after 1 second
// while followers retry between 1.2 and 1.4 seconds.
const delay = isLeader ? interval - 200 : interval + Math.floor(Math.random() * 200) + 1
this.#timeoutID = setTimeout(this.#keep.bind(this), delay > 0 ? delay : 0)
this.#onElection(isLeader)
})
}
// tryElection attempts to elect the current node as the leader. It returns
// a boolean indicating whether the election was successful and the
// expiration time, in milliseconds from the epoch, of the latest election.
// If the storage is out of quota, it raises a QuotaExceededError exception.
#tryElection(cb) {
// The implementation utilizes callbacks to facilitate code testing.
this.#getOwner(1, (id, expiration) => {
if (id != null && id !== this.#id) {
cb(false, expiration)
return
}
this.#setOwner(1, this.#id, (newExpiration) => {
if (id === this.#id) {
const now = getTime()
if (now < expiration) {
cb(true, newExpiration)
return
}
}
this.#getOwner(2, (id, expiration) => {
if (id != null && id !== this.#id) {
cb(false, expiration)
return
}
this.#setOwner(2, this.#id, () => {
this.#getOwner(1, (id, expiration) => {
cb(id === this.#id, expiration)
})
})
})
})
})
}
// getOwner calls the callback with two arguments: the identifier of the
// owner of the provided location, and the expiration time of the ownership
// in milliseconds. If the location has no owner, both arguments passed to
// the callback are undefined.
#getOwner(location, cb) {
this.#state.read(this.#id, location, (owner) => {
if (owner != null) {
const [id, expiration] = owner.split(' ')
const now = getTime()
if (Number(expiration) > now) {
cb(id, expiration)
return
}
}
cb()
})
}
// setOwner assigns the candidate with identifier id as the owner of the
// provided location, and then proceeds to call the callback.
#setOwner(location, id, cb) {
const expiration = getTime() + 1200
this.#state.write(this.#id, location, `${id} ${expiration}`, () => cb(expiration))
}
}
export default Elections