@zukhruf/election 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,6 +50,8 @@ next term: 2n
50
50
 
51
51
  - `campaign({ timeout, signal })` resolves with a `Term`, or with `undefined` when another candidate still leads after `timeout` milliseconds. Without a `timeout`, it tries once. When `signal` aborts, it rejects with `signal.reason`, and a claim that it won meanwhile is given up.
52
52
  - `claimFile` and `epochFile` are files in `directory`. Candidates with the same directory and the same file names are one group. Never delete `claimFile` while candidates run: a new file lets a second leader win.
53
+ - `epochFile` holds the epoch of the last term as decimal digits. When it holds other text, `campaign` rejects with a `SyntaxError` that names the file, gives the claim up, and does not change the file. An example is the empty file that `@zukhruf/mutex` 0.2.0 or earlier can leave after a power loss, because those versions did not sync the file to the disk. Thus the epochs never start again at 1, and each new term has a higher epoch than each earlier term.
54
+ - A term that resigns clean writes a third file, `<claimFile>.clean`, beside `claimFile`. It holds the epoch of that term. The next term reads it and removes it.
53
55
  - The directory must be on a local file system. On a network file system, `campaign` rejects with `NetworkDirectoryError`.
54
56
  - `pollInterval` is the time between two tries, in milliseconds. It defaults to 10. A try never blocks the process.
55
57
 
@@ -58,7 +60,9 @@ next term: 2n
58
60
  A term is a lease on leadership. `Term` implements the `Lease` interface of [`@zukhruf/lease`](../lease/README.md), so a function that takes a `Lease` also takes a term. See the glossary of [leases](../lease/CONTEXT.md).
59
61
 
60
62
  - `term.epoch` is the number of the term. Each epoch is below 2^31.
61
- - `term.resign()` ends the term and gives the claim up. A second call waits for the first. `await using` resigns at the end of its scope.
63
+ - `term.resign()` ends the term and gives the claim up. A second call waits for the first, and its options do nothing. `await using` resigns at the end of its scope.
64
+ - `term.resign({ clean: true })` records a clean shutdown for the next term, and then ends the term. Use it only when the leader leaves nothing for the next term to recover, for example no work in progress. A lost term records nothing, also with `clean`. When the record fails, the claim is freed, and `resign` rejects with that error.
65
+ - `term.afterCleanShutdown` is `true` only when the term just before this one resigned clean. Then the new leader knows that it has nothing to recover. It is `false` after a crash, after a lost term, after a resign without `clean`, and for the first term.
62
66
  - `term.signal` aborts with `LeaseLostError` when the term is lost while its leader still runs. The `subject` of the error is `'leadership'`, and its `cause` is the reason of the backend. The signal does not abort when the leader resigns.
63
67
 
64
68
  A leader that acts for the group must stop when `term.signal` aborts, because another candidate can lead then. `SqliteElection` never loses a living term, so its signal never aborts. A backend that can take the claim away can lose one.
@@ -76,22 +80,24 @@ const token = await tokens.next('orders');
76
80
 
77
81
  ## Write a backend
78
82
 
79
- `LeaderElection` is an abstract class. It runs the campaign, and it owns the term. A backend is a subclass that implements five steps for its claim:
83
+ `LeaderElection` is an abstract class. It runs the campaign, and it owns the term. A backend is a subclass that implements six steps for its claim:
80
84
 
81
- | Step | What it does |
82
- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | `open(signal)` | Prepares one attempt, for example a connection. The campaign calls `tryClaim` on it again and again. |
84
- | `tryClaim(claim, signal)` | Tries the claim once. Resolves with the epoch of the term it won, or `undefined` while another candidate holds the claim. |
85
- | `watch(claim, lose)` | Watches a won claim for as long as its term lasts. Calls `lose` when the backend takes the claim away. Returns a `Disposable` that stops the watch. |
86
- | `release(claim)` | Gives a won claim up when the leader resigns. |
87
- | `close(claim)` | Frees the resources of an attempt, at each end. |
85
+ | Step | What it does |
86
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | `open(signal)` | Prepares one attempt, for example a connection. The campaign calls `tryClaim` on it again and again. |
88
+ | `tryClaim(claim, signal)` | Tries the claim once. Resolves with `{ epoch, afterCleanShutdown }` for the term it won, or with `undefined` while another candidate holds the claim. |
89
+ | `watch(claim, lose)` | Watches a won claim for as long as its term lasts. Calls `lose` when the backend takes the claim away. Returns a `Disposable` that stops the watch. |
90
+ | `recordCleanShutdown(claim, epoch)` | Records that the term of `epoch` resigned clean. The campaign calls it before `release`, while the term still holds the claim. |
91
+ | `release(claim)` | Gives a won claim up when the leader resigns. |
92
+ | `close(claim)` | Frees the resources of an attempt, at each end. |
88
93
 
