Skip to content

Commit edf9ab0

Browse files
committed
fix: elections sometimes electing >1 leader
Fixes #176 Fixes #158
1 parent eb99050 commit edf9ab0

File tree

3 files changed

+63
-16
lines changed

3 files changed

+63
-16
lines changed

changelog.md

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

3+
# 1.2.1 2023-07-30
4+
5+
- **fix:** elections sometimes electing >1 leader (see [#176](https://github.com/microsoft/etcd3/issues/176))
6+
- **fix:** a race condition in Host.resetAllServices (see [#182](https://github.com/microsoft/etcd3/issues/182))
7+
38
## 1.2.0 2023-07-28
49

510
- **fix:** leases revoked or released before grant completes leaking

src/election.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ClientRuntimeError, NotCampaigningError } from './errors';
88
import { Lease } from './lease';
99
import { Namespace } from './namespace';
1010
import { IKeyValue } from './rpc';
11-
import { getDeferred, IDeferred, toBuffer } from './util';
11+
import { IDeferred, getDeferred, toBuffer } from './util';
1212

1313
const UnsetCurrent = Symbol('unset');
1414

@@ -331,20 +331,28 @@ export class Campaign extends EventEmitter {
331331
}
332332

333333
private async waitForElected(revision: string) {
334-
// find last created before this one
335-
const lastRevision = new BigNumber(revision).minus(1).toString();
336-
const result = await this.namespace
337-
.getAll()
338-
.maxCreateRevision(lastRevision)
339-
.sort('Create', 'Descend')
340-
.limit(1)
341-
.exec();
342-
343-
// wait for all older keys to be deleted for us to become the leader
344-
await waitForDeletes(
345-
this.namespace,
346-
result.kvs.map(k => k.key),
347-
);
334+
while (this.keyRevision !== ResignedCampaign) {
335+
// find last created before this one
336+
const lastRevision = new BigNumber(revision).minus(1).toString();
337+
const result = await this.namespace
338+
.getAll()
339+
.maxCreateRevision(lastRevision)
340+
.sort('Create', 'Descend')
341+
.limit(1)
342+
.exec();
343+
344+
if (result.kvs.length === 0) {
345+
return;
346+
}
347+
348+
this.emit('_isWaiting'); // internal event used to sync unit tests
349+
350+
// wait for all it to be deleted for us to become the leader
351+
await waitForDeletes(
352+
this.namespace,
353+
result.kvs.map(k => k.key),
354+
);
355+
}
348356
}
349357
}
350358

src/test/election.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { take } from 'rxjs/operators';
44
import { Election, Etcd3 } from '../';
55
import { Campaign } from '../election';
66
import { NotCampaigningError } from '../errors';
7-
import { delay, getDeferred } from '../util';
7+
import { delay, getDeferred, onceEvent } from '../util';
88
import { getOptions, tearDownTestClient } from './util';
99

1010
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
@@ -192,4 +192,38 @@ describe('election', () => {
192192
await observer.cancel();
193193
});
194194
});
195+
196+
it('fixes #176', async function () {
197+
const observer1 = await election.observe();
198+
199+
const client2 = new Etcd3(getOptions());
200+
const election2 = client2.election('test-election', 1);
201+
const observer2 = await election2.observe();
202+
const campaign2 = election2.campaign('candidate2');
203+
await onceEvent(campaign2, '_isWaiting');
204+
205+
const client3 = new Etcd3(getOptions());
206+
const election3 = client3.election('test-election', 1);
207+
const observer3 = await election3.observe();
208+
const campaign3 = election3.campaign('candidate3');
209+
await onceEvent(campaign3, '_isWaiting');
210+
211+
expect(observer1.leader()).to.equal('candidate');
212+
expect(observer2.leader()).to.equal('candidate');
213+
expect(observer3.leader()).to.equal('candidate');
214+
215+
const changes: string[] = [];
216+
campaign.on('elected', () => changes.push('leader is now 1'));
217+
campaign3.on('elected', () => changes.push('leader is now 3'));
218+
219+
await campaign2.resign();
220+
await delay(1000); // give others a chance to see the change, if any
221+
222+
expect(observer1.leader()).to.equal('candidate');
223+
expect(observer3.leader()).to.equal('candidate');
224+
expect(changes).to.be.empty;
225+
226+
client2.close();
227+
client3.close();
228+
});
195229
});

0 commit comments

Comments
 (0)