@einaraglen/quorum 1.0.2 → 1.0.3
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 +66 -45
- package/dist/core/bully.d.ts +3 -3
- package/dist/core/bully.d.ts.map +1 -1
- package/dist/core/bully.js +5 -5
- package/dist/core/forum.d.ts +3 -3
- package/dist/core/forum.d.ts.map +1 -1
- package/dist/core/forum.js +7 -8
- package/dist/core/quorum.d.ts +6 -2
- package/dist/core/quorum.d.ts.map +1 -1
- package/dist/core/quorum.js +38 -12
- package/dist/core/sharding.d.ts +9 -3
- package/dist/core/sharding.d.ts.map +1 -1
- package/dist/core/sharding.js +19 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -18,13 +18,13 @@ When a pod starts up it runs an election: it contacts every peer with a higher n
|
|
|
18
18
|
|
|
19
19
|
All coordination travels over a single internal HTTP port (default `4001`). Each pod runs a small Express server on that port with five endpoints:
|
|
20
20
|
|
|
21
|
-
| Route
|
|
22
|
-
|
|
23
|
-
| `GET /health`
|
|
24
|
-
| `POST /bully/election`
|
|
25
|
-
| `POST /bully/coordinator`
|
|
26
|
-
| `POST /bully/message`
|
|
27
|
-
| `GET /channel/stream/:event` | SSE stream for live subscriptions
|
|
21
|
+
| Route | Purpose |
|
|
22
|
+
| ---------------------------- | --------------------------------------------- |
|
|
23
|
+
| `GET /health` | Heartbeat probe |
|
|
24
|
+
| `POST /bully/election` | Incoming election challenge from a lower peer |
|
|
25
|
+
| `POST /bully/coordinator` | Incoming leader announcement |
|
|
26
|
+
| `POST /bully/message` | Incoming application-level event |
|
|
27
|
+
| `GET /channel/stream/:event` | SSE stream for live subscriptions |
|
|
28
28
|
|
|
29
29
|
The Transport class owns both sides of this: the server that listens on those routes, and the HTTP client (`postToPeer`, `pingPeer`) that sends to them.
|
|
30
30
|
|
|
@@ -32,12 +32,12 @@ The Transport class owns both sides of this: the server that listens on those ro
|
|
|
32
32
|
|
|
33
33
|
Four messaging patterns are available, each with a different routing model:
|
|
34
34
|
|
|
35
|
-
| Method
|
|
36
|
-
|
|
37
|
-
| `broadcast(event, payload)`
|
|
38
|
-
| `send(event, payload)`
|
|
39
|
-
| `tell(peer, event, payload)`
|
|
40
|
-
| `messagePeer(peer, event, payload)` | Anyone
|
|
35
|
+
| Method | Who can call | Who receives |
|
|
36
|
+
| ----------------------------------- | ------------ | ----------------------- |
|
|
37
|
+
| `broadcast(event, payload)` | Leader only | All followers + self |
|
|
38
|
+
| `send(event, payload)` | Any follower | Current leader |
|
|
39
|
+
| `tell(peer, event, payload)` | Leader only | One specific named peer |
|
|
40
|
+
| `messagePeer(peer, event, payload)` | Anyone | One specific named peer |
|
|
41
41
|
|
|
42
42
|
All four deliver to the pod's local `channel` EventEmitter when the target happens to be self, with no network round-trip.
|
|
43
43
|
|
|
@@ -45,7 +45,7 @@ All four deliver to the pod's local `channel` EventEmitter when the target happe
|
|
|
45
45
|
|
|
46
46
|
`subscribeToPeer(peer, event, onData)` opens a live SSE connection to a named peer and delivers every event it emits under that name. Returns an unsubscribe function. Targets itself locally — no loopback HTTP connection.
|
|
47
47
|
|
|
48
|
-
###
|
|
48
|
+
### Sharding — work distribution
|
|
49
49
|
|
|
50
50
|
The leader periodically reconciles who owns what. Reconciliation works in four steps:
|
|
51
51
|
|
|
@@ -60,20 +60,20 @@ Ownership changes drive subscriptions: `subscribe(id, onEvent)` listens to the c
|
|
|
60
60
|
|
|
61
61
|
### Quorum — the wrapper
|
|
62
62
|
|
|
63
|
-
`Quorum` creates and wires all
|
|
63
|
+
`Quorum` creates and wires all four layers (Transport, Bully, Forum, Sharding) from a single options object. It is the primary entry point for production use. The individual classes are exported for testing and advanced composition.
|
|
64
64
|
|
|
65
65
|
---
|
|
66
66
|
|
|
67
67
|
## Quick start
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
|
-
import { Quorum } from "quorum";
|
|
71
|
-
import {
|
|
70
|
+
import { Quorum } from "@einaraglen/quorum";
|
|
71
|
+
import { discovery } from "./my-discovery.js"; // returns { self, cluster }
|
|
72
72
|
|
|
73
73
|
const q = new Quorum({
|
|
74
|
-
|
|
74
|
+
discovery,
|
|
75
75
|
ids: ["uuid-1", "uuid-2", "uuid-3"], // the full set of work items to distribute
|
|
76
|
-
expectedClusterSize: 4,
|
|
76
|
+
expectedClusterSize: 4, // enables quorum protection
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
q.start(); // starts the HTTP server, runs the first election, begins reconciling
|
|
@@ -89,6 +89,10 @@ q.publish("uuid-1", { reading: 42 });
|
|
|
89
89
|
// Look up who owns an id without a network call:
|
|
90
90
|
console.log(q.getOwner("uuid-1")); // "pod-name-c"
|
|
91
91
|
|
|
92
|
+
// React the instant this pod gains or loses ids — no polling getHeldIds():
|
|
93
|
+
q.onAssigned((ids) => console.log("now holding:", ids));
|
|
94
|
+
q.onReleased((ids) => console.log("no longer holding:", ids));
|
|
95
|
+
|
|
92
96
|
// Clean shutdown (or use `using q = new Quorum(...)` for automatic teardown):
|
|
93
97
|
q.stop();
|
|
94
98
|
```
|
|
@@ -97,7 +101,7 @@ q.stop();
|
|
|
97
101
|
|
|
98
102
|
```typescript
|
|
99
103
|
async function main() {
|
|
100
|
-
using q = new Quorum({
|
|
104
|
+
using q = new Quorum({ discovery, ids });
|
|
101
105
|
q.start();
|
|
102
106
|
|
|
103
107
|
await new Promise<void>((resolve) => {
|
|
@@ -114,22 +118,22 @@ async function main() {
|
|
|
114
118
|
|
|
115
119
|
### `new Quorum(opts)`
|
|
116
120
|
|
|
117
|
-
| Option
|
|
118
|
-
|
|
119
|
-
| `
|
|
120
|
-
| `ids`
|
|
121
|
-
| `fetchFn`
|
|
122
|
-
| `internalPort`
|
|
123
|
-
| `internalHost`
|
|
124
|
-
| `requestTimeoutMs`
|
|
125
|
-
| `coordinatorWaitMs`
|
|
126
|
-
| `heartbeatIntervalMs`
|
|
127
|
-
| `reportTimeoutMs`
|
|
128
|
-
| `rebalanceIntervalMs`
|
|
129
|
-
| `expectedClusterSize`
|
|
130
|
-
| `quorumFailureThreshold` | `number`
|
|
131
|
-
| `onSustainedQuorumLoss`
|
|
132
|
-
| `logger`
|
|
121
|
+
| Option | Type | Default | Description |
|
|
122
|
+
| ------------------------ | ---------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `discovery` | `() => Promise<{ self, cluster }>` | required | Peer discovery. Return this pod as `self` and the full peer list (including self) as `cluster`. |
|
|
124
|
+
| `ids` | `TId[]` | required | The complete set of work IDs to distribute. |
|
|
125
|
+
| `fetchFn` | `typeof fetch` | `globalThis.fetch` | Swap in a custom fetch for testing. |
|
|
126
|
+
| `internalPort` | `number` | `4001` or `$INTERNAL_PORT` | Port for the internal coordination server. |
|
|
127
|
+
| `internalHost` | `string` | all interfaces | Bind address for the internal server. Useful in tests running multiple real instances on one machine. |
|
|
128
|
+
| `requestTimeoutMs` | `number` | `2000` | Timeout for outgoing HTTP requests to peers. |
|
|
129
|
+
| `coordinatorWaitMs` | `number` | `4000` | How long a pod waits for a coordinator announcement before restarting the election. |
|
|
130
|
+
| `heartbeatIntervalMs` | `number` | `5000` | How often followers ping the leader to check it is still alive. |
|
|
131
|
+
| `reportTimeoutMs` | `number` | `2000` | How long the leader waits for holdings reports before reconciling with whoever responded. |
|
|
132
|
+
| `rebalanceIntervalMs` | `number` | `10000` | How often the leader runs a full reconcile. |
|
|
133
|
+
| `expectedClusterSize` | `number` | none | Target pod count for quorum checks. |
|
|
134
|
+
| `quorumFailureThreshold` | `number` | `3` | Consecutive quorum failures before `onSustainedQuorumLoss` fires. |
|
|
135
|
+
| `onSustainedQuorumLoss` | `() => void` | `process.exit(1)` | Called once when the failure threshold is reached. |
|
|
136
|
+
| `logger` | `Logger` | `console` | Log sink, must implement `info`/`warn`/`error`/`debug`. `console` satisfies this as-is. |
|
|
133
137
|
|
|
134
138
|
**Methods:**
|
|
135
139
|
|
|
@@ -146,6 +150,9 @@ q.getOwner(id): string | undefined // local lookup — no network call
|
|
|
146
150
|
q.getOwnership(): Map<TId, string> // full id→peer map as of last reconcile
|
|
147
151
|
q.getHeldIds(): TId[] // ids currently assigned to this pod
|
|
148
152
|
q.updateIds(ids): Promise<void> // leader-only: replace the full id set and reconcile
|
|
153
|
+
|
|
154
|
+
q.onAssigned(onEvent): () => void // fires with the ids just gained when held ids grow
|
|
155
|
+
q.onReleased(onEvent): () => void // fires with the ids just lost when held ids shrink
|
|
149
156
|
```
|
|
150
157
|
|
|
151
158
|
---
|
|
@@ -155,12 +162,12 @@ q.updateIds(ids): Promise<void> // leader-only: replace the full id set a
|
|
|
155
162
|
For testing or custom wiring you can create the layers individually:
|
|
156
163
|
|
|
157
164
|
```typescript
|
|
158
|
-
import { Transport, Bully, Forum,
|
|
165
|
+
import { Transport, Bully, Forum, Sharding } from "@einaraglen/quorum";
|
|
159
166
|
|
|
160
167
|
const transport = new Transport({ fetchFn: mockFetch, internalPort: 9000 });
|
|
161
|
-
const bully = new Bully({
|
|
162
|
-
const forum = new Forum({ bully, transport,
|
|
163
|
-
const shard = new
|
|
168
|
+
const bully = new Bully({ discovery, transport });
|
|
169
|
+
const forum = new Forum({ bully, transport, discovery });
|
|
170
|
+
const shard = new Sharding({ bully, forum, ids: [] });
|
|
164
171
|
|
|
165
172
|
// Wire the transport callbacks manually:
|
|
166
173
|
transport.start({
|
|
@@ -183,12 +190,15 @@ await bully.startElection();
|
|
|
183
190
|
bully.channel.emit("assign-connections", [7, 12, 44]);
|
|
184
191
|
|
|
185
192
|
// Simulate the leader publishing an updated ownership map:
|
|
186
|
-
bully.channel.emit("ownership-map", [
|
|
193
|
+
bully.channel.emit("ownership-map", [
|
|
194
|
+
[7, "pod-c"],
|
|
195
|
+
[12, "pod-a"],
|
|
196
|
+
]);
|
|
187
197
|
```
|
|
188
198
|
|
|
189
199
|
### Lifecycle events
|
|
190
200
|
|
|
191
|
-
`bully.lifecycle` emits `"elected"` when this pod becomes leader (after peers have been notified), and `"demoted"` when it steps down.
|
|
201
|
+
`bully.lifecycle` emits `"elected"` when this pod becomes leader (after peers have been notified), and `"demoted"` when it steps down. Sharding uses these internally; you can also listen directly:
|
|
192
202
|
|
|
193
203
|
```typescript
|
|
194
204
|
bully.lifecycle.on("elected", () => {
|
|
@@ -199,11 +209,22 @@ bully.lifecycle.on("demoted", () => {
|
|
|
199
209
|
});
|
|
200
210
|
```
|
|
201
211
|
|
|
212
|
+
`shard.lifecycle` similarly emits `"assigned"` and `"released"`, each with the delta of ids that just changed (not the full held set) — this is what `Quorum.onAssigned`/`onReleased` wrap:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
shard.lifecycle.on("assigned", (ids) => {
|
|
216
|
+
console.log("now holding:", ids);
|
|
217
|
+
});
|
|
218
|
+
shard.lifecycle.on("released", (ids) => {
|
|
219
|
+
console.log("no longer holding:", ids);
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
202
223
|
---
|
|
203
224
|
|
|
204
225
|
## Peer discovery
|
|
205
226
|
|
|
206
|
-
`
|
|
227
|
+
`discovery` is the only application-specific piece. It must return:
|
|
207
228
|
|
|
208
229
|
```typescript
|
|
209
230
|
{
|
|
@@ -216,7 +237,7 @@ bully.lifecycle.on("demoted", () => {
|
|
|
216
237
|
|
|
217
238
|
```typescript
|
|
218
239
|
// In your application layer (not in quorum itself):
|
|
219
|
-
const
|
|
240
|
+
const discovery = async () => {
|
|
220
241
|
const pods = await k8s.listNamespacedPod({
|
|
221
242
|
namespace: "default",
|
|
222
243
|
labelSelector: "app=my-service",
|
|
@@ -255,5 +276,5 @@ src/
|
|
|
255
276
|
Every class (and `Quorum` itself) accepts an optional `logger` implementing `info`/`warn`/`error`/`debug` — `console` satisfies this shape as-is and is the default. Pass your own (winston, pino, a wrapper around your APM, etc.) to route quorum's election/reconcile/messaging logs wherever the rest of your app's logs go:
|
|
256
277
|
|
|
257
278
|
```typescript
|
|
258
|
-
const q = new Quorum({
|
|
279
|
+
const q = new Quorum({ discovery, ids, logger: myWinstonLogger });
|
|
259
280
|
```
|
package/dist/core/bully.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ export type BullyCluster = {
|
|
|
9
9
|
self: BullyPeer;
|
|
10
10
|
cluster: BullyPeer[];
|
|
11
11
|
};
|
|
12
|
-
export type
|
|
12
|
+
export type Discovery = () => Promise<BullyCluster>;
|
|
13
13
|
export type BullyOptions = {
|
|
14
|
-
|
|
14
|
+
discovery: Discovery;
|
|
15
15
|
transport: Transport;
|
|
16
16
|
coordinatorWaitMs?: number;
|
|
17
17
|
heartbeatIntervalMs?: number;
|
|
@@ -20,7 +20,7 @@ export type BullyOptions = {
|
|
|
20
20
|
export declare class Bully implements Disposable {
|
|
21
21
|
readonly lifecycle: EventEmitter<[never]>;
|
|
22
22
|
readonly channel: EventEmitter<[never]>;
|
|
23
|
-
private readonly
|
|
23
|
+
private readonly discovery;
|
|
24
24
|
private readonly transport;
|
|
25
25
|
private readonly coordinatorWaitMs;
|
|
26
26
|
private readonly heartbeatIntervalMs;
|
package/dist/core/bully.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bully.d.ts","sourceRoot":"","sources":["../../src/core/bully.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC;AACrE,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"bully.d.ts","sourceRoot":"","sources":["../../src/core/bully.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC;AACrE,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,KAAM,YAAW,UAAU;IACtC,SAAgB,SAAS,wBAAsB;IAC/C,SAAgB,OAAO,wBAAsB;IAE7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,sBAAsB,CAAC,CAAiB;IAChD,OAAO,CAAC,iBAAiB,CAAC,CAAiB;IAE3C,YAAY,IAAI,EAAE,YAAY,EAM7B;IAEM,QAAQ,IAAI,OAAO,CAEzB;IAEM,SAAS;QACL,IAAI;QAAe,MAAM;QAAiB,QAAQ;MAC5D;IAEY,WAAW;;;;SAOvB;IAED,4EAA4E;IAC/D,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAI5C;YAEa,YAAY;IAeb,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkC1C;IAEM,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAG7C;IAEM,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAa5C;IAEM,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAEtD;IAEM,KAAK,IAAI,IAAI,CAuBnB;IAEM,IAAI,IAAI,IAAI,CAIlB;IAEM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAE9B;CACF"}
|
package/dist/core/bully.js
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from "events";
|
|
|
2
2
|
export class Bully {
|
|
3
3
|
lifecycle = new EventEmitter();
|
|
4
4
|
channel = new EventEmitter();
|
|
5
|
-
|
|
5
|
+
discovery;
|
|
6
6
|
transport;
|
|
7
7
|
coordinatorWaitMs;
|
|
8
8
|
heartbeatIntervalMs;
|
|
@@ -13,7 +13,7 @@ export class Bully {
|
|
|
13
13
|
coordinatorWaitTimeout;
|
|
14
14
|
heartbeatInterval;
|
|
15
15
|
constructor(opts) {
|
|
16
|
-
this.
|
|
16
|
+
this.discovery = opts.discovery;
|
|
17
17
|
this.transport = opts.transport;
|
|
18
18
|
this.coordinatorWaitMs = opts.coordinatorWaitMs ?? 4000;
|
|
19
19
|
this.heartbeatIntervalMs = opts.heartbeatIntervalMs ?? 5000;
|
|
@@ -26,7 +26,7 @@ export class Bully {
|
|
|
26
26
|
return { self: this.selfId, leader: this.leaderId, isLeader: this.isLeader() };
|
|
27
27
|
}
|
|
28
28
|
async getPodRoles() {
|
|
29
|
-
const { cluster } = await this.
|
|
29
|
+
const { cluster } = await this.discovery();
|
|
30
30
|
return cluster.map((pod) => ({
|
|
31
31
|
name: pod.name,
|
|
32
32
|
host: pod.host,
|
|
@@ -35,7 +35,7 @@ export class Bully {
|
|
|
35
35
|
}
|
|
36
36
|
/** Returns all cluster peers except self. Side-effect: populates selfId. */
|
|
37
37
|
async getPeers() {
|
|
38
|
-
const { self, cluster } = await this.
|
|
38
|
+
const { self, cluster } = await this.discovery();
|
|
39
39
|
this.selfId = self.name;
|
|
40
40
|
return cluster.filter((pod) => pod.name !== self.name && pod.host);
|
|
41
41
|
}
|
|
@@ -46,7 +46,7 @@ export class Bully {
|
|
|
46
46
|
this.logger.info(`Became leader (id=${this.selfId})`);
|
|
47
47
|
await Promise.all(peers.map((peer) => this.transport.postToPeer(peer.host, "/bully/coordinator", { id: this.selfId })));
|
|
48
48
|
// Emitted only after peers have been told, so an "elected" listener that immediately talks
|
|
49
|
-
// to peers (e.g.
|
|
49
|
+
// to peers (e.g. Sharding.reconcile()) doesn't race ahead of them learning who's leader.
|
|
50
50
|
if (!wasLeader)
|
|
51
51
|
this.lifecycle.emit("elected");
|
|
52
52
|
}
|
package/dist/core/forum.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { Logger } from "./logger";
|
|
2
|
-
import type { Bully, BullyPeer,
|
|
2
|
+
import type { Bully, BullyPeer, Discovery } from "./bully";
|
|
3
3
|
import type { Transport } from "./transport";
|
|
4
4
|
export type ForumOptions = {
|
|
5
5
|
bully: Bully;
|
|
6
6
|
transport: Transport;
|
|
7
|
-
|
|
7
|
+
discovery: Discovery;
|
|
8
8
|
fetchFn?: typeof fetch;
|
|
9
9
|
logger?: Logger;
|
|
10
10
|
};
|
|
11
11
|
export declare class Forum {
|
|
12
12
|
private readonly bully;
|
|
13
13
|
private readonly transport;
|
|
14
|
-
private readonly
|
|
14
|
+
private readonly discovery;
|
|
15
15
|
private readonly fetchFn;
|
|
16
16
|
private readonly logger;
|
|
17
17
|
constructor(opts: ForumOptions);
|
package/dist/core/forum.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forum.d.ts","sourceRoot":"","sources":["../../src/core/forum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"forum.d.ts","sourceRoot":"","sources":["../../src/core/forum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,YAAY,IAAI,EAAE,YAAY,EAM7B;IAED,sFAAsF;IACzE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtE;IAED,+EAA+E;IAClE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBjE;IAED,kFAAkF;IACrE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnF;IAED;;;OAGG;IACU,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa1F;IAED;;;OAGG;IACU,UAAU,CAAC,CAAC,EACvB,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,GAC3E,OAAO,CAAC,IAAI,CAAC,CAoBf;IAED;;;;OAIG;IACI,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CA+DtG;CACF"}
|
package/dist/core/forum.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export class Forum {
|
|
2
2
|
bully;
|
|
3
3
|
transport;
|
|
4
|
-
|
|
4
|
+
discovery;
|
|
5
5
|
fetchFn;
|
|
6
6
|
logger;
|
|
7
7
|
constructor(opts) {
|
|
8
8
|
this.bully = opts.bully;
|
|
9
9
|
this.transport = opts.transport;
|
|
10
|
-
this.
|
|
10
|
+
this.discovery = opts.discovery;
|
|
11
11
|
this.fetchFn = opts.fetchFn ?? this.transport.fetchFn;
|
|
12
12
|
this.logger = opts.logger ?? console;
|
|
13
13
|
}
|
|
@@ -75,11 +75,8 @@ export class Forum {
|
|
|
75
75
|
this.logger.warn(`distribute('${event}') called while not leader, ignoring`);
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
const { self, cluster } = await this.
|
|
79
|
-
const allPeers = [
|
|
80
|
-
self,
|
|
81
|
-
...cluster.filter((pod) => pod.name !== self.name && pod.host),
|
|
82
|
-
].sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
78
|
+
const { self, cluster } = await this.discovery();
|
|
79
|
+
const allPeers = [self, ...cluster.filter((pod) => pod.name !== self.name && pod.host)].sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
83
80
|
await Promise.all(allPeers.map((peer, index) => {
|
|
84
81
|
const payload = resolvePayload(peer, index, allPeers);
|
|
85
82
|
if (peer.name === self.name) {
|
|
@@ -110,7 +107,9 @@ export class Forum {
|
|
|
110
107
|
return;
|
|
111
108
|
}
|
|
112
109
|
try {
|
|
113
|
-
const res = await this.fetchFn(`http://${target.host}:${this.transport.port}/channel/stream/${event}`, {
|
|
110
|
+
const res = await this.fetchFn(`http://${target.host}:${this.transport.port}/channel/stream/${event}`, {
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
});
|
|
114
113
|
if (!res.ok || !res.body) {
|
|
115
114
|
this.logger.warn(`subscribeToPeer('${event}') to '${peerName}' failed to connect`);
|
|
116
115
|
return;
|
package/dist/core/quorum.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Discovery } from "./bully";
|
|
2
2
|
import type { Logger } from "./logger";
|
|
3
3
|
export type QuorumOptions<TId = string> = {
|
|
4
|
-
|
|
4
|
+
discovery: Discovery;
|
|
5
5
|
ids: TId[];
|
|
6
6
|
fetchFn?: typeof fetch;
|
|
7
7
|
internalPort?: number;
|
|
@@ -40,6 +40,10 @@ export declare class Quorum<TId = string> implements Disposable {
|
|
|
40
40
|
getOwnership(): Map<TId, string>;
|
|
41
41
|
getHeldIds(): TId[];
|
|
42
42
|
updateIds(ids: TId[]): Promise<void>;
|
|
43
|
+
/** Fires with the ids just gained whenever this pod's held ids grow. Returns an unsubscribe function. */
|
|
44
|
+
onAssigned(onEvent: (ids: TId[]) => void): () => void;
|
|
45
|
+
/** Fires with the ids just lost whenever this pod's held ids shrink. Returns an unsubscribe function. */
|
|
46
|
+
onReleased(onEvent: (ids: TId[]) => void): () => void;
|
|
43
47
|
start(): void;
|
|
44
48
|
stop(): void;
|
|
45
49
|
[Symbol.dispose](): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quorum.d.ts","sourceRoot":"","sources":["../../src/core/quorum.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,
|
|
1
|
+
{"version":3,"file":"quorum.d.ts","sourceRoot":"","sources":["../../src/core/quorum.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,aAAa,CAAC,GAAG,GAAG,MAAM,IAAI;IACxC,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,GAAG,EAAE,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,MAAM,CAAC,GAAG,GAAG,MAAM,CAAE,YAAW,UAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IAEtC,YAAY,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,EAiCnC;IAEM,QAAQ,IAAI,OAAO,CAEzB;IACM,SAAS;;;;MAEf;IACY,WAAW;;;;SAEvB;IAEM,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAEzE;IACM,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAE9C;IACM,QAAQ,CAAC,EAAE,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAE3C;IACM,YAAY,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAEtC;IACM,UAAU,IAAI,GAAG,EAAE,CAEzB;IACY,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhD;IAED,yGAAyG;IAClG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAG3D;IACD,yGAAyG;IAClG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAG3D;IAEM,KAAK,IAAI,IAAI,CAUnB;IAEM,IAAI,IAAI,IAAI,CAIlB;IAEM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAE9B;CACF"}
|
package/dist/core/quorum.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Transport } from "./transport";
|
|
2
2
|
import { Bully } from "./bully";
|
|
3
3
|
import { Forum } from "./forum";
|
|
4
|
-
import {
|
|
4
|
+
import { Sharding } from "./sharding";
|
|
5
5
|
export class Quorum {
|
|
6
6
|
transport;
|
|
7
7
|
bully;
|
|
@@ -16,7 +16,7 @@ export class Quorum {
|
|
|
16
16
|
logger: opts.logger,
|
|
17
17
|
});
|
|
18
18
|
this.bully = new Bully({
|
|
19
|
-
|
|
19
|
+
discovery: opts.discovery,
|
|
20
20
|
transport: this.transport,
|
|
21
21
|
coordinatorWaitMs: opts.coordinatorWaitMs,
|
|
22
22
|
heartbeatIntervalMs: opts.heartbeatIntervalMs,
|
|
@@ -25,11 +25,11 @@ export class Quorum {
|
|
|
25
25
|
this.forum = new Forum({
|
|
26
26
|
bully: this.bully,
|
|
27
27
|
transport: this.transport,
|
|
28
|
-
|
|
28
|
+
discovery: opts.discovery,
|
|
29
29
|
fetchFn: opts.fetchFn,
|
|
30
30
|
logger: opts.logger,
|
|
31
31
|
});
|
|
32
|
-
this.shard = new
|
|
32
|
+
this.shard = new Sharding({
|
|
33
33
|
bully: this.bully,
|
|
34
34
|
forum: this.forum,
|
|
35
35
|
ids: opts.ids,
|
|
@@ -41,17 +41,43 @@ export class Quorum {
|
|
|
41
41
|
logger: opts.logger,
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
isLeader() {
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
isLeader() {
|
|
45
|
+
return this.bully.isLeader();
|
|
46
|
+
}
|
|
47
|
+
getStatus() {
|
|
48
|
+
return this.bully.getStatus();
|
|
49
|
+
}
|
|
50
|
+
async getPodRoles() {
|
|
51
|
+
return this.bully.getPodRoles();
|
|
52
|
+
}
|
|
47
53
|
subscribe(id, onEvent) {
|
|
48
54
|
return this.shard.subscribe(id, onEvent);
|
|
49
55
|
}
|
|
50
|
-
publish(id, payload) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
publish(id, payload) {
|
|
57
|
+
this.shard.publish(id, payload);
|
|
58
|
+
}
|
|
59
|
+
getOwner(id) {
|
|
60
|
+
return this.shard.getOwner(id);
|
|
61
|
+
}
|
|
62
|
+
getOwnership() {
|
|
63
|
+
return this.shard.getOwnership();
|
|
64
|
+
}
|
|
65
|
+
getHeldIds() {
|
|
66
|
+
return this.shard.getHeldIds();
|
|
67
|
+
}
|
|
68
|
+
async updateIds(ids) {
|
|
69
|
+
return this.shard.updateIds(ids);
|
|
70
|
+
}
|
|
71
|
+
/** Fires with the ids just gained whenever this pod's held ids grow. Returns an unsubscribe function. */
|
|
72
|
+
onAssigned(onEvent) {
|
|
73
|
+
this.shard.lifecycle.on("assigned", onEvent);
|
|
74
|
+
return () => this.shard.lifecycle.off("assigned", onEvent);
|
|
75
|
+
}
|
|
76
|
+
/** Fires with the ids just lost whenever this pod's held ids shrink. Returns an unsubscribe function. */
|
|
77
|
+
onReleased(onEvent) {
|
|
78
|
+
this.shard.lifecycle.on("released", onEvent);
|
|
79
|
+
return () => this.shard.lifecycle.off("released", onEvent);
|
|
80
|
+
}
|
|
55
81
|
start() {
|
|
56
82
|
this.transport.start({
|
|
57
83
|
onElectionMessage: (id) => this.bully.onElectionMessage(id),
|
package/dist/core/sharding.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
1
2
|
import type { Bully } from "./bully";
|
|
2
3
|
import type { Forum } from "./forum";
|
|
3
4
|
import type { Logger } from "./logger";
|
|
4
|
-
export type
|
|
5
|
+
export type ShardingOptions<TId> = {
|
|
5
6
|
bully: Bully;
|
|
6
7
|
forum: Forum;
|
|
7
8
|
ids: TId[];
|
|
@@ -29,7 +30,9 @@ export type ShardManagerOptions<TId> = {
|
|
|
29
30
|
onSustainedQuorumLoss?: () => void;
|
|
30
31
|
logger?: Logger;
|
|
31
32
|
};
|
|
32
|
-
export declare class
|
|
33
|
+
export declare class Sharding<TId = string> implements Disposable {
|
|
34
|
+
/** Emits `"assigned"` / `"released"` (each with the delta of ids, not the full held set) whenever this pod's held ids change. */
|
|
35
|
+
readonly lifecycle: EventEmitter<[never]>;
|
|
33
36
|
private bully;
|
|
34
37
|
private forum;
|
|
35
38
|
private allIds;
|
|
@@ -42,6 +45,8 @@ export declare class ShardManager<TId = string> implements Disposable {
|
|
|
42
45
|
private consecutiveQuorumFailures;
|
|
43
46
|
/** Ensures sustainedQuorumLossCallback fires once per episode, not on every failure past threshold. */
|
|
44
47
|
private hasTriggeredSustainedLoss;
|
|
48
|
+
/** Guards against a periodic rebalance tick overlapping an explicit reconcile() already in flight. */
|
|
49
|
+
private reconcileInFlight;
|
|
45
50
|
private heldIds;
|
|
46
51
|
private ownership;
|
|
47
52
|
private rebalanceInterval?;
|
|
@@ -49,7 +54,7 @@ export declare class ShardManager<TId = string> implements Disposable {
|
|
|
49
54
|
private ownershipEvents;
|
|
50
55
|
/** Safety net: any subscribe() the caller never explicitly closed gets torn down in stop(). */
|
|
51
56
|
private activeSubscriptions;
|
|
52
|
-
constructor(opts:
|
|
57
|
+
constructor(opts: ShardingOptions<TId>);
|
|
53
58
|
getHeldIds(): TId[];
|
|
54
59
|
/** Local, instant lookup — no network round-trip — of which peer currently owns an id. */
|
|
55
60
|
getOwner(id: TId): string | undefined;
|
|
@@ -90,5 +95,6 @@ export declare class ShardManager<TId = string> implements Disposable {
|
|
|
90
95
|
* assign/release messages needed to converge on that target.
|
|
91
96
|
*/
|
|
92
97
|
reconcile(): Promise<void>;
|
|
98
|
+
private doReconcile;
|
|
93
99
|
}
|
|
94
100
|
//# sourceMappingURL=sharding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharding.d.ts","sourceRoot":"","sources":["../../src/core/sharding.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sharding.d.ts","sourceRoot":"","sources":["../../src/core/sharding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,GAAG,EAAE,CAAC;IACX,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAKF,qBAAa,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAE,YAAW,UAAU;IACvD,iIAAiI;IACjI,SAAgB,SAAS,wBAAsB;IAE/C,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,mBAAmB,CAAC,CAAS;IACrC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,2BAA2B,CAAa;IAChD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,yBAAyB,CAAK;IACtC,uGAAuG;IACvG,OAAO,CAAC,yBAAyB,CAAS;IAC1C,sGAAsG;IACtG,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAiB;IAC3C,+FAA+F;IAC/F,OAAO,CAAC,eAAe,CAAsB;IAC7C,+FAA+F;IAC/F,OAAO,CAAC,mBAAmB,CAAyB;IAEpD,YAAY,IAAI,EAAE,eAAe,CAAC,GAAG,CAAC,EAUrC;IAEM,UAAU,UAEhB;IAED,0FAA0F;IACnF,QAAQ,CAAC,EAAE,EAAE,GAAG,sBAEtB;IAED,kGAAkG;IAC3F,YAAY,qBAElB;IAED;;;;;;;OAOG;IACI,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CA+BzE;IAED,sFAAsF;IAC/E,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,QAMvC;IAED,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,SAAS,CAIf;IAEF,OAAO,CAAC,eAAe,CAIrB;IAEF,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,cAAc,CAUpB;IAEF,OAAO,CAAC,SAAS,CAGf;IAEF,OAAO,CAAC,SAAS,CAKf;IAEK,KAAK,SAQX;IAEM,IAAI,SAWV;IAEM,CAAC,MAAM,CAAC,OAAO,CAAC,SAEtB;IAED;;;;;OAKG;IACU,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAQhC;IAED;;;;;OAKG;IACU,SAAS,kBASrB;YAEa,WAAW;CA2F1B"}
|
package/dist/core/sharding.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
-
export class
|
|
2
|
+
export class Sharding {
|
|
3
|
+
/** Emits `"assigned"` / `"released"` (each with the delta of ids, not the full held set) whenever this pod's held ids change. */
|
|
4
|
+
lifecycle = new EventEmitter();
|
|
3
5
|
bully;
|
|
4
6
|
forum;
|
|
5
7
|
allIds;
|
|
@@ -12,6 +14,8 @@ export class ShardManager {
|
|
|
12
14
|
consecutiveQuorumFailures = 0;
|
|
13
15
|
/** Ensures sustainedQuorumLossCallback fires once per episode, not on every failure past threshold. */
|
|
14
16
|
hasTriggeredSustainedLoss = false;
|
|
17
|
+
/** Guards against a periodic rebalance tick overlapping an explicit reconcile() already in flight. */
|
|
18
|
+
reconcileInFlight = false;
|
|
15
19
|
heldIds = new Set();
|
|
16
20
|
ownership = new Map();
|
|
17
21
|
rebalanceInterval;
|
|
@@ -89,11 +93,13 @@ export class ShardManager {
|
|
|
89
93
|
for (const id of ids)
|
|
90
94
|
this.heldIds.add(id);
|
|
91
95
|
this.logger.debug(`Now holding ${this.heldIds.size} id(s)`);
|
|
96
|
+
this.lifecycle.emit("assigned", ids);
|
|
92
97
|
};
|
|
93
98
|
onRelease = (ids) => {
|
|
94
99
|
for (const id of ids)
|
|
95
100
|
this.heldIds.delete(id);
|
|
96
101
|
this.logger.debug(`Released ${ids.length} id(s), now holding ${this.heldIds.size}`);
|
|
102
|
+
this.lifecycle.emit("released", ids);
|
|
97
103
|
};
|
|
98
104
|
onReportRequest = () => {
|
|
99
105
|
const { self } = this.bully.getStatus();
|
|
@@ -144,7 +150,7 @@ export class ShardManager {
|
|
|
144
150
|
this.bully.lifecycle.off("elected", this.onElected);
|
|
145
151
|
this.bully.lifecycle.off("demoted", this.onDemoted);
|
|
146
152
|
this.onDemoted();
|
|
147
|
-
for (const unsubscribe of
|
|
153
|
+
for (const unsubscribe of this.activeSubscriptions)
|
|
148
154
|
unsubscribe();
|
|
149
155
|
}
|
|
150
156
|
[Symbol.dispose]() {
|
|
@@ -171,6 +177,17 @@ export class ShardManager {
|
|
|
171
177
|
* assign/release messages needed to converge on that target.
|
|
172
178
|
*/
|
|
173
179
|
async reconcile() {
|
|
180
|
+
if (this.reconcileInFlight)
|
|
181
|
+
return;
|
|
182
|
+
this.reconcileInFlight = true;
|
|
183
|
+
try {
|
|
184
|
+
await this.doReconcile();
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
this.reconcileInFlight = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async doReconcile() {
|
|
174
191
|
const reports = new Map();
|
|
175
192
|
const { self } = this.bully.getStatus();
|
|
176
193
|
if (self)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { Quorum } from "./core/quorum";
|
|
2
2
|
export type { QuorumOptions } from "./core/quorum";
|
|
3
3
|
export { Bully } from "./core/bully";
|
|
4
|
-
export type { BullyPeer, BullyCluster,
|
|
4
|
+
export type { BullyPeer, BullyCluster, Discovery, BullyOptions } from "./core/bully";
|
|
5
5
|
export { Forum } from "./core/forum";
|
|
6
6
|
export type { ForumOptions } from "./core/forum";
|
|
7
7
|
export { Transport } from "./core/transport";
|
|
8
8
|
export type { TransportOptions, TransportCallbacks } from "./core/transport";
|
|
9
|
-
export {
|
|
10
|
-
export type {
|
|
9
|
+
export { Sharding } from "./core/sharding";
|
|
10
|
+
export type { ShardingOptions } from "./core/sharding";
|
|
11
11
|
export type { Logger } from "./core/logger";
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErF,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@einaraglen/quorum",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Distributed pod coordination: Bully leader election and work sharding",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leader-election",
|
|
@@ -29,11 +29,17 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"test": "tsx --test --test-reporter=spec --test-reporter-destination=stdout src/**/*.test.ts",
|
|
32
|
-
"build": "tsc"
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"format": "prettier --write .",
|
|
34
|
+
"format:check": "prettier --check .",
|
|
35
|
+
"lint": "oxlint .",
|
|
36
|
+
"check": "npm run format:check && npm run lint"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@types/express": "^5.0.6",
|
|
36
40
|
"@types/node": "^24.9.2",
|
|
41
|
+
"oxlint": "^1.80.0",
|
|
42
|
+
"prettier": "^3.9.6",
|
|
37
43
|
"tsx": "^4.23.12",
|
|
38
44
|
"typescript": "^7.0.2"
|
|
39
45
|
},
|