89
- A backend must keep four rules:
94
+ A backend must keep five rules:
90
95
 
91
96
  1. `tryClaim` gives the epoch in the same step that wins the claim. Each epoch is higher than each earlier one, and below 2^31.
92
- 2. `watch` calls `lose` before the backend can give the claim to another candidate. For a claim that the backend keeps only for a time, renew it well before that time ends, and count the time on a monotonic clock. Then an old leader stops before a new one starts.
93
- 3. The campaign never calls `release` after a loss, because the claim can be another candidate's then.
94
- 4. The campaign calls `close` after each end: a won term, a lost term, a failed attempt, or an attempt that never won.
97
+ 2. `tryClaim` gives `afterCleanShutdown: true` only when the term just before this one recorded a clean shutdown. It reads each record one time only. Thus a crash after a clean term never reads as clean.
98
+ 3. `watch` calls `lose` before the backend can give the claim to another candidate. For a claim that the backend keeps only for a time, renew it well before that time ends, and count the time on a monotonic clock. Then an old leader stops before a new one starts.
99
+ 4. The campaign never calls `recordCleanShutdown` or `release` after a loss, because the claim can be another candidate's then.
100
+ 5. The campaign calls `close` after each end: a won term, a lost term, a failed attempt, or an attempt that never won.
95
101
 
96
102
  This backend elects among the candidates of one process. Its claim is an entry in a map:
97
103
 
@@ -99,6 +105,7 @@ This backend elects among the candidates of one process. Its claim is an entry i
99
105
  import { LeaderElection } from '@zukhruf/election';
100
106
 
101
107
  const holders = new Map<string, bigint>();
108
+ const cleanShutdowns = new Set<string>();
102
109
  let lastEpoch = 0n;
103
110
 
