Skip to content

Commit 3bc74c7

Browse files
committed
CHANGE Better determine the correct responseTime to use to make it less likely to elect duplicate leaders
1 parent 31a86ad commit 3bc74c7

File tree

5 files changed

+65
-21
lines changed

5 files changed

+65
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 4.8.0 (15 December 2021)
4+
5+
Changes:
6+
- Better determine the correct `responseTime` to use to make it less likely to elect duplicate leaders.
37

48
## 4.7.1 (13 December 2021)
59

src/leader-election.js

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ const LeaderElection = function (broadcastChannel, options) {
3131
// things to clean up
3232
this._unl = []; // _unloads
3333
this._lstns = []; // _listeners
34-
this._invs = []; // _intervals
3534
this._dpL = () => { }; // onduplicate listener
3635
this._dpLC = false; // true when onduplicate called
3736

@@ -61,7 +60,10 @@ LeaderElection.prototype = {
6160
* false if not.
6261
* @async
6362
*/
64-
applyOnce() {
63+
applyOnce(
64+
// true if the applyOnce() call came from the fallbackInterval cycle
65+
isFromFallbackInterval
66+
) {
6567
if (this.isLeader) {
6668
return sleep(0, true);
6769
}
@@ -126,16 +128,29 @@ LeaderElection.prototype = {
126128
}
127129
};
128130
this.broadcastChannel.addEventListener('internal', handleMessage);
131+
132+
/**
133+
* If the applyOnce() call came from the fallbackInterval,
134+
* we can assume that the election runs in the background and
135+
* not critical process is waiting for it.
136+
* When this is true, we give the other intances
137+
* more time to answer to messages in the election cycle.
138+
* This makes it less likely to elect duplicate leaders.
139+
* But also it takes longer which is not a problem because we anyway
140+
* run in the background.
141+
*/
142+
const waitForAnswerTime = isFromFallbackInterval ? this._options.responseTime * 4 : this._options.responseTime;
143+
129144
const applyPromise = _sendMessage(this, 'apply') // send out that this one is applying
130145
.then(() => Promise.race([
131-
sleep(this._options.responseTime / 2),
146+
sleep(waitForAnswerTime),
132147
stopCriteriaPromise.then(() => Promise.reject(new Error()))
133148
]))
134149
// send again in case another instance was just created
135150
.then(() => _sendMessage(this, 'apply'))
136151
// let others time to respond
137152
.then(() => Promise.race([
138-
sleep(this._options.responseTime / 2),
153+
sleep(waitForAnswerTime),
139154
stopCriteriaPromise.then(() => Promise.reject(new Error()))
140155
]))
141156
.catch(() => { })
@@ -177,8 +192,6 @@ LeaderElection.prototype = {
177192
die() {
178193
this._lstns.forEach(listener => this.broadcastChannel.removeEventListener('internal', listener));
179194
this._lstns = [];
180-
this._invs.forEach(interval => clearInterval(interval));
181-
this._invs = [];
182195
this._unl.forEach(uFn => uFn.remove());
183196
this._unl = [];
184197

@@ -207,7 +220,6 @@ function _awaitLeadershipOnce(leaderElector) {
207220
return;
208221
}
209222
resolved = true;
210-
clearInterval(interval);
211223
leaderElector.broadcastChannel.removeEventListener('internal', whenDeathListener);
212224
res(true);
213225
}
@@ -219,15 +231,32 @@ function _awaitLeadershipOnce(leaderElector) {
219231
}
220232
});
221233

222-
// try on fallbackInterval
223-
const interval = setInterval(() => {
224-
leaderElector.applyOnce().then(() => {
225-
if (leaderElector.isLeader) {
226-
finish();
227-
}
228-
});
229-
}, leaderElector._options.fallbackInterval);
230-
leaderElector._invs.push(interval);
234+
/**
235+
* Try on fallbackInterval
236+
* @recursive
237+
*/
238+
const tryOnFallBack = () => {
239+
return sleep(leaderElector._options.fallbackInterval)
240+
.then(() => {
241+
if (leaderElector.isDead || resolved) {
242+
return;
243+
}
244+
if (leaderElector.isLeader) {
245+
finish();
246+
} else {
247+
return leaderElector
248+
.applyOnce(true)
249+
.then(() => {
250+
if (leaderElector.isLeader) {
251+
finish();
252+
} else {
253+
tryOnFallBack();
254+
}
255+
});
256+
}
257+
});
258+
};
259+
tryOnFallBack();
231260

232261
// try when other leader dies
233262
const whenDeathListener = msg => {

test/integration.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,9 +643,16 @@ function runTest(channelOptions) {
643643
const elector2 = createLeaderElection(channel2);
644644

645645
await elector.awaitLeadership();
646+
await AsyncTestUtil.wait(200);
646647

647648
let resolved = false;
648-
elector2.awaitLeadership().then(() => resolved = true);
649+
elector2.awaitLeadership().then(() => {
650+
resolved = true;
651+
});
652+
653+
// wait for the applyQueue to be done
654+
// to not accientially skip testing the fallbackInterval election cycle.
655+
await elector2._aplQ;
649656

650657
// overwrite postInternal to simulate non-responding leader
651658
channel.postInternal = () => Promise.resolve();

test/unit/indexed-db.method.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('unit/indexed-db.method.test.js', () => {
207207
});
208208
/**
209209
* localstorage-pings do not work in a web-workers,
210-
* which means this should be detected and work over intervall
210+
* which means this should be detected and work over interval
211211
* @link https://stackoverflow.com/a/6179599/3443137
212212
*/
213213
it('should also work if localstorage does not work', async () => {

types/leader-election.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import {
55

66
export type LeaderElectionOptions = {
77
/**
8-
* This value decides how often instances will renegotiate who is leader.
9-
* Probably should be at least 2x bigger than responseTime.
8+
* Normally, when the leading JavaScript process dies, it will send an I-am-dead
9+
* message to the other LeaderElectors, so that they can elect a new leader.
10+
* On rare cases, when the JavaScript process exits ungracefully, it can happen
11+
* that the other electors do not get a dead-message.
12+
* So we have to also run the election cycle in an interval to ensure
13+
* we never stuck on a state where noone is leader and noone is trying to get elected.
1014
*/
1115
fallbackInterval?: number;
1216
/**
@@ -41,7 +45,7 @@ export declare class LeaderElector {
4145
readonly isDead: boolean;
4246
readonly token: string;
4347

44-
applyOnce(): Promise<boolean>;
48+
applyOnce(isFromFallbackInterval?: boolean): Promise<boolean>;
4549
awaitLeadership(): Promise<void>;
4650
die(): Promise<void>;
4751

0 commit comments

Comments
 (0)