@delali/sirannon-db 0.1.7 → 0.2.0
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 +117 -969
- package/dist/backup-scheduler/index.d.ts +1 -1
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/baseline-Br77Fnhb.d.ts +6 -0
- package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
- package/dist/{chunk-DJLX6CAE.mjs → chunk-4ISB7XMA.mjs} +2 -1
- package/dist/chunk-5NOIGN5Y.mjs +18 -0
- package/dist/chunk-67M7KAH6.mjs +90 -0
- package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
- package/dist/{chunk-4IGMIJQK.mjs → chunk-EBJXPQQO.mjs} +59 -20
- package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
- package/dist/chunk-GVCNMPOS.mjs +130 -0
- package/dist/chunk-H237TXZW.mjs +1407 -0
- package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
- package/dist/chunk-HR5CWTLC.mjs +661 -0
- package/dist/chunk-JU64Y7HM.mjs +299 -0
- package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
- package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
- package/dist/chunk-LNY2VVHE.mjs +18 -0
- package/dist/chunk-NVQS53NT.mjs +665 -0
- package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
- package/dist/chunk-OQVZBEBY.mjs +283 -0
- package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
- package/dist/chunk-TJF5GZSV.mjs +187 -0
- package/dist/chunk-TUD5CJ76.mjs +126 -0
- package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
- package/dist/client/index.d.ts +216 -321
- package/dist/client/index.mjs +1192 -1342
- package/dist/client/topology.d.ts +61 -0
- package/dist/client/topology.mjs +682 -0
- package/dist/client-base-CLWmH5Ln.d.ts +234 -0
- package/dist/codegen/cli.d.ts +2 -0
- package/dist/codegen/cli.mjs +11 -0
- package/dist/codegen/index.d.ts +30 -0
- package/dist/codegen/index.mjs +5 -0
- package/dist/core/index.d.ts +37 -16
- package/dist/core/index.mjs +2322 -298
- package/dist/core/writer-worker.mjs +28 -6
- package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +6 -6
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +6 -6
- package/dist/driver/wa-sqlite.d.ts +1 -136
- package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
- package/dist/file-migrations/index.d.ts +7 -5
- package/dist/file-migrations/index.mjs +42 -37
- package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
- package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
- package/dist/protocol-6KrSq2Hy.d.ts +66 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.mjs +137 -0
- package/dist/replication/coordinator/etcd.d.ts +5 -3
- package/dist/replication/coordinator/etcd.mjs +554 -462
- package/dist/replication/index.d.ts +51 -105
- package/dist/replication/index.mjs +1415 -2061
- package/dist/server/index.d.ts +111 -87
- package/dist/server/index.mjs +1917 -489
- package/dist/server-options-1JHu8pid.d.ts +191 -0
- package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
- package/dist/transport/grpc.d.ts +7 -4
- package/dist/transport/grpc.mjs +191 -97
- package/dist/transport/memory.d.ts +5 -134
- package/dist/transport/memory.mjs +3 -1
- package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
- package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
- package/dist/types-CL6piSnD.d.ts +43 -0
- package/dist/types-C_D8IhpO.d.ts +60 -0
- package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
- package/package.json +44 -6
- package/dist/chunk-TKGHYWQ6.mjs +0 -35
- package/dist/types-D4p4UyDK.d.ts +0 -26
|
@@ -1,183 +1,312 @@
|
|
|
1
1
|
import { compatibilityAllowsPromotion } from '../../chunk-ER7ODTDA.mjs';
|
|
2
|
-
import { CoordinatorError, NoSafePrimaryError } from '../../chunk-
|
|
3
|
-
import '../../chunk-
|
|
2
|
+
import { CoordinatorError, NoSafePrimaryError } from '../../chunk-O7SLN3GI.mjs';
|
|
3
|
+
import '../../chunk-5NOIGN5Y.mjs';
|
|
4
|
+
import '../../chunk-UC3SCMIN.mjs';
|
|
4
5
|
import { Etcd3 } from 'etcd3';
|
|
5
6
|
|
|
7
|
+
// src/replication/coordinator/group-rules.ts
|
|
6
8
|
var MIN_AUTOMATIC_FAILOVER_VOTERS = 3;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
onWatcherError;
|
|
11
|
-
leases = /* @__PURE__ */ new Map();
|
|
12
|
-
watchers = /* @__PURE__ */ new Set();
|
|
13
|
-
constructor(options) {
|
|
14
|
-
assertEtcdOptions(options);
|
|
15
|
-
this.client = new Etcd3(toEtcdOptions(options));
|
|
16
|
-
this.namespace = this.client.namespace(normaliseKeyPrefix(options.keyPrefix));
|
|
17
|
-
this.onWatcherError = options.onWatcherError;
|
|
9
|
+
function assertNonEmpty(value, name) {
|
|
10
|
+
if (value.length === 0) {
|
|
11
|
+
throw new TypeError(`${name} must not be empty`);
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const key = controllerLeaseKey(input.clusterId);
|
|
24
|
-
const lease = this.namespace.lease(ttlMsToSeconds(input.ttlMs));
|
|
25
|
-
const leaseId = await lease.grant();
|
|
26
|
-
const grantedAtMs = Date.now();
|
|
27
|
-
const value = serializeLease({
|
|
28
|
-
id: leaseId,
|
|
29
|
-
kind: "controller",
|
|
30
|
-
clusterId: input.clusterId,
|
|
31
|
-
holderId: input.holderId,
|
|
32
|
-
ttlMs: input.ttlMs,
|
|
33
|
-
grantedAtMs,
|
|
34
|
-
expiresAtMs: grantedAtMs + input.ttlMs,
|
|
35
|
-
metadata: cloneMetadata(input.metadata)
|
|
36
|
-
});
|
|
37
|
-
const result = await this.namespace.if(key, "Create", "==", 0).then(this.namespace.put(key).value(value).lease(leaseId)).commit();
|
|
38
|
-
if (!result.succeeded) {
|
|
39
|
-
await revokeLeaseQuietly(lease);
|
|
40
|
-
const current = await this.getLeaseFromKey(key);
|
|
41
|
-
return { acquired: false, lease: current };
|
|
42
|
-
}
|
|
43
|
-
this.trackLease(lease, {
|
|
44
|
-
leaseId,
|
|
45
|
-
key,
|
|
46
|
-
ttlMs: input.ttlMs,
|
|
47
|
-
ttlSeconds: ttlMsToSeconds(input.ttlMs),
|
|
48
|
-
kind: "controller",
|
|
49
|
-
clusterId: input.clusterId,
|
|
50
|
-
holderId: input.holderId,
|
|
51
|
-
metadata: cloneMetadata(input.metadata)
|
|
52
|
-
});
|
|
53
|
-
const parsed = parseLease(value);
|
|
54
|
-
return { acquired: true, lease: parsed };
|
|
13
|
+
}
|
|
14
|
+
function assertPositiveTtl(ttlMs) {
|
|
15
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
16
|
+
throw new RangeError("ttlMs must be a positive safe integer");
|
|
55
17
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
grantedAtMs: renewedAtMs,
|
|
73
|
-
expiresAtMs: renewedAtMs + ttlMs,
|
|
74
|
-
metadata: cloneMetadata(entry.metadata)
|
|
75
|
-
};
|
|
76
|
-
const value = entry.kind === "node-session" && entry.nodeSession ? JSON.stringify({ ...entry.nodeSession, lease: leaseValue }) : serializeLease(leaseValue);
|
|
77
|
-
await this.namespace.put(entry.key).value(value).ignoreLease();
|
|
78
|
-
entry.ttlMs = ttlMs;
|
|
79
|
-
return true;
|
|
80
|
-
} catch {
|
|
81
|
-
this.leases.delete(leaseId);
|
|
82
|
-
return false;
|
|
18
|
+
}
|
|
19
|
+
function assertNonNegativeTerm(term) {
|
|
20
|
+
if (term < 0n) {
|
|
21
|
+
throw new RangeError("primaryTerm must not be negative");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function assertNonNegativeSeq(seq, name) {
|
|
25
|
+
if (seq < 0n) {
|
|
26
|
+
throw new RangeError(`${name} must not be negative`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function assertSubset(values, allowed, name) {
|
|
30
|
+
const allowedSet = new Set(allowed);
|
|
31
|
+
for (const value of values) {
|
|
32
|
+
if (!allowedSet.has(value)) {
|
|
33
|
+
throw new RangeError(`${name} contains node id '${value}' that is not configured for the replication group`);
|
|
83
34
|
}
|
|
84
35
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
36
|
+
}
|
|
37
|
+
function assertPrimaryInGroup(primary, votingDataBearingNodeIds) {
|
|
38
|
+
if (!primary) return;
|
|
39
|
+
assertNonEmpty(primary.nodeId, "primary.nodeId");
|
|
40
|
+
if (!votingDataBearingNodeIds.includes(primary.nodeId)) {
|
|
41
|
+
throw new RangeError(`Primary node '${primary.nodeId}' is not configured for the replication group`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertNoInSyncAdditions(previous, next) {
|
|
45
|
+
const previousSet = new Set(previous);
|
|
46
|
+
for (const nodeId of next) {
|
|
47
|
+
if (!previousSet.has(nodeId)) {
|
|
48
|
+
throw new RangeError(`Node '${nodeId}' cannot be added to the in-sync set without catch-up proof`);
|
|
90
49
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function normaliseNodeIds(nodeIds, name) {
|
|
53
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54
|
+
const result = [];
|
|
55
|
+
for (const nodeId of nodeIds) {
|
|
56
|
+
assertNonEmpty(nodeId, `${name} entry`);
|
|
57
|
+
if (seen.has(nodeId)) {
|
|
58
|
+
throw new RangeError(`${name} contains duplicate node id '${nodeId}'`);
|
|
98
59
|
}
|
|
99
|
-
|
|
100
|
-
|
|
60
|
+
seen.add(nodeId);
|
|
61
|
+
result.push(nodeId);
|
|
101
62
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function setMembership(values, nodeId, enabled) {
|
|
66
|
+
if (enabled === void 0) return values;
|
|
67
|
+
const next = values.filter((value) => value !== nodeId);
|
|
68
|
+
if (enabled) {
|
|
69
|
+
next.push(nodeId);
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
function removeNodeId(values, nodeId) {
|
|
74
|
+
return values.filter((value) => value !== nodeId);
|
|
75
|
+
}
|
|
76
|
+
function arraysEqual(left, right) {
|
|
77
|
+
if (left.length !== right.length) return false;
|
|
78
|
+
for (let i = 0; i < left.length; i++) {
|
|
79
|
+
if (left[i] !== right[i]) return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function markDisplacedPrimaryForRepair(state, displacedPrimaryId, nextPrimaryId) {
|
|
84
|
+
if (!displacedPrimaryId || displacedPrimaryId === nextPrimaryId) return;
|
|
85
|
+
state.inSyncNodeIds = removeNodeId(state.inSyncNodeIds, displacedPrimaryId);
|
|
86
|
+
state.repairingNodeIds = setMembership(state.repairingNodeIds, displacedPrimaryId, true);
|
|
87
|
+
}
|
|
88
|
+
function cloneCompatibility(compatibility) {
|
|
89
|
+
return compatibility ? { ...compatibility } : void 0;
|
|
90
|
+
}
|
|
91
|
+
function cloneMetadata(metadata) {
|
|
92
|
+
return metadata ? { ...metadata } : void 0;
|
|
93
|
+
}
|
|
94
|
+
function isEligiblePromotionSession(state, nodeId, session) {
|
|
95
|
+
if (!session) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return session.dataBearing && session.voting && state.inSyncNodeIds.includes(nodeId) && compatibilityAllowsPromotion(state.compatibility, session.compatibility) && !state.drainingNodeIds.includes(nodeId) && !state.repairingNodeIds.includes(nodeId) && !state.faultedNodeIds.includes(nodeId);
|
|
99
|
+
}
|
|
100
|
+
function nextInSyncSetState(state, input, nowMs) {
|
|
101
|
+
const inSyncNodeIds = normaliseNodeIds(input.inSyncNodeIds, "inSyncNodeIds");
|
|
102
|
+
assertSubset(inSyncNodeIds, state.votingDataBearingNodeIds, "inSyncNodeIds");
|
|
103
|
+
assertNoInSyncAdditions(state.inSyncNodeIds, inSyncNodeIds);
|
|
104
|
+
const durabilityPointSeq = input.durabilityPointSeq !== void 0 && input.durabilityPointSeq > state.durabilityPointSeq ? input.durabilityPointSeq : state.durabilityPointSeq;
|
|
105
|
+
if (arraysEqual(inSyncNodeIds, state.inSyncNodeIds) && durabilityPointSeq === state.durabilityPointSeq) {
|
|
106
|
+
return state;
|
|
107
|
+
}
|
|
108
|
+
return { ...state, durabilityPointSeq, inSyncNodeIds, updatedAtMs: nowMs };
|
|
109
|
+
}
|
|
110
|
+
function nextAdmittedInSyncState(state, input, nowMs) {
|
|
111
|
+
assertNodeInGroup(state, input.nodeId);
|
|
112
|
+
if (state.currentPrimary?.nodeId !== input.sourceNodeId || state.drainingNodeIds.includes(input.nodeId) || state.faultedNodeIds.includes(input.nodeId) || input.appliedSeq < state.durabilityPointSeq) {
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
const inSyncNodeIds = state.inSyncNodeIds.includes(input.nodeId) ? state.inSyncNodeIds : [...state.inSyncNodeIds, input.nodeId];
|
|
116
|
+
const repairingNodeIds = removeNodeId(state.repairingNodeIds, input.nodeId);
|
|
117
|
+
if (arraysEqual(inSyncNodeIds, state.inSyncNodeIds) && arraysEqual(repairingNodeIds, state.repairingNodeIds)) {
|
|
118
|
+
return state;
|
|
119
|
+
}
|
|
120
|
+
return { ...state, inSyncNodeIds, repairingNodeIds, updatedAtMs: nowMs };
|
|
121
|
+
}
|
|
122
|
+
function nextMaintenanceState(state, input, nowMs) {
|
|
123
|
+
assertNodeInGroup(state, input.nodeId);
|
|
124
|
+
const leavingService = input.draining === true || input.repairing === true || input.faulted === true;
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
drainingNodeIds: setMembership(state.drainingNodeIds, input.nodeId, input.draining),
|
|
128
|
+
repairingNodeIds: setMembership(state.repairingNodeIds, input.nodeId, input.repairing),
|
|
129
|
+
faultedNodeIds: setMembership(state.faultedNodeIds, input.nodeId, input.faulted),
|
|
130
|
+
inSyncNodeIds: leavingService ? removeNodeId(state.inSyncNodeIds, input.nodeId) : state.inSyncNodeIds,
|
|
131
|
+
updatedAtMs: nowMs
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function assertNodeInGroup(state, nodeId) {
|
|
135
|
+
if (!state.votingDataBearingNodeIds.includes(nodeId)) {
|
|
136
|
+
throw new RangeError(`Node '${nodeId}' is not configured for the replication group`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function buildReplicationGroupState(input, nowMs = Date.now()) {
|
|
140
|
+
assertNonEmpty(input.clusterId, "clusterId");
|
|
141
|
+
assertNonEmpty(input.groupId, "groupId");
|
|
142
|
+
const votingDataBearingNodeIds = normaliseNodeIds(input.votingDataBearingNodeIds, "votingDataBearingNodeIds");
|
|
143
|
+
const inSyncNodeIds = normaliseNodeIds(input.inSyncNodeIds ?? [], "inSyncNodeIds");
|
|
144
|
+
const drainingNodeIds = normaliseNodeIds(input.drainingNodeIds ?? [], "drainingNodeIds");
|
|
145
|
+
const repairingNodeIds = normaliseNodeIds(input.repairingNodeIds ?? [], "repairingNodeIds");
|
|
146
|
+
const faultedNodeIds = normaliseNodeIds(input.faultedNodeIds ?? [], "faultedNodeIds");
|
|
147
|
+
assertNonNegativeTerm(input.primaryTerm ?? 0n);
|
|
148
|
+
assertNonNegativeSeq(input.durabilityPointSeq ?? 0n, "durabilityPointSeq");
|
|
149
|
+
assertPrimaryInGroup(input.currentPrimary ?? null, votingDataBearingNodeIds);
|
|
150
|
+
assertSubset(inSyncNodeIds, votingDataBearingNodeIds, "inSyncNodeIds");
|
|
151
|
+
assertSubset(drainingNodeIds, votingDataBearingNodeIds, "drainingNodeIds");
|
|
152
|
+
assertSubset(repairingNodeIds, votingDataBearingNodeIds, "repairingNodeIds");
|
|
153
|
+
assertSubset(faultedNodeIds, votingDataBearingNodeIds, "faultedNodeIds");
|
|
154
|
+
return {
|
|
155
|
+
clusterId: input.clusterId,
|
|
156
|
+
groupId: input.groupId,
|
|
157
|
+
votingDataBearingNodeIds,
|
|
158
|
+
currentPrimary: input.currentPrimary ? { ...input.currentPrimary } : null,
|
|
159
|
+
primaryTerm: input.primaryTerm ?? 0n,
|
|
160
|
+
durabilityPointSeq: input.durabilityPointSeq ?? 0n,
|
|
161
|
+
inSyncNodeIds,
|
|
162
|
+
drainingNodeIds,
|
|
163
|
+
repairingNodeIds,
|
|
164
|
+
faultedNodeIds,
|
|
165
|
+
compatibility: cloneCompatibility(input.compatibility),
|
|
166
|
+
updatedAtMs: nowMs
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function cloneReplicationGroupState(state) {
|
|
170
|
+
return {
|
|
171
|
+
...state,
|
|
172
|
+
currentPrimary: state.currentPrimary ? { ...state.currentPrimary } : null,
|
|
173
|
+
votingDataBearingNodeIds: [...state.votingDataBearingNodeIds],
|
|
174
|
+
durabilityPointSeq: state.durabilityPointSeq,
|
|
175
|
+
inSyncNodeIds: [...state.inSyncNodeIds],
|
|
176
|
+
drainingNodeIds: [...state.drainingNodeIds],
|
|
177
|
+
repairingNodeIds: [...state.repairingNodeIds],
|
|
178
|
+
faultedNodeIds: [...state.faultedNodeIds],
|
|
179
|
+
compatibility: cloneCompatibility(state.compatibility)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/replication/coordinator/etcd-codec.ts
|
|
184
|
+
function serializeLease(lease) {
|
|
185
|
+
return JSON.stringify(lease);
|
|
186
|
+
}
|
|
187
|
+
function parseLease(raw) {
|
|
188
|
+
const value = JSON.parse(raw);
|
|
189
|
+
return {
|
|
190
|
+
...value,
|
|
191
|
+
metadata: cloneMetadata(value.metadata)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function parseNodeSession(raw) {
|
|
195
|
+
const value = JSON.parse(raw);
|
|
196
|
+
return {
|
|
197
|
+
...value,
|
|
198
|
+
lease: parseLease(JSON.stringify(value.lease)),
|
|
199
|
+
groupIds: [...value.groupIds],
|
|
200
|
+
compatibility: cloneCompatibility(value.compatibility),
|
|
201
|
+
metadata: cloneMetadata(value.metadata)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function parseLeaseIdForEntry(kind, raw) {
|
|
205
|
+
try {
|
|
206
|
+
if (kind === "node-session") {
|
|
207
|
+
return parseNodeSession(raw).lease.id;
|
|
113
208
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
209
|
+
return parseLease(raw).id;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function serializeGroupState(state) {
|
|
215
|
+
const serialized = {
|
|
216
|
+
...state,
|
|
217
|
+
currentPrimary: state.currentPrimary ? { ...state.currentPrimary } : null,
|
|
218
|
+
votingDataBearingNodeIds: [...state.votingDataBearingNodeIds],
|
|
219
|
+
primaryTerm: state.primaryTerm.toString(),
|
|
220
|
+
durabilityPointSeq: state.durabilityPointSeq.toString(),
|
|
221
|
+
inSyncNodeIds: [...state.inSyncNodeIds],
|
|
222
|
+
drainingNodeIds: [...state.drainingNodeIds],
|
|
223
|
+
repairingNodeIds: [...state.repairingNodeIds],
|
|
224
|
+
faultedNodeIds: [...state.faultedNodeIds],
|
|
225
|
+
compatibility: cloneCompatibility(state.compatibility)
|
|
226
|
+
};
|
|
227
|
+
return JSON.stringify(serialized);
|
|
228
|
+
}
|
|
229
|
+
function parseGroupState(raw) {
|
|
230
|
+
const value = JSON.parse(raw);
|
|
231
|
+
return {
|
|
232
|
+
...value,
|
|
233
|
+
currentPrimary: value.currentPrimary ? { ...value.currentPrimary } : null,
|
|
234
|
+
votingDataBearingNodeIds: [...value.votingDataBearingNodeIds],
|
|
235
|
+
primaryTerm: BigInt(value.primaryTerm),
|
|
236
|
+
durabilityPointSeq: value.durabilityPointSeq !== void 0 ? BigInt(value.durabilityPointSeq) : 0n,
|
|
237
|
+
inSyncNodeIds: [...value.inSyncNodeIds],
|
|
238
|
+
drainingNodeIds: [...value.drainingNodeIds],
|
|
239
|
+
repairingNodeIds: [...value.repairingNodeIds],
|
|
240
|
+
faultedNodeIds: [...value.faultedNodeIds ?? []],
|
|
241
|
+
compatibility: cloneCompatibility(value.compatibility)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/replication/coordinator/etcd-connection.ts
|
|
246
|
+
function assertEtcdOptions(options) {
|
|
247
|
+
const hosts = Array.isArray(options.hosts) ? options.hosts : [options.hosts];
|
|
248
|
+
if (hosts.length === 0) {
|
|
249
|
+
throw new TypeError("hosts must contain at least one etcd endpoint");
|
|
250
|
+
}
|
|
251
|
+
for (const host of hosts) {
|
|
252
|
+
assertNonEmpty(host, "hosts entry");
|
|
253
|
+
if (!options.allowInsecure && !host.startsWith("https://")) {
|
|
254
|
+
throw new TypeError("production coordinator access requires https etcd endpoints");
|
|
143
255
|
}
|
|
144
|
-
this.trackLease(lease, {
|
|
145
|
-
leaseId,
|
|
146
|
-
key,
|
|
147
|
-
ttlMs: input.ttlMs,
|
|
148
|
-
ttlSeconds: ttlMsToSeconds(input.ttlMs),
|
|
149
|
-
kind: "node-session",
|
|
150
|
-
clusterId: input.clusterId,
|
|
151
|
-
holderId: input.nodeId,
|
|
152
|
-
metadata: cloneMetadata(input.metadata),
|
|
153
|
-
nodeSession: {
|
|
154
|
-
clusterId: session.clusterId,
|
|
155
|
-
nodeId: session.nodeId,
|
|
156
|
-
endpoint: session.endpoint,
|
|
157
|
-
groupIds: [...session.groupIds],
|
|
158
|
-
dataBearing: session.dataBearing,
|
|
159
|
-
voting: session.voting,
|
|
160
|
-
compatibility: cloneCompatibility(session.compatibility),
|
|
161
|
-
metadata: cloneMetadata(session.metadata)
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
return parseNodeSession(rawSession);
|
|
165
256
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
257
|
+
assertNonEmpty(options.keyPrefix, "keyPrefix");
|
|
258
|
+
if (!options.allowInsecure && !options.credentials) {
|
|
259
|
+
throw new TypeError("production coordinator access requires TLS credentials");
|
|
260
|
+
}
|
|
261
|
+
const hasMtlsIdentity = Boolean(options.credentials?.privateKey && options.credentials.certChain);
|
|
262
|
+
const hasPasswordAuth = Boolean(options.auth?.username && options.auth.password);
|
|
263
|
+
if (!options.allowInsecure && !hasMtlsIdentity && !hasPasswordAuth) {
|
|
264
|
+
throw new TypeError("production coordinator access requires an authenticated Sirannon identity");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function toEtcdOptions(options) {
|
|
268
|
+
const defaultCallTimeoutMs = options.defaultCallTimeoutMs;
|
|
269
|
+
const defaultCallOptions = defaultCallTimeoutMs ? () => ({ deadline: Date.now() + defaultCallTimeoutMs }) : void 0;
|
|
270
|
+
return {
|
|
271
|
+
hosts: options.hosts,
|
|
272
|
+
credentials: options.credentials,
|
|
273
|
+
auth: options.auth,
|
|
274
|
+
grpcOptions: options.grpcOptions,
|
|
275
|
+
dialTimeout: options.dialTimeoutMs,
|
|
276
|
+
defaultCallOptions
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function normaliseKeyPrefix(prefix) {
|
|
280
|
+
const trimmed = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
281
|
+
if (trimmed.length === 0) {
|
|
282
|
+
throw new TypeError("keyPrefix must not resolve to the etcd root");
|
|
171
283
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
284
|
+
return `${trimmed}/`;
|
|
285
|
+
}
|
|
286
|
+
function controllerLeaseKey(clusterId) {
|
|
287
|
+
return `clusters/${encodeKey(clusterId)}/controller`;
|
|
288
|
+
}
|
|
289
|
+
function nodeSessionKey(clusterId, nodeId) {
|
|
290
|
+
return `clusters/${encodeKey(clusterId)}/nodes/${encodeKey(nodeId)}`;
|
|
291
|
+
}
|
|
292
|
+
function replicationGroupKey(clusterId, groupId) {
|
|
293
|
+
return `clusters/${encodeKey(clusterId)}/groups/${encodeKey(groupId)}`;
|
|
294
|
+
}
|
|
295
|
+
function ttlMsToSeconds(ttlMs) {
|
|
296
|
+
return Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
297
|
+
}
|
|
298
|
+
function encodeKey(value) {
|
|
299
|
+
return encodeURIComponent(value);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/replication/coordinator/etcd-group-store.ts
|
|
303
|
+
var MAX_CAS_ATTEMPTS = 5;
|
|
304
|
+
var MAX_PROMOTION_ATTEMPTS = 3;
|
|
305
|
+
var EtcdGroupStore = class {
|
|
306
|
+
constructor(namespace, watchers, onWatcherError) {
|
|
307
|
+
this.namespace = namespace;
|
|
308
|
+
this.watchers = watchers;
|
|
309
|
+
this.onWatcherError = onWatcherError;
|
|
181
310
|
}
|
|
182
311
|
async setReplicationGroupState(input) {
|
|
183
312
|
const state = buildReplicationGroupState(input);
|
|
@@ -247,16 +376,12 @@ var EtcdClusterCoordinator = class {
|
|
|
247
376
|
if (input.durabilityPointSeq !== void 0) {
|
|
248
377
|
assertNonNegativeSeq(input.durabilityPointSeq, "durabilityPointSeq");
|
|
249
378
|
}
|
|
250
|
-
|
|
251
|
-
return this.updateGroupStateWithRetry(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return state;
|
|
257
|
-
}
|
|
258
|
-
return { ...state, durabilityPointSeq, inSyncNodeIds, updatedAtMs: Date.now() };
|
|
259
|
-
});
|
|
379
|
+
normaliseNodeIds(input.inSyncNodeIds, "inSyncNodeIds");
|
|
380
|
+
return this.updateGroupStateWithRetry(
|
|
381
|
+
input.clusterId,
|
|
382
|
+
input.groupId,
|
|
383
|
+
(state) => nextInSyncSetState(state, input, Date.now())
|
|
384
|
+
);
|
|
260
385
|
}
|
|
261
386
|
async admitNodeToInSyncSet(input) {
|
|
262
387
|
assertNonEmpty(input.clusterId, "clusterId");
|
|
@@ -264,44 +389,27 @@ var EtcdClusterCoordinator = class {
|
|
|
264
389
|
assertNonEmpty(input.nodeId, "nodeId");
|
|
265
390
|
assertNonEmpty(input.sourceNodeId, "sourceNodeId");
|
|
266
391
|
assertNonNegativeSeq(input.appliedSeq, "appliedSeq");
|
|
267
|
-
return this.updateGroupStateWithRetry(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return state;
|
|
273
|
-
}
|
|
274
|
-
const inSyncNodeIds = state.inSyncNodeIds.includes(input.nodeId) ? state.inSyncNodeIds : [...state.inSyncNodeIds, input.nodeId];
|
|
275
|
-
const repairingNodeIds = removeNodeId(state.repairingNodeIds, input.nodeId);
|
|
276
|
-
if (arraysEqual(inSyncNodeIds, state.inSyncNodeIds) && arraysEqual(repairingNodeIds, state.repairingNodeIds)) {
|
|
277
|
-
return state;
|
|
278
|
-
}
|
|
279
|
-
return { ...state, inSyncNodeIds, repairingNodeIds, updatedAtMs: Date.now() };
|
|
280
|
-
});
|
|
392
|
+
return this.updateGroupStateWithRetry(
|
|
393
|
+
input.clusterId,
|
|
394
|
+
input.groupId,
|
|
395
|
+
(state) => nextAdmittedInSyncState(state, input, Date.now())
|
|
396
|
+
);
|
|
281
397
|
}
|
|
282
398
|
async updateNodeMaintenance(input) {
|
|
283
399
|
assertNonEmpty(input.clusterId, "clusterId");
|
|
284
400
|
assertNonEmpty(input.groupId, "groupId");
|
|
285
401
|
assertNonEmpty(input.nodeId, "nodeId");
|
|
286
|
-
return this.updateGroupStateWithRetry(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
...state,
|
|
292
|
-
drainingNodeIds: setMembership(state.drainingNodeIds, input.nodeId, input.draining),
|
|
293
|
-
repairingNodeIds: setMembership(state.repairingNodeIds, input.nodeId, input.repairing),
|
|
294
|
-
faultedNodeIds: setMembership(state.faultedNodeIds, input.nodeId, input.faulted),
|
|
295
|
-
inSyncNodeIds: input.draining === true || input.repairing === true || input.faulted === true ? removeNodeId(state.inSyncNodeIds, input.nodeId) : state.inSyncNodeIds,
|
|
296
|
-
updatedAtMs: Date.now()
|
|
297
|
-
};
|
|
298
|
-
});
|
|
402
|
+
return this.updateGroupStateWithRetry(
|
|
403
|
+
input.clusterId,
|
|
404
|
+
input.groupId,
|
|
405
|
+
(state) => nextMaintenanceState(state, input, Date.now())
|
|
406
|
+
);
|
|
299
407
|
}
|
|
300
408
|
async promoteEligibleReplica(input) {
|
|
301
409
|
assertNonEmpty(input.clusterId, "clusterId");
|
|
302
410
|
assertNonEmpty(input.groupId, "groupId");
|
|
303
411
|
const excludedNodeIds = new Set(input.excludeNodeIds ?? []);
|
|
304
|
-
for (let attempt = 0; attempt <
|
|
412
|
+
for (let attempt = 0; attempt < MAX_PROMOTION_ATTEMPTS; attempt++) {
|
|
305
413
|
const state = await this.getReplicationGroupState(input.clusterId, input.groupId);
|
|
306
414
|
if (!state) {
|
|
307
415
|
throw new NoSafePrimaryError(`No replication group '${input.groupId}' is registered`);
|
|
@@ -332,35 +440,12 @@ var EtcdClusterCoordinator = class {
|
|
|
332
440
|
}
|
|
333
441
|
throw new NoSafePrimaryError(`No safe primary is available for replication group '${input.groupId}'`);
|
|
334
442
|
}
|
|
335
|
-
async
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
watcherCancels.push(watcher.cancel());
|
|
339
|
-
}
|
|
340
|
-
this.watchers.clear();
|
|
341
|
-
await Promise.allSettled(watcherCancels);
|
|
342
|
-
const leaseRevokes = [];
|
|
343
|
-
for (const entry of this.leases.values()) {
|
|
344
|
-
leaseRevokes.push(revokeLeaseQuietly(entry.lease));
|
|
345
|
-
}
|
|
346
|
-
this.leases.clear();
|
|
347
|
-
await Promise.allSettled(leaseRevokes);
|
|
348
|
-
this.client.close();
|
|
349
|
-
}
|
|
350
|
-
async getLeaseFromKey(key) {
|
|
351
|
-
const value = await this.namespace.get(key).string();
|
|
352
|
-
return value ? parseLease(value) : null;
|
|
353
|
-
}
|
|
354
|
-
trackLease(lease, entry) {
|
|
355
|
-
const fullEntry = { ...entry, lease };
|
|
356
|
-
this.leases.set(entry.leaseId, fullEntry);
|
|
357
|
-
lease.on("lost", (err) => {
|
|
358
|
-
this.leases.delete(entry.leaseId);
|
|
359
|
-
this.onWatcherError?.(err instanceof Error ? err : new CoordinatorError(String(err)));
|
|
360
|
-
});
|
|
443
|
+
async getLiveNodeSession(clusterId, nodeId) {
|
|
444
|
+
const value = await this.namespace.get(nodeSessionKey(clusterId, nodeId)).string();
|
|
445
|
+
return value ? parseNodeSession(value) : null;
|
|
361
446
|
}
|
|
362
447
|
async updateGroupStateWithRetry(clusterId, groupId, mutate) {
|
|
363
|
-
for (let attempt = 0; attempt <
|
|
448
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
364
449
|
const state = await this.getReplicationGroupState(clusterId, groupId);
|
|
365
450
|
if (!state) {
|
|
366
451
|
return null;
|
|
@@ -387,257 +472,264 @@ var EtcdClusterCoordinator = class {
|
|
|
387
472
|
return { updated: false, state: await this.getReplicationGroupState(previous.clusterId, previous.groupId) };
|
|
388
473
|
}
|
|
389
474
|
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
475
|
+
|
|
476
|
+
// src/replication/coordinator/etcd.ts
|
|
477
|
+
var EtcdClusterCoordinator = class {
|
|
478
|
+
client;
|
|
479
|
+
namespace;
|
|
480
|
+
onWatcherError;
|
|
481
|
+
leases = /* @__PURE__ */ new Map();
|
|
482
|
+
grantedNodeSessionLeaseIds = /* @__PURE__ */ new Map();
|
|
483
|
+
watchers = /* @__PURE__ */ new Set();
|
|
484
|
+
groups;
|
|
485
|
+
constructor(options) {
|
|
486
|
+
assertEtcdOptions(options);
|
|
487
|
+
this.client = new Etcd3(toEtcdOptions(options));
|
|
488
|
+
this.namespace = this.client.namespace(normaliseKeyPrefix(options.keyPrefix));
|
|
489
|
+
this.onWatcherError = options.onWatcherError;
|
|
490
|
+
this.groups = new EtcdGroupStore(this.namespace, this.watchers, this.onWatcherError);
|
|
397
491
|
}
|
|
398
|
-
|
|
399
|
-
assertNonEmpty(
|
|
400
|
-
|
|
401
|
-
|
|
492
|
+
async tryAcquireControllerLease(input) {
|
|
493
|
+
assertNonEmpty(input.clusterId, "clusterId");
|
|
494
|
+
assertNonEmpty(input.holderId, "holderId");
|
|
495
|
+
assertPositiveTtl(input.ttlMs);
|
|
496
|
+
const key = controllerLeaseKey(input.clusterId);
|
|
497
|
+
const lease = this.namespace.lease(ttlMsToSeconds(input.ttlMs));
|
|
498
|
+
const leaseId = await lease.grant();
|
|
499
|
+
const grantedAtMs = Date.now();
|
|
500
|
+
const value = serializeLease({
|
|
501
|
+
id: leaseId,
|
|
502
|
+
kind: "controller",
|
|
503
|
+
clusterId: input.clusterId,
|
|
504
|
+
holderId: input.holderId,
|
|
505
|
+
ttlMs: input.ttlMs,
|
|
506
|
+
grantedAtMs,
|
|
507
|
+
expiresAtMs: grantedAtMs + input.ttlMs,
|
|
508
|
+
metadata: cloneMetadata(input.metadata)
|
|
509
|
+
});
|
|
510
|
+
const result = await this.namespace.if(key, "Create", "==", 0).then(this.namespace.put(key).value(value).lease(leaseId)).commit();
|
|
511
|
+
if (!result.succeeded) {
|
|
512
|
+
await revokeLeaseQuietly(lease);
|
|
513
|
+
const current = await this.getLeaseFromKey(key);
|
|
514
|
+
return { acquired: false, lease: current };
|
|
402
515
|
}
|
|
516
|
+
this.trackLease(lease, {
|
|
517
|
+
leaseId,
|
|
518
|
+
key,
|
|
519
|
+
ttlMs: input.ttlMs,
|
|
520
|
+
ttlSeconds: ttlMsToSeconds(input.ttlMs),
|
|
521
|
+
kind: "controller",
|
|
522
|
+
clusterId: input.clusterId,
|
|
523
|
+
holderId: input.holderId,
|
|
524
|
+
metadata: cloneMetadata(input.metadata)
|
|
525
|
+
});
|
|
526
|
+
const parsed = parseLease(value);
|
|
527
|
+
return { acquired: true, lease: parsed };
|
|
403
528
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const defaultCallOptions = defaultCallTimeoutMs ? () => ({ deadline: Date.now() + defaultCallTimeoutMs }) : void 0;
|
|
417
|
-
return {
|
|
418
|
-
hosts: options.hosts,
|
|
419
|
-
credentials: options.credentials,
|
|
420
|
-
auth: options.auth,
|
|
421
|
-
grpcOptions: options.grpcOptions,
|
|
422
|
-
dialTimeout: options.dialTimeoutMs,
|
|
423
|
-
defaultCallOptions
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
function normaliseKeyPrefix(prefix) {
|
|
427
|
-
const trimmed = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
428
|
-
if (trimmed.length === 0) {
|
|
429
|
-
throw new TypeError("keyPrefix must not resolve to the etcd root");
|
|
430
|
-
}
|
|
431
|
-
return `${trimmed}/`;
|
|
432
|
-
}
|
|
433
|
-
function controllerLeaseKey(clusterId) {
|
|
434
|
-
return `clusters/${encodeKey(clusterId)}/controller`;
|
|
435
|
-
}
|
|
436
|
-
function nodeSessionKey(clusterId, nodeId) {
|
|
437
|
-
return `clusters/${encodeKey(clusterId)}/nodes/${encodeKey(nodeId)}`;
|
|
438
|
-
}
|
|
439
|
-
function replicationGroupKey(clusterId, groupId) {
|
|
440
|
-
return `clusters/${encodeKey(clusterId)}/groups/${encodeKey(groupId)}`;
|
|
441
|
-
}
|
|
442
|
-
function encodeKey(value) {
|
|
443
|
-
return encodeURIComponent(value);
|
|
444
|
-
}
|
|
445
|
-
function ttlMsToSeconds(ttlMs) {
|
|
446
|
-
return Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
447
|
-
}
|
|
448
|
-
function buildReplicationGroupState(input) {
|
|
449
|
-
assertNonEmpty(input.clusterId, "clusterId");
|
|
450
|
-
assertNonEmpty(input.groupId, "groupId");
|
|
451
|
-
const votingDataBearingNodeIds = normaliseNodeIds(input.votingDataBearingNodeIds, "votingDataBearingNodeIds");
|
|
452
|
-
const inSyncNodeIds = normaliseNodeIds(input.inSyncNodeIds ?? [], "inSyncNodeIds");
|
|
453
|
-
const drainingNodeIds = normaliseNodeIds(input.drainingNodeIds ?? [], "drainingNodeIds");
|
|
454
|
-
const repairingNodeIds = normaliseNodeIds(input.repairingNodeIds ?? [], "repairingNodeIds");
|
|
455
|
-
const faultedNodeIds = normaliseNodeIds(input.faultedNodeIds ?? [], "faultedNodeIds");
|
|
456
|
-
assertNonNegativeTerm(input.primaryTerm ?? 0n);
|
|
457
|
-
assertNonNegativeSeq(input.durabilityPointSeq ?? 0n, "durabilityPointSeq");
|
|
458
|
-
assertPrimaryInGroup(input.currentPrimary ?? null, votingDataBearingNodeIds);
|
|
459
|
-
assertSubset(inSyncNodeIds, votingDataBearingNodeIds, "inSyncNodeIds");
|
|
460
|
-
assertSubset(drainingNodeIds, votingDataBearingNodeIds, "drainingNodeIds");
|
|
461
|
-
assertSubset(repairingNodeIds, votingDataBearingNodeIds, "repairingNodeIds");
|
|
462
|
-
assertSubset(faultedNodeIds, votingDataBearingNodeIds, "faultedNodeIds");
|
|
463
|
-
return {
|
|
464
|
-
clusterId: input.clusterId,
|
|
465
|
-
groupId: input.groupId,
|
|
466
|
-
votingDataBearingNodeIds,
|
|
467
|
-
currentPrimary: input.currentPrimary ? { ...input.currentPrimary } : null,
|
|
468
|
-
primaryTerm: input.primaryTerm ?? 0n,
|
|
469
|
-
durabilityPointSeq: input.durabilityPointSeq ?? 0n,
|
|
470
|
-
inSyncNodeIds,
|
|
471
|
-
drainingNodeIds,
|
|
472
|
-
repairingNodeIds,
|
|
473
|
-
faultedNodeIds,
|
|
474
|
-
compatibility: cloneCompatibility(input.compatibility),
|
|
475
|
-
updatedAtMs: Date.now()
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
function serializeLease(lease) {
|
|
479
|
-
return JSON.stringify(lease);
|
|
480
|
-
}
|
|
481
|
-
function parseLease(raw) {
|
|
482
|
-
const value = JSON.parse(raw);
|
|
483
|
-
return {
|
|
484
|
-
...value,
|
|
485
|
-
metadata: cloneMetadata(value.metadata)
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
function parseNodeSession(raw) {
|
|
489
|
-
const value = JSON.parse(raw);
|
|
490
|
-
return {
|
|
491
|
-
...value,
|
|
492
|
-
lease: parseLease(JSON.stringify(value.lease)),
|
|
493
|
-
groupIds: [...value.groupIds],
|
|
494
|
-
compatibility: cloneCompatibility(value.compatibility),
|
|
495
|
-
metadata: cloneMetadata(value.metadata)
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
function parseLeaseIdForEntry(kind, raw) {
|
|
499
|
-
try {
|
|
500
|
-
if (kind === "node-session") {
|
|
501
|
-
return parseNodeSession(raw).lease.id;
|
|
529
|
+
async renewLease(leaseId, ttlMs) {
|
|
530
|
+
assertNonEmpty(leaseId, "leaseId");
|
|
531
|
+
assertPositiveTtl(ttlMs);
|
|
532
|
+
const entry = this.leases.get(leaseId);
|
|
533
|
+
if (!entry) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
await entry.lease.keepaliveOnce();
|
|
538
|
+
} catch {
|
|
539
|
+
this.leases.delete(leaseId);
|
|
540
|
+
return false;
|
|
502
541
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
542
|
+
const renewedAtMs = Date.now();
|
|
543
|
+
const leaseValue = {
|
|
544
|
+
id: leaseId,
|
|
545
|
+
kind: entry.kind,
|
|
546
|
+
clusterId: entry.clusterId,
|
|
547
|
+
holderId: entry.holderId,
|
|
548
|
+
ttlMs,
|
|
549
|
+
grantedAtMs: renewedAtMs,
|
|
550
|
+
expiresAtMs: renewedAtMs + ttlMs,
|
|
551
|
+
metadata: cloneMetadata(entry.metadata)
|
|
552
|
+
};
|
|
553
|
+
const value = entry.kind === "node-session" && entry.nodeSession ? JSON.stringify({ ...entry.nodeSession, lease: leaseValue }) : serializeLease(leaseValue);
|
|
554
|
+
let refreshed;
|
|
555
|
+
try {
|
|
556
|
+
const result = await this.namespace.if(entry.key, "Lease", "==", leaseId).then(this.namespace.put(entry.key).value(value).ignoreLease()).commit();
|
|
557
|
+
refreshed = result.succeeded === true;
|
|
558
|
+
} catch {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
if (!refreshed) {
|
|
562
|
+
this.leases.delete(leaseId);
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
entry.ttlMs = ttlMs;
|
|
566
|
+
return true;
|
|
506
567
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
function parseGroupState(raw) {
|
|
524
|
-
const value = JSON.parse(raw);
|
|
525
|
-
return {
|
|
526
|
-
...value,
|
|
527
|
-
currentPrimary: value.currentPrimary ? { ...value.currentPrimary } : null,
|
|
528
|
-
votingDataBearingNodeIds: [...value.votingDataBearingNodeIds],
|
|
529
|
-
primaryTerm: BigInt(value.primaryTerm),
|
|
530
|
-
durabilityPointSeq: value.durabilityPointSeq !== void 0 ? BigInt(value.durabilityPointSeq) : 0n,
|
|
531
|
-
inSyncNodeIds: [...value.inSyncNodeIds],
|
|
532
|
-
drainingNodeIds: [...value.drainingNodeIds],
|
|
533
|
-
repairingNodeIds: [...value.repairingNodeIds],
|
|
534
|
-
faultedNodeIds: [...value.faultedNodeIds ?? []],
|
|
535
|
-
compatibility: cloneCompatibility(value.compatibility)
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
function cloneReplicationGroupState(state) {
|
|
539
|
-
return {
|
|
540
|
-
...state,
|
|
541
|
-
currentPrimary: state.currentPrimary ? { ...state.currentPrimary } : null,
|
|
542
|
-
votingDataBearingNodeIds: [...state.votingDataBearingNodeIds],
|
|
543
|
-
durabilityPointSeq: state.durabilityPointSeq,
|
|
544
|
-
inSyncNodeIds: [...state.inSyncNodeIds],
|
|
545
|
-
drainingNodeIds: [...state.drainingNodeIds],
|
|
546
|
-
repairingNodeIds: [...state.repairingNodeIds],
|
|
547
|
-
faultedNodeIds: [...state.faultedNodeIds],
|
|
548
|
-
compatibility: cloneCompatibility(state.compatibility)
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
function isEligiblePromotionSession(state, nodeId, session) {
|
|
552
|
-
if (!session) {
|
|
553
|
-
return false;
|
|
568
|
+
async releaseLease(leaseId) {
|
|
569
|
+
assertNonEmpty(leaseId, "leaseId");
|
|
570
|
+
const entry = this.leases.get(leaseId);
|
|
571
|
+
if (!entry) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
this.leases.delete(leaseId);
|
|
575
|
+
const currentValue = await this.namespace.get(entry.key).string();
|
|
576
|
+
const currentLeaseId = currentValue ? parseLeaseIdForEntry(entry.kind, currentValue) : null;
|
|
577
|
+
let released = false;
|
|
578
|
+
if (currentLeaseId === leaseId && currentValue) {
|
|
579
|
+
const result = await this.namespace.if(entry.key, "Value", "==", currentValue).then(this.namespace.delete().key(entry.key)).commit();
|
|
580
|
+
released = result.succeeded === true;
|
|
581
|
+
}
|
|
582
|
+
await revokeLeaseQuietly(entry.lease);
|
|
583
|
+
return released;
|
|
554
584
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
585
|
+
async registerNodeSession(input) {
|
|
586
|
+
assertNonEmpty(input.clusterId, "clusterId");
|
|
587
|
+
assertNonEmpty(input.nodeId, "nodeId");
|
|
588
|
+
assertPositiveTtl(input.ttlMs);
|
|
589
|
+
const key = nodeSessionKey(input.clusterId, input.nodeId);
|
|
590
|
+
const existingRawSession = await this.namespace.get(key).string();
|
|
591
|
+
if (existingRawSession) {
|
|
592
|
+
const existingSession = parseNodeSession(existingRawSession);
|
|
593
|
+
const supersedesOwnSession = this.grantedNodeSessionLeaseIds.get(key) === existingSession.lease.id;
|
|
594
|
+
if (existingSession.lease.expiresAtMs > Date.now() && !supersedesOwnSession) {
|
|
595
|
+
throw new CoordinatorError(`Node session '${input.nodeId}' is already registered`);
|
|
596
|
+
}
|
|
564
597
|
}
|
|
565
|
-
|
|
566
|
-
|
|
598
|
+
const lease = this.namespace.lease(ttlMsToSeconds(input.ttlMs));
|
|
599
|
+
const leaseId = await lease.grant();
|
|
600
|
+
const grantedAtMs = Date.now();
|
|
601
|
+
const session = {
|
|
602
|
+
clusterId: input.clusterId,
|
|
603
|
+
nodeId: input.nodeId,
|
|
604
|
+
lease: {
|
|
605
|
+
id: leaseId,
|
|
606
|
+
kind: "node-session",
|
|
607
|
+
clusterId: input.clusterId,
|
|
608
|
+
holderId: input.nodeId,
|
|
609
|
+
ttlMs: input.ttlMs,
|
|
610
|
+
grantedAtMs,
|
|
611
|
+
expiresAtMs: grantedAtMs + input.ttlMs,
|
|
612
|
+
metadata: cloneMetadata(input.metadata)
|
|
613
|
+
},
|
|
614
|
+
endpoint: input.endpoint,
|
|
615
|
+
groupIds: [...input.groupIds ?? []],
|
|
616
|
+
dataBearing: input.dataBearing ?? true,
|
|
617
|
+
voting: input.voting ?? true,
|
|
618
|
+
compatibility: cloneCompatibility(input.compatibility),
|
|
619
|
+
metadata: cloneMetadata(input.metadata)
|
|
620
|
+
};
|
|
621
|
+
const rawSession = JSON.stringify(session);
|
|
622
|
+
const transaction = existingRawSession ? this.namespace.if(key, "Value", "==", existingRawSession) : this.namespace.if(key, "Create", "==", 0);
|
|
623
|
+
const result = await transaction.then(this.namespace.put(key).value(rawSession).lease(leaseId)).commit();
|
|
624
|
+
if (!result.succeeded) {
|
|
625
|
+
await revokeLeaseQuietly(lease);
|
|
626
|
+
throw new CoordinatorError(`Node session '${input.nodeId}' registration conflicted with a concurrent write`);
|
|
627
|
+
}
|
|
628
|
+
await this.discardSupersededLeases(key, leaseId);
|
|
629
|
+
this.grantedNodeSessionLeaseIds.set(key, leaseId);
|
|
630
|
+
this.trackLease(lease, {
|
|
631
|
+
leaseId,
|
|
632
|
+
key,
|
|
633
|
+
ttlMs: input.ttlMs,
|
|
634
|
+
ttlSeconds: ttlMsToSeconds(input.ttlMs),
|
|
635
|
+
kind: "node-session",
|
|
636
|
+
clusterId: input.clusterId,
|
|
637
|
+
holderId: input.nodeId,
|
|
638
|
+
metadata: cloneMetadata(input.metadata),
|
|
639
|
+
nodeSession: {
|
|
640
|
+
clusterId: session.clusterId,
|
|
641
|
+
nodeId: session.nodeId,
|
|
642
|
+
endpoint: session.endpoint,
|
|
643
|
+
groupIds: [...session.groupIds],
|
|
644
|
+
dataBearing: session.dataBearing,
|
|
645
|
+
voting: session.voting,
|
|
646
|
+
compatibility: cloneCompatibility(session.compatibility),
|
|
647
|
+
metadata: cloneMetadata(session.metadata)
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return parseNodeSession(rawSession);
|
|
567
651
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (enabled) {
|
|
574
|
-
next.push(nodeId);
|
|
652
|
+
async getLiveNodeSession(clusterId, nodeId) {
|
|
653
|
+
assertNonEmpty(clusterId, "clusterId");
|
|
654
|
+
assertNonEmpty(nodeId, "nodeId");
|
|
655
|
+
const value = await this.namespace.get(nodeSessionKey(clusterId, nodeId)).string();
|
|
656
|
+
return value ? parseNodeSession(value) : null;
|
|
575
657
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
return values.filter((value) => value !== nodeId);
|
|
585
|
-
}
|
|
586
|
-
function assertSubset(values, allowed, name) {
|
|
587
|
-
const allowedSet = new Set(allowed);
|
|
588
|
-
for (const value of values) {
|
|
589
|
-
if (!allowedSet.has(value)) {
|
|
590
|
-
throw new RangeError(`${name} contains node id '${value}' that is not configured for the replication group`);
|
|
658
|
+
async deregisterNodeSession(clusterId, nodeId) {
|
|
659
|
+
assertNonEmpty(clusterId, "clusterId");
|
|
660
|
+
assertNonEmpty(nodeId, "nodeId");
|
|
661
|
+
const key = nodeSessionKey(clusterId, nodeId);
|
|
662
|
+
for (const [leaseId, entry] of this.leases) {
|
|
663
|
+
if (entry.key === key) {
|
|
664
|
+
await this.releaseLease(leaseId);
|
|
665
|
+
}
|
|
591
666
|
}
|
|
592
667
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (!primary) return;
|
|
596
|
-
assertNonEmpty(primary.nodeId, "primary.nodeId");
|
|
597
|
-
if (!votingDataBearingNodeIds.includes(primary.nodeId)) {
|
|
598
|
-
throw new RangeError(`Primary node '${primary.nodeId}' is not configured for the replication group`);
|
|
668
|
+
setReplicationGroupState(input) {
|
|
669
|
+
return this.groups.setReplicationGroupState(input);
|
|
599
670
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (term < 0n) {
|
|
603
|
-
throw new RangeError("primaryTerm must not be negative");
|
|
671
|
+
getReplicationGroupState(clusterId, groupId) {
|
|
672
|
+
return this.groups.getReplicationGroupState(clusterId, groupId);
|
|
604
673
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (seq < 0n) {
|
|
608
|
-
throw new RangeError(`${name} must not be negative`);
|
|
674
|
+
watchReplicationGroup(clusterId, groupId, watcher) {
|
|
675
|
+
return this.groups.watchReplicationGroup(clusterId, groupId, watcher);
|
|
609
676
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
677
|
+
compareAndAdvancePrimaryTerm(input) {
|
|
678
|
+
return this.groups.compareAndAdvancePrimaryTerm(input);
|
|
679
|
+
}
|
|
680
|
+
updateInSyncSet(input) {
|
|
681
|
+
return this.groups.updateInSyncSet(input);
|
|
682
|
+
}
|
|
683
|
+
admitNodeToInSyncSet(input) {
|
|
684
|
+
return this.groups.admitNodeToInSyncSet(input);
|
|
685
|
+
}
|
|
686
|
+
updateNodeMaintenance(input) {
|
|
687
|
+
return this.groups.updateNodeMaintenance(input);
|
|
688
|
+
}
|
|
689
|
+
promoteEligibleReplica(input) {
|
|
690
|
+
return this.groups.promoteEligibleReplica(input);
|
|
691
|
+
}
|
|
692
|
+
async close() {
|
|
693
|
+
const watcherCancels = [];
|
|
694
|
+
for (const watcher of this.watchers) {
|
|
695
|
+
watcherCancels.push(watcher.cancel());
|
|
696
|
+
}
|
|
697
|
+
this.watchers.clear();
|
|
698
|
+
await Promise.allSettled(watcherCancels);
|
|
699
|
+
const leaseRevokes = [];
|
|
700
|
+
for (const entry of this.leases.values()) {
|
|
701
|
+
leaseRevokes.push(revokeLeaseQuietly(entry.lease));
|
|
616
702
|
}
|
|
703
|
+
this.leases.clear();
|
|
704
|
+
this.grantedNodeSessionLeaseIds.clear();
|
|
705
|
+
await Promise.allSettled(leaseRevokes);
|
|
706
|
+
this.client.close();
|
|
617
707
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
708
|
+
async discardSupersededLeases(key, keepLeaseId) {
|
|
709
|
+
const superseded = [];
|
|
710
|
+
for (const [leaseId, entry] of this.leases) {
|
|
711
|
+
if (entry.key === key && leaseId !== keepLeaseId) {
|
|
712
|
+
this.leases.delete(leaseId);
|
|
713
|
+
superseded.push(entry);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
await Promise.allSettled(superseded.map((entry) => revokeLeaseQuietly(entry.lease)));
|
|
623
717
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return compatibility ? { ...compatibility } : void 0;
|
|
628
|
-
}
|
|
629
|
-
function cloneMetadata(metadata) {
|
|
630
|
-
return metadata ? { ...metadata } : void 0;
|
|
631
|
-
}
|
|
632
|
-
function assertPositiveTtl(ttlMs) {
|
|
633
|
-
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
634
|
-
throw new RangeError("ttlMs must be a positive safe integer");
|
|
718
|
+
async getLeaseFromKey(key) {
|
|
719
|
+
const value = await this.namespace.get(key).string();
|
|
720
|
+
return value ? parseLease(value) : null;
|
|
635
721
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
722
|
+
trackLease(lease, entry) {
|
|
723
|
+
const fullEntry = { ...entry, lease };
|
|
724
|
+
this.leases.set(entry.leaseId, fullEntry);
|
|
725
|
+
lease.on("lost", (err) => {
|
|
726
|
+
this.leases.delete(entry.leaseId);
|
|
727
|
+
this.onWatcherError?.(err instanceof Error ? err : new CoordinatorError(String(err)));
|
|
728
|
+
});
|
|
640
729
|
}
|
|
730
|
+
};
|
|
731
|
+
function createEtcdCoordinator(options) {
|
|
732
|
+
return new EtcdClusterCoordinator(options);
|
|
641
733
|
}
|
|
642
734
|
async function revokeLeaseQuietly(lease) {
|
|
643
735
|
try {
|