104
111
  class MapElection extends LeaderElection<string> {
@@ -115,9 +122,12 @@ class MapElection extends LeaderElection<string> {
115
122
 
116
123
  protected async tryClaim(name: string) {
117
124
  if (holders.has(name)) return undefined;
125
+ // Only the holder records, and each claim removes the record. Thus a
126
+ // record is always from the term just before this one.
127
+ const afterCleanShutdown = cleanShutdowns.delete(name);
118
128
  lastEpoch += 1n;
119
129
  holders.set(name, lastEpoch);
120
- return lastEpoch;
130
+ return { epoch: lastEpoch, afterCleanShutdown };
121
131
  }
122
132
 
123
133
  // Nothing takes an entry of the map away, so there is nothing to watch.
@@ -125,6 +135,10 @@ class MapElection extends LeaderElection<string> {
125
135
  return { [Symbol.dispose]() {} };
126
136
  }
127
137
 
138
+ protected async recordCleanShutdown(name: string) {
139
+ cleanShutdowns.add(name);
140
+ }
141
+
128
142
  protected async release(name: string) {
129
143
  holders.delete(name);
130
144
  }
@@ -135,11 +149,10 @@ class MapElection extends LeaderElection<string> {
135
149
  const term = await new MapElection('jobs').campaign();
136
150
  console.log('leader:', term?.epoch);
137
151
  console.log('rival:', await new MapElection('jobs').campaign());
138
- await term?.resign();
139
- console.log(
140
- 'rival after the resign:',
141
- (await new MapElection('jobs').campaign())?.epoch,
142
- );
152
+ await term?.resign({ clean: true });
153
+ const next = await new MapElection('jobs').campaign();
154
+ console.log('rival after the resign:', next?.epoch);
155
+ console.log('after a clean shutdown:', next?.afterCleanShutdown);
143
156
  ```
144
157
 
145
158
  Output:
@@ -148,6 +161,7 @@ Output:
148
161
  leader: 1n
149
162
  rival: undefined
150
163
  rival after the resign: 2n
164
+ after a clean shutdown: true
151
165
  ```
152
166
 
153
167
  The socket lock store of `@zukhruf/mutex` uses `SqliteElection`, with the claim file `leader.lock` and the epoch file `leader.epoch`. Its leader serves the other candidates over a socket in the directory, so it works on one host only, also with another backend. Its server stops when `term.signal` aborts. `@zukhruf/single-flight` uses `SqliteElection` the same way, with the claim file `flight.lock` and the epoch file `flight.epoch`, so the two packages never join one election in a shared directory.
@@ -166,6 +180,7 @@ The socket lock store of `@zukhruf/mutex` uses `SqliteElection`, with the claim
166
180
  - [Recipe: Run a job in only one process](./docs/recipes/singleton-job-with-leader-election.md)
167
181
  - [ADR 0001: Leader election is a package, and each backend is a subclass of one campaign](./docs/adr/0001-leader-election-is-a-package-and-each-backend-is-a-subclass.md)
168
182
  - [ADR 0002: A term is a lease on leadership, with no fencing](./docs/adr/0002-a-term-is-a-lease-on-leadership-with-no-fencing.md)
183
+ - [ADR 0003: A term that shuts down cleanly tells its successor](./docs/adr/0003-a-term-that-shuts-down-cleanly-tells-its-successor.md)
169
184
  - [Code copied into this package](./docs/copied-code.md)
170
185
 
171
186
  ## Development
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { type CampaignOptions, LeaderElection } from './leader-election.ts';
2
- export { Term, type TermSteps } from './term.ts';
2
+ export { type ResignOptions, Term, type TermSteps } from './term.ts';
3
3
  export { SqliteElection, type SqliteElectionOptions, } from './sqlite/sqlite-election.ts';
4
4
  export { NetworkDirectoryError } from '@zukhruf/fs';
5
5
  export { LeaseLostError } from '@zukhruf/lease';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,KAAK,aAAa,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,IAAI,EAAkB,MAAM,WAAW,CAAC;AACjD,OAAO,EACL,cAAc,GAEf,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAsB,IAAI,EAAkB,MAAM,WAAW,CAAC;AACrE,OAAO,EACL,cAAc,GAEf,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
@@ -11,9 +11,12 @@ export interface CampaignOptions {
11
11
  * up, watch the won claim, and hand it to a term. Each backend is a
12
12
  * subclass that implements the steps for its claim.
13
13
  *
14
- * A backend keeps four rules:
14
+ * A backend keeps five rules:
15
15
  * - `tryClaim` returns the epoch of the term it won, in the same step, and
16
16
  * each epoch is higher than every earlier one and below 2^31.
17
+ * - `tryClaim` reports `afterCleanShutdown` only when the term just before
18
+ * it recorded a clean shutdown with `recordCleanShutdown`, and it reports
19
+ * each record once.
17
20
  * - `watch` calls `lose` before the backend can grant the claim to another
18
21
  * candidate, so an old leader stops before a new one starts.
19
22
  * - `release` is never called after a loss: the claim may be another
@@ -27,10 +30,20 @@ export declare abstract class LeaderElection<Claim> {
27
30
  campaign({ timeout, signal }?: CampaignOptions): Promise<Term | undefined>;
28
31
  /** Prepares one attempt: the resources that `tryClaim` uses again and again. */
29
32
  protected abstract open(signal: AbortSignal | undefined): Promise<Claim>;
30
- /** Tries the claim once. Resolves with the epoch of the term it won, or `undefined` while another candidate holds it. */
31
- protected abstract tryClaim(claim: Claim, signal: AbortSignal | undefined): Promise<bigint | undefined>;
33
+ /**
34
+ * Tries the claim once. Resolves with the epoch of the term it won and
35
+ * whether the term before it resigned clean, or `undefined` while another
36
+ * candidate holds it.
37
+ */
38
+ protected abstract tryClaim(claim: Claim, signal: AbortSignal | undefined): Promise<Pick<Term, 'epoch' | 'afterCleanShutdown'> | undefined>;
32
39
  /** Watches a won claim for as long as its term lasts, and calls `lose` when the backend takes it away. */
33
40
  protected abstract watch(claim: Claim, lose: (reason: Error) => void): Disposable;
41
+ /**
42
+ * Records that the term of `epoch` resigned clean, so that the next
43
+ * `tryClaim` reports it. Called while the claim is held, before `release`,
44
+ * and never after a loss.
45
+ */
46
+ protected abstract recordCleanShutdown(claim: Claim, epoch: bigint): Promise<void>;
34
47
  /** Gives a won claim up. Never called after a loss. */
35
48
  protected abstract release(claim: Claim): Promise<void>;
36
49
  /** Frees the resources of an attempt, whether it won, lost or failed. */
@@ -1 +1 @@
1
- {"version":3,"file":"leader-election.d.ts","sourceRoot":"","sources":["../src/leader-election.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,4GAA4G;IAC5G,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;;;;;;;;GAcG;AACH,8BAAsB,cAAc,CAAC,KAAK;;IAGxC,SAAS,aAAa,YAAY,EAAE,MAAM;IAI1C,qGAAqG;IAC/F,QAAQ,CAAC,EAAE,OAAW,EAAE,MAAM,EAAE,GAAE,eAAoB,GAAG,OAAO,CACpE,IAAI,GAAG,SAAS,CACjB;IAUD,gFAAgF;IAChF,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC;IAExE,yHAAyH;IACzH,SAAS,CAAC,QAAQ,CAAC,QAAQ,CACzB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAE9B,0GAA0G;IAC1G,SAAS,CAAC,QAAQ,CAAC,KAAK,CACtB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI,GAC5B,UAAU;IAEb,uDAAuD;IACvD,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD,yEAAyE;IACzE,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;CAkDtD"}
1
+ {"version":3,"file":"leader-election.d.ts","sourceRoot":"","sources":["../src/leader-election.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,4GAA4G;IAC5G,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,8BAAsB,cAAc,CAAC,KAAK;;IAGxC,SAAS,aAAa,YAAY,EAAE,MAAM;IAI1C,qGAAqG;IAC/F,QAAQ,CAAC,EAAE,OAAW,EAAE,MAAM,EAAE,GAAE,eAAoB,GAAG,OAAO,CACpE,IAAI,GAAG,SAAS,CACjB;IAUD,gFAAgF;IAChF,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC;IAExE;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,CACzB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,oBAAoB,CAAC,GAAG,SAAS,CAAC;IAElE,0GAA0G;IAC1G,SAAS,CAAC,QAAQ,CAAC,KAAK,CACtB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI,GAC5B,UAAU;IAEb;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,mBAAmB,CACpC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAEhB,uDAAuD;IACvD,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD,yEAAyE;IACzE,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;CA+CtD"}
@@ -1,5 +1,4 @@
1
- import { setTimeout as delay } from 'node:timers/promises';
2
- import { untilAborted } from '@zukhruf/async';
1
+ import { poll } from '@zukhruf/async';
3
2
  import { Term } from "./term.js";
4
3
  /**
5
4
  * Elects one leader among candidates. The campaign is the same for each
@@ -7,9 +6,12 @@ import { Term } from "./term.js";
7
6
  * up, watch the won claim, and hand it to a term. Each backend is a
8
7
  * subclass that implements the steps for its claim.
9
8
  *
10
- * A backend keeps four rules:
9
+ * A backend keeps five rules:
11
10
  * - `tryClaim` returns the epoch of the term it won, in the same step, and
12
11
  * each epoch is higher than every earlier one and below 2^31.
12
+ * - `tryClaim` reports `afterCleanShutdown` only when the term just before
13
+ * it recorded a clean shutdown with `recordCleanShutdown`, and it reports
14
+ * each record once.
13
15
  * - `watch` calls `lose` before the backend can grant the claim to another
14
16
  * candidate, so an old leader stops before a new one starts.
15
17
  * - `release` is never called after a loss: the claim may be another
@@ -34,34 +36,32 @@ export class LeaderElection {
34
36
  }
35
37
  async #attempt(deadline, signal) {
36
38
  const claim = await this.open(signal);
37
- let epoch;
39
+ let won;
38
40
  try {
39
- for (;;) {
40
- epoch = await this.tryClaim(claim, signal);
41
- if (epoch !== undefined)
42
- break;
43
- if (performance.now() >= deadline)
44
- break;
45
- await untilAborted(delay(this.#pollInterval, undefined, { signal }), signal);
46
- }
41
+ won = await poll(() => this.tryClaim(claim, signal), {
42
+ interval: this.#pollInterval,
43
+ signal,
44
+ deadline,
45
+ });
47
46
  }
48
47
  catch (error) {
49
48
  await this.close(claim).catch(() => { });
50
49
  throw error;
51
50
  }
52
- if (epoch === undefined) {
51
+ if (won === undefined) {
53
52
  await this.close(claim);
54
53
  return undefined;
55
54
  }
56
- return this.#begin(claim, epoch, signal);
55
+ return this.#begin(claim, won, signal);
57
56
  }
58
57
  /** Hands a won claim to its term, or gives it up when the caller left meanwhile. */
59
- async #begin(claim, epoch, signal) {
58
+ async #begin(claim, won, signal) {
60
59
  let term;
61
60
  try {
62
61
  signal?.throwIfAborted();
63
- term = new Term(epoch, {
62
+ term = new Term(won, {
64
63
  watch: (lose) => this.watch(claim, lose),
64
+ recordCleanShutdown: () => this.recordCleanShutdown(claim, won.epoch),
65
65
  release: () => this.release(claim),
66
66
  close: () => this.close(claim),
67
67
  });
@@ -1 +1 @@
1
- {"version":3,"file":"leader-election.js","sourceRoot":"","sources":["../src/leader-election.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"leader-election.js","sourceRoot":"","sources":["../src/leader-election.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,14 @@
1
1
  import { FileLock } from '@zukhruf/fs';
2
2
  import { LeaderElection } from '../leader-election.ts';
3
+ import type { Term } from '../term.ts';
3
4
  export interface SqliteElectionOptions {
4
5
  /** The local folder where the candidates meet. */
5
6
  directory: string;
6
7
  /**
7
8
  * The file in `directory` whose file lock is the claim.
8
9
  * Never delete it while candidates run: a new file would let a second
9
- * leader win.
10
+ * leader win. A term that resigns clean writes `<claimFile>.clean` beside
11
+ * it, and the next term reads and removes it.
10
12
  */
11
13
  claimFile: string;
12
14
  /** The file in `directory` that records the epoch of the last term, as decimal text. */
@@ -24,9 +26,15 @@ export declare class SqliteElection extends LeaderElection<FileLock> {
24
26
  #private;
25
27
  constructor({ directory, claimFile, epochFile, pollInterval, }: SqliteElectionOptions);
26
28
  protected open(): Promise<FileLock>;
27
- protected tryClaim(claim: FileLock): Promise<bigint | undefined>;
29
+ protected tryClaim(claim: FileLock): Promise<Pick<Term, 'epoch' | 'afterCleanShutdown'> | undefined>;
28
30
  /** The kernel holds the claim for as long as the process lives, so there is nothing to watch. */
29
31
  protected watch(): Disposable;
32
+ /**
33
+ * A lost note costs the next term only its recovery, so the note needs no
34
+ * sync to the disk: an atomic replace is enough, and a reader never sees a
35
+ * part of it.
36
+ */
37
+ protected recordCleanShutdown(_claim: FileLock, epoch: bigint): Promise<void>;
30
38
  protected release(claim: FileLock): Promise<void>;
31
39
  protected close(claim: FileLock): Promise<void>;
32
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-election.d.ts","sourceRoot":"","sources":["../../src/sqlite/sqlite-election.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,QAAQ,EAIT,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,cAAc,CAAC,QAAQ,CAAC;;gBAK9C,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAiB,GAClB,EAAE,qBAAqB;cAOR,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC;cAMzB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQtE,iGAAiG;IACjG,SAAS,CAAC,KAAK,IAAI,UAAU;cAIb,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;cAIvC,KAAK,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAGtD"}
1
+ {"version":3,"file":"sqlite-election.d.ts","sourceRoot":"","sources":["../../src/sqlite/sqlite-election.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,QAAQ,EAKT,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,WAAW,qBAAqB;IACpC,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,cAAc,CAAC,QAAQ,CAAC;;gBAM9C,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAiB,GAClB,EAAE,qBAAqB;cAQR,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC;cAMzB,QAAQ,CACtB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,oBAAoB,CAAC,GAAG,SAAS,CAAC;IAgBlE,iGAAiG;IACjG,SAAS,CAAC,KAAK,IAAI,UAAU;IAI7B;;;;OAIG;cACa,mBAAmB,CACjC,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;cAIA,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;cAIvC,KAAK,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAGtD"}
@@ -1,6 +1,6 @@
1
- import { mkdir, readFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, rm } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { FileLock, assertLocalDirectory, durableWrite, isErrno, } from '@zukhruf/fs';
3
+ import { FileLock, assertLocalDirectory, atomicWrite, durableWrite, isErrno, } from '@zukhruf/fs';
4
4
  import { LeaderElection } from "../leader-election.js";
5
5
  /**
6
6
  * Elects one leader among the processes of one host that share `directory`.
@@ -12,11 +12,13 @@ export class SqliteElection extends LeaderElection {
12
12
  #directory;
13
13
  #claimPath;
14
14
  #epochPath;
15
+ #cleanShutdownPath;
15
16
  constructor({ directory, claimFile, epochFile, pollInterval = 10, }) {
16
17
  super(pollInterval);
17
18
  this.#directory = directory;
18
19
  this.#claimPath = join(directory, claimFile);
19
20
  this.#epochPath = join(directory, epochFile);
21
+ this.#cleanShutdownPath = `${this.#claimPath}.clean`;
20
22
  }
21
23
  async open() {
22
24
  await assertLocalDirectory(this.#directory);
@@ -27,14 +29,29 @@ export class SqliteElection extends LeaderElection {
27
29
  if (!claim.tryLock())
28
30
  return undefined;
29
31
  // Safe without further locking: only the holder of the claim gets here.
30
- const epoch = (await readEpoch(this.#epochPath)) + 1n;
32
+ const previous = await readEpoch(this.#epochPath);
33
+ // Only a note that names the epoch just before this one tells of a clean
34
+ // shutdown: an older note is from a term that ended long ago.
35
+ const afterCleanShutdown = (await readCleanShutdown(this.#cleanShutdownPath)) ===
36
+ previous.toString();
37
+ const epoch = previous + 1n;
31
38
  await durableWrite(this.#epochPath, epoch.toString());
32
- return epoch;
39
+ // Read once: if the epochs ever start again, an old note could name one of them.
40
+ await rm(this.#cleanShutdownPath, { force: true });
41
+ return { epoch, afterCleanShutdown };
33
42
  }
34
43
  /** The kernel holds the claim for as long as the process lives, so there is nothing to watch. */
35
44
  watch() {
36
45
  return { [Symbol.dispose]() { } };
37
46
  }
47
+ /**
48
+ * A lost note costs the next term only its recovery, so the note needs no
49
+ * sync to the disk: an atomic replace is enough, and a reader never sees a
50
+ * part of it.
51
+ */
52
+ async recordCleanShutdown(_claim, epoch) {
53
+ await atomicWrite(this.#cleanShutdownPath, epoch.toString());
54
+ }
38
55
  async release(claim) {
39
56
  claim.unlock();
40
57
  }
@@ -42,14 +59,34 @@ export class SqliteElection extends LeaderElection {
42
59
  claim[Symbol.dispose]();
43
60
  }
44
61
  }
62
+ /** The epoch that the note names, as its text, or `undefined` when there is no note. */
63
+ async function readCleanShutdown(path) {
64
+ try {
65
+ return await readFile(path, 'utf8');
66
+ }
67
+ catch (error) {
68
+ if (isErrno(error, 'ENOENT'))
69
+ return undefined;
70
+ throw error;
71
+ }
72
+ }
73
+ /**
74
+ * The epoch of the last term, or 0 when there is no epoch file. Only decimal
75
+ * digits are an epoch: `BigInt` alone would also read `''` as 0, and the
76
+ * epochs would start again at 1.
77
+ */
45
78
  async function readEpoch(path) {
79
+ let text;
46
80
  try {
47
- return BigInt(await readFile(path, 'utf8'));
81
+ text = await readFile(path, 'utf8');
48
82
  }
49
83
  catch (error) {
50
84
  if (isErrno(error, 'ENOENT'))
51
85
  return 0n;
52
86
  throw error;
53
87
  }
88
+ if (!/^\d+$/.test(text))
89
+ throw new SyntaxError(`The epoch file ${path} holds ${JSON.stringify(text)}, not decimal digits.`);
90
+ return BigInt(text);
54
91
  }
55
92
  //# sourceMappingURL=sqlite-election.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-election.js","sourceRoot":"","sources":["../../src/sqlite/sqlite-election.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,YAAY,EACZ,OAAO,GACR,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAiBvD;;;;;GAKG;AACH,MAAM,OAAO,cAAe,SAAQ,cAAwB;IACjD,UAAU,CAAS;IACnB,UAAU,CAAS;IACnB,UAAU,CAAS;IAE5B,YAAY,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,GAAG,EAAE,GACK;QACtB,KAAK,CAAC,YAAY,CAAC,CAAC;QACpB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC/C,CAAC;IAES,KAAK,CAAC,IAAI;QAClB,MAAM,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IAES,KAAK,CAAC,QAAQ,CAAC,KAAe;QACtC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;YAAE,OAAO,SAAS,CAAC;QACvC,wEAAwE;QACxE,MAAM,KAAK,GAAG,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,EAAE,CAAC;QACtD,MAAM,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,iGAAiG;IACvF,KAAK;QACb,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAI,CAAC,EAAE,CAAC;IACnC,CAAC;IAES,KAAK,CAAC,OAAO,CAAC,KAAe;QACrC,KAAK,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC;IAES,KAAK,CAAC,KAAK,CAAC,KAAe;QACnC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1B,CAAC;CACF;AAED,KAAK,UAAU,SAAS,CAAC,IAAY;IACnC,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"sqlite-election.js","sourceRoot":"","sources":["../../src/sqlite/sqlite-election.ts"],"names":[],"mappings":"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"}
package/dist/term.d.ts CHANGED
@@ -3,11 +3,24 @@ import { type Lease } from '@zukhruf/lease';
3
3
  export interface TermSteps {
4
4
  /** Starts watching the claim; `lose` reports that the backend took it away. */
5
5
  watch(lose: (reason: Error) => void): Disposable;
6
+ /**
7
+ * Records that the term resigned clean, while it still holds the claim.
8
+ * Called only for a term that was not lost, before `release`.
9
+ */
10
+ recordCleanShutdown(): Promise<void>;
6
11
  /** Gives the claim up. Called only for a term that was not lost. */
7
12
  release(): Promise<void>;
8
13
  /** Frees the claim's resources. Called at each end. */
9
14
  close(): Promise<void>;
10
15
  }
16
+ export interface ResignOptions {
17
+ /**
18
+ * The leader says that it leaves nothing for its successor to recover,
19
+ * for example no work in progress. The next term then reads
20
+ * `afterCleanShutdown` as true. A lost term never says it.
21
+ */
22
+ clean: boolean;
23
+ }
11
24
  /**
12
25
  * One term of the elected leader: a lease on leadership. It ends when the
13
26
  * leader resigns, when its process dies, or when the backend takes the claim
@@ -18,15 +31,22 @@ export declare class Term implements Lease, AsyncDisposable {
18
31
  #private;
19
32
  /** Higher than the epoch of each earlier term, so a newer leader can always outrank an older one. */
20
33
  readonly epoch: bigint;
21
- constructor(epoch: bigint, steps: TermSteps);
34
+ /**
35
+ * True only when the term just before this one resigned clean: its leader
36
+ * left nothing to recover. False after a crash, a lost term, a resign that
37
+ * did not say it was clean, and for the first term.
38
+ */
39
+ readonly afterCleanShutdown: boolean;
40
+ constructor({ epoch, afterCleanShutdown }: Pick<Term, 'epoch' | 'afterCleanShutdown'>, steps: TermSteps);
22
41
  /** Aborts with `LeaseLostError` when the term ends while its leader still runs. */
23
42
  get signal(): AbortSignal;
24
43
  /**
25
- * Ends the term and gives the claim up. A second call waits for the first.
26
- * After a loss, it only waits until the claim's resources are free, and it
27
- * never rejects.
44
+ * Ends the term and gives the claim up. With `clean`, it first records a
45
+ * clean shutdown for the next term. A second call waits for the first, and
46
+ * its options do nothing. After a loss, it only waits until the claim's
47
+ * resources are free, records nothing, and never rejects.
28
48
  */
29
- resign(): Promise<void>;
49
+ resign(options?: ResignOptions): Promise<void>;
30
50
  [Symbol.asyncDispose](): Promise<void>;
31
51
  }
32
52
  //# sourceMappingURL=term.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"term.d.ts","sourceRoot":"","sources":["../src/term.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,EAAmB,MAAM,gBAAgB,CAAC;AAE7D,4FAA4F;AAC5F,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI,GAAG,UAAU,CAAC;IACjD,oEAAoE;IACpE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,uDAAuD;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAKD;;;;;GAKG;AACH,qBAAa,IAAK,YAAW,KAAK,EAAE,eAAe;;IACjD,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAQX,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS;IAM3C,mFAAmF;IACnF,IAAI,MAAM,IAAI,WAAW,CAExB;IAED;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAU7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;CA+CvC"}
1
+ {"version":3,"file":"term.d.ts","sourceRoot":"","sources":["../src/term.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,EAAmB,MAAM,gBAAgB,CAAC;AAE7D,4FAA4F;AAC5F,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI,GAAG,UAAU,CAAC;IACjD;;;OAGG;IACH,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,oEAAoE;IACpE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,uDAAuD;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;CAChB;AAKD;;;;;GAKG;AACH,qBAAa,IAAK,YAAW,KAAK,EAAE,eAAe;;IACjD,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;gBASnC,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,oBAAoB,CAAC,EACzE,KAAK,EAAE,SAAS;IAQlB,mFAAmF;IACnF,IAAI,MAAM,IAAI,WAAW,CAExB;IAED;;;;;OAKG;IACG,MAAM,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpD,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;CAiDvC"}
package/dist/term.js CHANGED
@@ -9,14 +9,21 @@ import { LeaseController } from '@zukhruf/lease';
9
9
  export class Term {
10
10
  /** Higher than the epoch of each earlier term, so a newer leader can always outrank an older one. */
11
11
  epoch;
12
+ /**
13
+ * True only when the term just before this one resigned clean: its leader
14
+ * left nothing to recover. False after a crash, a lost term, a resign that
15
+ * did not say it was clean, and for the first term.
16
+ */
17
+ afterCleanShutdown;
12
18
  #steps;
13
19
  #lease = new LeaseController('leadership');
14
20
  /** Aborts once the term starts to end, by resign or by loss. */
15
21
  #ending = new AbortController();
16
22
  #ended = new Latch();
17
23
  #watching = new DisposableStack();
18
- constructor(epoch, steps) {
24
+ constructor({ epoch, afterCleanShutdown }, steps) {
19
25
  this.epoch = epoch;
26
+ this.afterCleanShutdown = afterCleanShutdown;
20
27
  this.#steps = steps;
21
28
  this.#watching.use(steps.watch((reason) => this.#lose(reason)));
22
29
  }
@@ -25,15 +32,16 @@ export class Term {
25
32
  return this.#lease.signal;
26
33
  }
27
34
  /**
28
- * Ends the term and gives the claim up. A second call waits for the first.
29
- * After a loss, it only waits until the claim's resources are free, and it
30
- * never rejects.
35
+ * Ends the term and gives the claim up. With `clean`, it first records a
36
+ * clean shutdown for the next term. A second call waits for the first, and
37
+ * its options do nothing. After a loss, it only waits until the claim's
38
+ * resources are free, records nothing, and never rejects.
31
39
  */
32
- async resign() {
40
+ async resign(options) {
33
41
  if (!this.#ending.signal.aborted) {
34
42
  this.#ending.abort();
35
43
  this.#lease.end();
36
- this.#ended.open(await this.#giveUp());
44
+ this.#ended.open(await this.#giveUp(options?.clean === true));
37
45
  }
38
46
  const ending = await this.#ended.wait();
39
47
  if (ending.failed)
@@ -56,9 +64,12 @@ export class Term {
56
64
  .then(() => this.#freeLost())
57
65
  .then((ending) => this.#ended.open(ending));
58
66
  }
59
- async #giveUp() {
67
+ async #giveUp(clean) {
60
68
  try {
61
69
  this.#watching.dispose();
70
+ // Before the release: until then no other candidate can hold the claim.
71
+ if (clean)
72
+ await this.#steps.recordCleanShutdown();
62
73
  await this.#steps.release();
63
74
  }
64
75
  catch (error) {
package/dist/term.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"term.js","sourceRoot":"","sources":["../src/term.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"term.js","sourceRoot":"","sources":["../src/term.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zukhruf/election",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Elect one leader among the processes that share a directory, with a term that ends when the leader dies and an epoch that grows with each term. Backends are subclasses of one campaign.",
5
5
  "license": "MIT",
6
6
  "repository": {