@delali/sirannon-db 0.3.4-next.62 → 0.3.4
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 +79 -56
- package/dist/backup/index.d.ts +44 -45
- package/dist/backup/index.mjs +7 -7
- package/dist/backup-scheduler/index.d.ts +42 -41
- package/dist/backup-scheduler/index.mjs +4 -4
- package/dist/baseline-Co3iIsN_.d.ts +19 -0
- package/dist/change-tracker-DqCdeEyx.d.ts +128 -0
- package/dist/{chunk-VOSJBZ6Q.mjs → chunk-2ES3DT4A.mjs} +71 -33
- package/dist/{chunk-6CMCO7S4.mjs → chunk-2O3PP4CF.mjs} +1 -1
- package/dist/{chunk-E7Q7HE46.mjs → chunk-3BZLZ47R.mjs} +26 -26
- package/dist/chunk-4KASJKKK.mjs +2501 -0
- package/dist/{chunk-MYNI2WFS.mjs → chunk-52XW3Z76.mjs} +40 -8
- package/dist/chunk-55YRLSM2.mjs +57 -0
- package/dist/{chunk-KDAHYFW7.mjs → chunk-6AOJCNRU.mjs} +5 -5
- package/dist/{chunk-KXTYR2OQ.mjs → chunk-A3PHCO6W.mjs} +39 -111
- package/dist/chunk-ADNWEHGZ.mjs +299 -0
- package/dist/{chunk-2QLXDHAP.mjs → chunk-BLMRNSUG.mjs} +11 -2
- package/dist/chunk-BVMZJOVM.mjs +5 -0
- package/dist/{chunk-IWGIYDMZ.mjs → chunk-BZWDPF4G.mjs} +1 -1
- package/dist/{chunk-YVF2ZJTE.mjs → chunk-CZL4D3BM.mjs} +1 -1
- package/dist/{chunk-7SC2TZAS.mjs → chunk-DX4WGJL7.mjs} +2 -2
- package/dist/{chunk-45MKELYA.mjs → chunk-ESTZZPT5.mjs} +132 -13
- package/dist/{chunk-QP5L5DFJ.mjs → chunk-FJPR2CJX.mjs} +3 -3
- package/dist/chunk-HR7K3GZW.mjs +36 -0
- package/dist/{chunk-LXCGPZPA.mjs → chunk-IEKIYO4V.mjs} +1 -1
- package/dist/{chunk-TUD5CJ76.mjs → chunk-KB4OLJAD.mjs} +26 -19
- package/dist/{chunk-PBRXXISQ.mjs → chunk-KINDFBQP.mjs} +3 -2
- package/dist/{chunk-E6IMX7RS.mjs → chunk-KJLNYOOL.mjs} +2 -2
- package/dist/{chunk-BTTFW4Z4.mjs → chunk-KU32OT4H.mjs} +2 -10
- package/dist/{chunk-JQWGHSEH.mjs → chunk-NVSQ2T5Q.mjs} +71 -70
- package/dist/{chunk-W4EUTJNT.mjs → chunk-NZGSDALX.mjs} +94 -96
- package/dist/{chunk-U445A3C4.mjs → chunk-QGVRRKB2.mjs} +1 -1
- package/dist/{chunk-GYCEJNU4.mjs → chunk-R7GBXNTO.mjs} +1 -1
- package/dist/{chunk-7C36BCSN.mjs → chunk-VDN6FHU4.mjs} +1 -1
- package/dist/{chunk-6IYS4WLH.mjs → chunk-VO3ZWA2G.mjs} +142 -115
- package/dist/chunk-WOMADPM4.mjs +12 -0
- package/dist/{chunk-OUSWVNWT.mjs → chunk-YCMB6GR4.mjs} +1 -1
- package/dist/{chunk-YNTDOFTN.mjs → chunk-YW7IKQ7T.mjs} +1 -1
- package/dist/{chunk-M2BEAH5I.mjs → chunk-ZN4HI5FK.mjs} +126 -72
- package/dist/{chunk-SBL6GN43.mjs → chunk-ZYWI45EN.mjs} +1 -1
- package/dist/client/index.d.ts +135 -137
- package/dist/client/index.mjs +177 -82
- package/dist/client/topology.d.ts +31 -31
- package/dist/client/topology.mjs +42 -23
- package/dist/client-base-BxTocU1x.d.ts +398 -0
- package/dist/client-streams-YKANXMWO.mjs +112 -0
- package/dist/codegen/cli.mjs +4 -4
- package/dist/codegen/index.d.ts +36 -29
- package/dist/codegen/index.mjs +4 -4
- package/dist/core/index.d.ts +130 -115
- package/dist/core/index.mjs +387 -297
- package/dist/core/writer-worker.mjs +3 -3
- package/dist/database-CGDsXW1K.d.ts +959 -0
- package/dist/database-backup-DcLhxoa1.d.ts +143 -0
- package/dist/driver/better-sqlite3.d.ts +11 -10
- package/dist/driver/better-sqlite3.mjs +11 -11
- package/dist/driver/bun.d.ts +9 -8
- package/dist/driver/expo.d.ts +6 -7
- package/dist/driver/node.d.ts +11 -10
- package/dist/driver/node.mjs +13 -11
- package/dist/driver/wa-sqlite.d.ts +6 -6
- package/dist/errors-CJUkQ-KL.d.ts +199 -0
- package/dist/file-migrations/index.d.ts +20 -18
- package/dist/file-migrations/index.mjs +3 -3
- package/dist/forward-rpc-VRWFZXHN.mjs +44 -0
- package/dist/operation-registry-CGhNT-Sn.d.ts +96 -0
- package/dist/primary-wins-CHNOYuN7.d.ts +81 -0
- package/dist/protocol-DFpNNsIT.d.ts +153 -0
- package/dist/query-types-5B02ixH0.d.ts +129 -0
- package/dist/react/index.d.ts +22 -22
- package/dist/replication/coordinator/etcd.d.ts +37 -31
- package/dist/replication/coordinator/etcd.mjs +296 -131
- package/dist/replication/index.d.ts +201 -161
- package/dist/replication/index.mjs +630 -529
- package/dist/restore-options-CH8mE-Tb.d.ts +84 -0
- package/dist/server/index.d.ts +201 -191
- package/dist/server/index.mjs +736 -619
- package/dist/server-options-aYtCkefX.d.ts +317 -0
- package/dist/server-streams-24XL4T43.mjs +254 -0
- package/dist/sirannon-J3EgPVD7.d.ts +179 -0
- package/dist/transport/grpc.d.ts +37 -24
- package/dist/transport/grpc.mjs +83 -3199
- package/dist/transport/memory.d.ts +25 -26
- package/dist/transport/memory.mjs +18 -9
- package/dist/types-BrAiDGOb.d.ts +505 -0
- package/dist/types-CEJG0eFS.d.ts +88 -0
- package/dist/types-Dc8oWlxj.d.ts +1580 -0
- package/dist/types-DqODKeOq.d.ts +123 -0
- package/dist/types-guEKSzMr.d.ts +367 -0
- package/package.json +7 -7
- package/dist/baseline-D93hcIEE.d.ts +0 -17
- package/dist/change-tracker-CjRFuZdB.d.ts +0 -144
- package/dist/client-base-Cmd9EVme.d.ts +0 -397
- package/dist/database-BLQvqjsw.d.ts +0 -917
- package/dist/database-backup-B7oszQ2n.d.ts +0 -141
- package/dist/errors-Dei4GdBb.d.ts +0 -196
- package/dist/operation-registry-oiPuSYI6.d.ts +0 -94
- package/dist/primary-wins-B0np8JS3.d.ts +0 -85
- package/dist/protocol-HI-nY8DD.d.ts +0 -152
- package/dist/query-types-p1liOP-b.d.ts +0 -127
- package/dist/restore-options-CksYsH1l.d.ts +0 -83
- package/dist/server-options-Bj5iJnjx.d.ts +0 -292
- package/dist/sirannon-BPxC5tC2.d.ts +0 -178
- package/dist/types-B0EhgDbr.d.ts +0 -340
- package/dist/types-CbjfRwrK.d.ts +0 -499
- package/dist/types-CjhxcjhA.d.ts +0 -123
- package/dist/types-DPSmjU54.d.ts +0 -1518
- package/dist/types-DTO4CYhW.d.ts +0 -88
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { compatibilityAllowsPromotion } from '../chunk-ER7ODTDA.mjs';
|
|
2
|
-
import { CoordinatorError, WriteConcernError, AuthorityError, SyncError, TopologyError, StalePrimaryError, NodeDrainingError, NodeNotInSyncError, ReadConcernError, ProtocolVersionMismatchError } from '../chunk-
|
|
3
|
-
export { AuthorityError,
|
|
4
|
-
import { LWWResolver,
|
|
5
|
-
export {
|
|
6
|
-
import '../chunk-
|
|
7
|
-
import { DumpOps, dumpSchema, tablesInFkOrder } from '../chunk-
|
|
8
|
-
import { canonicaliseForChecksum,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { CoordinatorError, WriteConcernError, AuthorityError, SyncError, TopologyError, StalePrimaryError, NodeDrainingError, NodeNotInSyncError, ReadConcernError, ProtocolVersionMismatchError } from '../chunk-KU32OT4H.mjs';
|
|
3
|
+
export { AuthorityError, CoordinatorError, FailoverError, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, ProtocolVersionMismatchError, ReadConcernError, StalePrimaryError, SyncError, TopologyError, TransportError, UnsafeRecoveryRequiredError, WriteConcernError } from '../chunk-KU32OT4H.mjs';
|
|
4
|
+
import { LWWResolver, BatchReader, BatchApplier, StampOps, isWellFormedHlc, Transaction, loadPersistedHlc } from '../chunk-A3PHCO6W.mjs';
|
|
5
|
+
export { LWWResolver } from '../chunk-A3PHCO6W.mjs';
|
|
6
|
+
import { runWriterTransaction, execute } from '../chunk-52XW3Z76.mjs';
|
|
7
|
+
import { DumpOps, dumpSchema, tablesInFkOrder } from '../chunk-FJPR2CJX.mjs';
|
|
8
|
+
import { canonicaliseForChecksum, HLC, PkResolver, selectMaxChangeHlc, selectMaxColumnVersionHlc, selectMaxAppliedSourceSeq, upsertPeerAckedSeq, selectPeerAckedSeq, selectMaxChangeSeq, selectMinPeerAckedSeq, upsertSyncTableStatus, upsertSyncMeta, selectSyncMetaRow, selectCompletedSyncTableNames, ensureChangesTable, ensureReplicationStateTables, ensureMetaTable, IDENTIFIER_RE, setForeignKeysEnabled, deleteSyncTableStates, extractDroppedTable, insertDdlChange, validateDdlSafety, prepareInsertAppliedChange, selectMaxAppliedSourceSeqByNode } from '../chunk-ESTZZPT5.mjs';
|
|
9
|
+
export { HLC } from '../chunk-ESTZZPT5.mjs';
|
|
10
|
+
import { ReplicationError, BatchValidationError } from '../chunk-BLMRNSUG.mjs';
|
|
11
|
+
export { BatchValidationError, ConflictError, ReplicationError } from '../chunk-BLMRNSUG.mjs';
|
|
12
|
+
import { CHANGES_TABLE, INTERNAL_TABLE_PREFIX, isReservedIdentifier } from '../chunk-2O3PP4CF.mjs';
|
|
12
13
|
import '../chunk-GVCNMPOS.mjs';
|
|
13
|
-
import '../chunk-
|
|
14
|
+
import '../chunk-KINDFBQP.mjs';
|
|
14
15
|
import { EventEmitter } from 'events';
|
|
15
16
|
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
16
17
|
|
|
@@ -22,10 +23,10 @@ var FieldMergeResolver = class {
|
|
|
22
23
|
this.getColumnVersions = getColumnVersions;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* Returns the resolution for one conflicting row, merging changes to different columns and settling each column that both sides changed by its per-column stamp.
|
|
26
27
|
*
|
|
27
28
|
* @param ctx - The local and incoming versions of one row.
|
|
28
|
-
* @returns
|
|
29
|
+
* @returns The version to keep, or the merged row.
|
|
29
30
|
*/
|
|
30
31
|
async resolve(ctx) {
|
|
31
32
|
const columnVersions = await this.getColumnVersions(ctx.table, ctx.rowId);
|
|
@@ -102,7 +103,7 @@ var PrimaryWinsResolver = class {
|
|
|
102
103
|
this.primaryNodeId = primaryNodeId;
|
|
103
104
|
}
|
|
104
105
|
/**
|
|
105
|
-
*
|
|
106
|
+
* Returns the version that the primary node wrote, or the last-writer-wins result when neither version comes from the primary.
|
|
106
107
|
*
|
|
107
108
|
* @param ctx - The local and incoming versions of one row.
|
|
108
109
|
* @returns Which version to write.
|
|
@@ -174,8 +175,9 @@ function toClusterStatusInfo(status, options) {
|
|
|
174
175
|
};
|
|
175
176
|
}
|
|
176
177
|
function toClusterReadEndpoints(coordinator, endpoints) {
|
|
178
|
+
const live = coordinator.liveNodeIds;
|
|
177
179
|
return coordinator.votingDataBearingNodeIds.filter(
|
|
178
|
-
(nodeId) => !coordinator.faultedNodeIds.includes(nodeId) && !coordinator.drainingNodeIds.includes(nodeId) && !coordinator.repairingNodeIds.includes(nodeId)
|
|
180
|
+
(nodeId) => !coordinator.faultedNodeIds.includes(nodeId) && !coordinator.drainingNodeIds.includes(nodeId) && !coordinator.repairingNodeIds.includes(nodeId) && (live === void 0 || live.includes(nodeId))
|
|
179
181
|
).map((nodeId) => ({
|
|
180
182
|
nodeId,
|
|
181
183
|
endpoint: endpoints[nodeId] ?? "",
|
|
@@ -451,6 +453,12 @@ var PeerTracker = class {
|
|
|
451
453
|
}
|
|
452
454
|
this.checkWaiters();
|
|
453
455
|
}
|
|
456
|
+
onBatchApplied(nodeId, highestHlc) {
|
|
457
|
+
const peer = this.peers.get(nodeId);
|
|
458
|
+
if (peer && (peer.lastReceivedHlc === "" || HLC.compare(highestHlc, peer.lastReceivedHlc) > 0)) {
|
|
459
|
+
peer.lastReceivedHlc = highestHlc;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
454
462
|
recordInFlightBatch(nodeId, batch) {
|
|
455
463
|
const peer = this.peers.get(nodeId);
|
|
456
464
|
if (peer) {
|
|
@@ -927,7 +935,9 @@ function getCoordinatorRuntimeStatus(engine) {
|
|
|
927
935
|
const config = engine.config.coordinator;
|
|
928
936
|
const state = engine.coordinatorState;
|
|
929
937
|
if (!config || !state) return void 0;
|
|
938
|
+
const connected = isCoordinatorConnected(engine);
|
|
930
939
|
return {
|
|
940
|
+
...connected && engine.liveNodeIds ? { liveNodeIds: [...engine.liveNodeIds] } : {},
|
|
931
941
|
clusterId: config.clusterId,
|
|
932
942
|
groupId: config.groupId,
|
|
933
943
|
currentPrimary: state.currentPrimary ? { ...state.currentPrimary } : null,
|
|
@@ -938,7 +948,7 @@ function getCoordinatorRuntimeStatus(engine) {
|
|
|
938
948
|
faultedNodeIds: [...state.faultedNodeIds],
|
|
939
949
|
votingDataBearingNodeIds: [...state.votingDataBearingNodeIds],
|
|
940
950
|
authority: engine.coordinatorAuthority,
|
|
941
|
-
connected
|
|
951
|
+
connected,
|
|
942
952
|
controllerState: engine.controllerState
|
|
943
953
|
};
|
|
944
954
|
}
|
|
@@ -1148,268 +1158,6 @@ async function evaluateFormerPrimaryHistory(engine, state) {
|
|
|
1148
1158
|
});
|
|
1149
1159
|
return nextState;
|
|
1150
1160
|
}
|
|
1151
|
-
|
|
1152
|
-
// src/replication/engine/coordinator-lifecycle.ts
|
|
1153
|
-
var DEFAULT_CONTROLLER_LEASE_TTL_MS = 1e4;
|
|
1154
|
-
var DEFAULT_CONTROLLER_TICK_INTERVAL_MS = 1e3;
|
|
1155
|
-
var IN_SYNC_RECONCILE_INTERVAL_MS = 1e3;
|
|
1156
|
-
function unrefTimer(timer) {
|
|
1157
|
-
const unref = timer.unref;
|
|
1158
|
-
unref?.call(timer);
|
|
1159
|
-
}
|
|
1160
|
-
function localCoordinatorPrimary(engine) {
|
|
1161
|
-
const endpoint = engine.config.coordinator?.endpoint;
|
|
1162
|
-
return endpoint ? { nodeId: engine.nodeId, endpoint } : { nodeId: engine.nodeId };
|
|
1163
|
-
}
|
|
1164
|
-
async function startCoordinatorMode(engine) {
|
|
1165
|
-
const config = engine.config.coordinator;
|
|
1166
|
-
if (!config) return;
|
|
1167
|
-
const coordinator = config.coordinator;
|
|
1168
|
-
let state = await coordinator.getReplicationGroupState(config.clusterId, config.groupId);
|
|
1169
|
-
if (!state && config.votingDataBearingNodeIds) {
|
|
1170
|
-
state = await coordinator.setReplicationGroupState({
|
|
1171
|
-
clusterId: config.clusterId,
|
|
1172
|
-
groupId: config.groupId,
|
|
1173
|
-
votingDataBearingNodeIds: config.votingDataBearingNodeIds,
|
|
1174
|
-
currentPrimary: engine.config.topology.role === "primary" ? localCoordinatorPrimary(engine) : null,
|
|
1175
|
-
primaryTerm: 1n,
|
|
1176
|
-
inSyncNodeIds: [engine.nodeId],
|
|
1177
|
-
compatibility: config.compatibility
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
if (!state) {
|
|
1181
|
-
throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
|
|
1182
|
-
}
|
|
1183
|
-
noteCoordinatorContact(engine);
|
|
1184
|
-
engine.coordinatorState = state;
|
|
1185
|
-
engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, state);
|
|
1186
|
-
const session = await coordinator.registerNodeSession({
|
|
1187
|
-
clusterId: config.clusterId,
|
|
1188
|
-
nodeId: engine.nodeId,
|
|
1189
|
-
ttlMs: config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS,
|
|
1190
|
-
endpoint: config.endpoint,
|
|
1191
|
-
groupIds: [config.groupId],
|
|
1192
|
-
dataBearing: true,
|
|
1193
|
-
voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
|
|
1194
|
-
compatibility: config.compatibility
|
|
1195
|
-
});
|
|
1196
|
-
engine.nodeSessionLeaseId = session.lease.id;
|
|
1197
|
-
noteCoordinatorContact(engine);
|
|
1198
|
-
engine.coordinatorWatchDisposer = await coordinator.watchReplicationGroup(config.clusterId, config.groupId, (next) => {
|
|
1199
|
-
handleCoordinatorStateUpdate(engine, next);
|
|
1200
|
-
});
|
|
1201
|
-
startCoordinatorLeaseRenewal(engine);
|
|
1202
|
-
startInSyncReconcileLoop(engine);
|
|
1203
|
-
startControllerLoop(engine);
|
|
1204
|
-
}
|
|
1205
|
-
function startInSyncReconcileLoop(engine) {
|
|
1206
|
-
const timer = setInterval(() => {
|
|
1207
|
-
reconcileInSyncSet(engine).catch((err) => {
|
|
1208
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1209
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-in-sync-reconcile", recoverable: true });
|
|
1210
|
-
});
|
|
1211
|
-
}, IN_SYNC_RECONCILE_INTERVAL_MS);
|
|
1212
|
-
unrefTimer(timer);
|
|
1213
|
-
engine.inSyncReconcileTimer = timer;
|
|
1214
|
-
}
|
|
1215
|
-
function handleCoordinatorStateUpdate(engine, next) {
|
|
1216
|
-
const previous = engine.coordinatorState;
|
|
1217
|
-
const wasPrimary = previous ? hasCurrentPrimaryAuthorityFor(engine, previous) : engine.coordinatorAuthority;
|
|
1218
|
-
noteCoordinatorContact(engine);
|
|
1219
|
-
engine.coordinatorState = next;
|
|
1220
|
-
engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, next);
|
|
1221
|
-
if (wasPrimary && !engine.coordinatorAuthority && next.primaryTerm > (previous?.primaryTerm ?? 0n)) {
|
|
1222
|
-
handleFormerPrimaryDemotion(engine, next).catch((err) => {
|
|
1223
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1224
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-former-primary-demotion", recoverable: false });
|
|
1225
|
-
});
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
startCoordinatorRejoinSyncIfReady(engine, next);
|
|
1229
|
-
}
|
|
1230
|
-
function startCoordinatorLeaseRenewal(engine) {
|
|
1231
|
-
const config = engine.config.coordinator;
|
|
1232
|
-
if (!config || !engine.nodeSessionLeaseId) return;
|
|
1233
|
-
const ttlMs = config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS;
|
|
1234
|
-
const timer = setInterval(
|
|
1235
|
-
() => {
|
|
1236
|
-
void keepCoordinatorSessionAlive(engine, ttlMs);
|
|
1237
|
-
},
|
|
1238
|
-
Math.max(1e3, Math.floor(ttlMs / 3))
|
|
1239
|
-
);
|
|
1240
|
-
unrefTimer(timer);
|
|
1241
|
-
engine.coordinatorLeaseTimer = timer;
|
|
1242
|
-
}
|
|
1243
|
-
function applyAuthorityFromKnownState(engine) {
|
|
1244
|
-
const state = engine.coordinatorState;
|
|
1245
|
-
engine.coordinatorAuthority = state ? hasCurrentPrimaryAuthorityFor(engine, state) : false;
|
|
1246
|
-
}
|
|
1247
|
-
async function keepCoordinatorSessionAlive(engine, ttlMs) {
|
|
1248
|
-
const config = engine.config.coordinator;
|
|
1249
|
-
if (!config || !engine.running || engine.coordinatorSessionRestoring) return;
|
|
1250
|
-
const leaseId = engine.nodeSessionLeaseId;
|
|
1251
|
-
if (leaseId) {
|
|
1252
|
-
try {
|
|
1253
|
-
if (await config.coordinator.renewLease(leaseId, ttlMs)) {
|
|
1254
|
-
noteCoordinatorContact(engine);
|
|
1255
|
-
applyAuthorityFromKnownState(engine);
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
} catch (err) {
|
|
1259
|
-
engine.coordinatorAuthority = false;
|
|
1260
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1261
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-session-renew", recoverable: true });
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
engine.coordinatorAuthority = false;
|
|
1266
|
-
engine.nodeSessionLeaseId = null;
|
|
1267
|
-
await restoreCoordinatorSession(engine, ttlMs);
|
|
1268
|
-
}
|
|
1269
|
-
async function restoreCoordinatorSession(engine, ttlMs) {
|
|
1270
|
-
const config = engine.config.coordinator;
|
|
1271
|
-
if (!config) return;
|
|
1272
|
-
engine.coordinatorSessionRestoring = true;
|
|
1273
|
-
try {
|
|
1274
|
-
const state = await config.coordinator.getReplicationGroupState(config.clusterId, config.groupId);
|
|
1275
|
-
if (!state) {
|
|
1276
|
-
throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
|
|
1277
|
-
}
|
|
1278
|
-
const session = await config.coordinator.registerNodeSession({
|
|
1279
|
-
clusterId: config.clusterId,
|
|
1280
|
-
nodeId: engine.nodeId,
|
|
1281
|
-
ttlMs,
|
|
1282
|
-
endpoint: config.endpoint,
|
|
1283
|
-
groupIds: [config.groupId],
|
|
1284
|
-
dataBearing: true,
|
|
1285
|
-
voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
|
|
1286
|
-
compatibility: config.compatibility
|
|
1287
|
-
});
|
|
1288
|
-
if (!engine.running) {
|
|
1289
|
-
await config.coordinator.releaseLease(session.lease.id).catch(() => void 0);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
engine.nodeSessionLeaseId = session.lease.id;
|
|
1293
|
-
engine.coordinatorState = state;
|
|
1294
|
-
noteCoordinatorContact(engine);
|
|
1295
|
-
applyAuthorityFromKnownState(engine);
|
|
1296
|
-
} catch (err) {
|
|
1297
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1298
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-session-restore", recoverable: true });
|
|
1299
|
-
} finally {
|
|
1300
|
-
engine.coordinatorSessionRestoring = false;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
function startControllerLoop(engine) {
|
|
1304
|
-
const config = engine.config.coordinator;
|
|
1305
|
-
if (!config) return;
|
|
1306
|
-
const controllerConfig = typeof config.controller === "object" ? config.controller : {};
|
|
1307
|
-
const enabled = typeof config.controller === "boolean" ? config.controller : controllerConfig.enabled ?? true;
|
|
1308
|
-
if (!enabled) {
|
|
1309
|
-
engine.controllerState = "disabled";
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
engine.controllerState = "standby";
|
|
1313
|
-
const ttlMs = controllerConfig.leaseTtlMs ?? DEFAULT_CONTROLLER_LEASE_TTL_MS;
|
|
1314
|
-
const holderId = controllerConfig.holderId ?? engine.nodeId;
|
|
1315
|
-
const tickMs = controllerConfig.tickIntervalMs ?? DEFAULT_CONTROLLER_TICK_INTERVAL_MS;
|
|
1316
|
-
const timer = setInterval(() => {
|
|
1317
|
-
controllerTick(engine, holderId, ttlMs).catch((err) => {
|
|
1318
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1319
|
-
engine.controllerState = "lost";
|
|
1320
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-controller", recoverable: true });
|
|
1321
|
-
});
|
|
1322
|
-
}, tickMs);
|
|
1323
|
-
unrefTimer(timer);
|
|
1324
|
-
engine.controllerTimer = timer;
|
|
1325
|
-
}
|
|
1326
|
-
async function controllerTick(engine, holderId, ttlMs) {
|
|
1327
|
-
const config = engine.config.coordinator;
|
|
1328
|
-
if (!config) return;
|
|
1329
|
-
if (engine.controllerLeaseId) {
|
|
1330
|
-
const renewed = await config.coordinator.renewLease(engine.controllerLeaseId, ttlMs);
|
|
1331
|
-
if (!renewed) {
|
|
1332
|
-
engine.controllerLeaseId = null;
|
|
1333
|
-
engine.controllerState = "lost";
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
noteCoordinatorContact(engine);
|
|
1337
|
-
engine.controllerState = "active";
|
|
1338
|
-
await runControllerPromotionCheck(engine);
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
const acquired = await config.coordinator.tryAcquireControllerLease({
|
|
1342
|
-
clusterId: config.clusterId,
|
|
1343
|
-
holderId,
|
|
1344
|
-
ttlMs
|
|
1345
|
-
});
|
|
1346
|
-
noteCoordinatorContact(engine);
|
|
1347
|
-
if (acquired.acquired) {
|
|
1348
|
-
engine.controllerLeaseId = acquired.lease.id;
|
|
1349
|
-
engine.controllerState = "active";
|
|
1350
|
-
await runControllerPromotionCheck(engine);
|
|
1351
|
-
} else {
|
|
1352
|
-
engine.controllerState = "standby";
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
async function runControllerPromotionCheck(engine) {
|
|
1356
|
-
const config = engine.config.coordinator;
|
|
1357
|
-
if (!config) return;
|
|
1358
|
-
const state = await refreshCoordinatorState(engine);
|
|
1359
|
-
if (!state) return;
|
|
1360
|
-
const primaryNodeId = state.currentPrimary?.nodeId;
|
|
1361
|
-
const primaryLive = primaryNodeId ? await config.coordinator.getLiveNodeSession(config.clusterId, primaryNodeId) : null;
|
|
1362
|
-
const primaryCanKeepDuty = primaryNodeId && primaryLive && compatibilityAllowsPromotion(state.compatibility, primaryLive.compatibility) && !state.drainingNodeIds.includes(primaryNodeId) && !state.repairingNodeIds.includes(primaryNodeId) && !state.faultedNodeIds.includes(primaryNodeId);
|
|
1363
|
-
if (primaryCanKeepDuty) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
try {
|
|
1367
|
-
const promoted = await config.coordinator.promoteEligibleReplica({
|
|
1368
|
-
clusterId: config.clusterId,
|
|
1369
|
-
groupId: config.groupId,
|
|
1370
|
-
excludeNodeIds: primaryNodeId ? [primaryNodeId] : []
|
|
1371
|
-
});
|
|
1372
|
-
engine.coordinatorState = promoted;
|
|
1373
|
-
engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, promoted);
|
|
1374
|
-
} catch (err) {
|
|
1375
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1376
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-promotion", recoverable: true });
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
function stopCoordinatorTimers(engine) {
|
|
1380
|
-
if (engine.coordinatorLeaseTimer) {
|
|
1381
|
-
clearInterval(engine.coordinatorLeaseTimer);
|
|
1382
|
-
engine.coordinatorLeaseTimer = null;
|
|
1383
|
-
}
|
|
1384
|
-
if (engine.inSyncReconcileTimer) {
|
|
1385
|
-
clearInterval(engine.inSyncReconcileTimer);
|
|
1386
|
-
engine.inSyncReconcileTimer = null;
|
|
1387
|
-
}
|
|
1388
|
-
if (engine.controllerTimer) {
|
|
1389
|
-
clearInterval(engine.controllerTimer);
|
|
1390
|
-
engine.controllerTimer = null;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
async function stopCoordinatorMode(engine) {
|
|
1394
|
-
const config = engine.config.coordinator;
|
|
1395
|
-
if (!config) return;
|
|
1396
|
-
if (engine.coordinatorWatchDisposer) {
|
|
1397
|
-
await engine.coordinatorWatchDisposer();
|
|
1398
|
-
engine.coordinatorWatchDisposer = null;
|
|
1399
|
-
}
|
|
1400
|
-
if (engine.controllerLeaseId) {
|
|
1401
|
-
const leaseId = engine.controllerLeaseId;
|
|
1402
|
-
engine.controllerLeaseId = null;
|
|
1403
|
-
await config.coordinator.releaseLease(leaseId).catch((err) => {
|
|
1404
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1405
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-controller-release", recoverable: true });
|
|
1406
|
-
});
|
|
1407
|
-
}
|
|
1408
|
-
await config.coordinator.deregisterNodeSession(config.clusterId, engine.nodeId).catch((err) => {
|
|
1409
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1410
|
-
engine.emitError({ error: wrappedErr, operation: "coordinator-session-deregister", recoverable: true });
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
1161
|
function assertReadyPhase(engine, activity) {
|
|
1414
1162
|
if (engine.syncState.phase !== "ready") {
|
|
1415
1163
|
throw new SyncError(`Node is in '${engine.syncState.phase}' phase and cannot ${activity}`);
|
|
@@ -1420,11 +1168,11 @@ async function query(engine, sql, params, options) {
|
|
|
1420
1168
|
await assertReadConcern(engine, options?.readConcern?.level);
|
|
1421
1169
|
return engine.database.query(sql, params, options);
|
|
1422
1170
|
}
|
|
1423
|
-
async function
|
|
1171
|
+
async function execute2(engine, sql, params, options) {
|
|
1424
1172
|
assertReadyPhase(engine, "accept operations");
|
|
1425
1173
|
if (!await canAcceptLocalWrite(engine)) {
|
|
1426
1174
|
if (engine.config.writeForwarding) {
|
|
1427
|
-
const result = await forwardStatements(engine, [{ sql, params }]);
|
|
1175
|
+
const result = await forwardStatements(engine, [{ sql, params }], options);
|
|
1428
1176
|
const first = result.results[0];
|
|
1429
1177
|
if (!first) {
|
|
1430
1178
|
return { changes: 0, lastInsertRowId: 0 };
|
|
@@ -1443,7 +1191,7 @@ async function executeBatch(engine, sql, paramsBatch, options) {
|
|
|
1443
1191
|
if (!await canAcceptLocalWrite(engine)) {
|
|
1444
1192
|
if (engine.config.writeForwarding) {
|
|
1445
1193
|
const statements = paramsBatch.map((p) => ({ sql, params: p }));
|
|
1446
|
-
const result = await forwardStatements(engine, statements);
|
|
1194
|
+
const result = await forwardStatements(engine, statements, options);
|
|
1447
1195
|
return result.results.map((r) => ({
|
|
1448
1196
|
changes: r.changes,
|
|
1449
1197
|
lastInsertRowId: typeof r.lastInsertRowId === "string" ? BigInt(r.lastInsertRowId) : Number(r.lastInsertRowId)
|
|
@@ -1451,12 +1199,7 @@ async function executeBatch(engine, sql, paramsBatch, options) {
|
|
|
1451
1199
|
}
|
|
1452
1200
|
throwNotCurrentPrimary(engine);
|
|
1453
1201
|
}
|
|
1454
|
-
|
|
1455
|
-
for (const params of paramsBatch) {
|
|
1456
|
-
const r = await engine.localExecutor.executeLocally(sql, params, options);
|
|
1457
|
-
results.push(r);
|
|
1458
|
-
}
|
|
1459
|
-
return results;
|
|
1202
|
+
return engine.localExecutor.executeBatchLocally(sql, paramsBatch, options);
|
|
1460
1203
|
}
|
|
1461
1204
|
async function transaction(engine, fn, options) {
|
|
1462
1205
|
assertReadyPhase(engine, "accept operations");
|
|
@@ -1465,9 +1208,10 @@ async function transaction(engine, fn, options) {
|
|
|
1465
1208
|
}
|
|
1466
1209
|
return engine.localExecutor.executeTransactionLocally(fn, options);
|
|
1467
1210
|
}
|
|
1468
|
-
async function forwardStatements(engine, statements,
|
|
1211
|
+
async function forwardStatements(engine, statements, options) {
|
|
1212
|
+
const writeConcern = options?.writeConcern;
|
|
1469
1213
|
if (await canAcceptLocalWrite(engine)) {
|
|
1470
|
-
return engine.localExecutor.executeForwardedLocally(statements);
|
|
1214
|
+
return engine.localExecutor.executeForwardedLocally(statements, writeConcern);
|
|
1471
1215
|
}
|
|
1472
1216
|
const primaryPeerId = getForwardingPrimaryPeerId(engine);
|
|
1473
1217
|
if (primaryPeerId === null) {
|
|
@@ -1476,7 +1220,8 @@ async function forwardStatements(engine, statements, _options) {
|
|
|
1476
1220
|
return engine.config.transport.forward(primaryPeerId, {
|
|
1477
1221
|
statements,
|
|
1478
1222
|
requestId: randomUUID(),
|
|
1479
|
-
...getCoordinatorMessageFields(engine)
|
|
1223
|
+
...getCoordinatorMessageFields(engine),
|
|
1224
|
+
...writeConcern === void 0 ? {} : { writeConcern }
|
|
1480
1225
|
});
|
|
1481
1226
|
}
|
|
1482
1227
|
|
|
@@ -1537,17 +1282,15 @@ var LocalExecutor = class {
|
|
|
1537
1282
|
this.engine = engine;
|
|
1538
1283
|
}
|
|
1539
1284
|
/**
|
|
1540
|
-
*
|
|
1285
|
+
* Makes each `executeTransactionLocally` call wait until the previous one finishes.
|
|
1541
1286
|
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1549
|
-
* the chain level so a failed transaction never poisons the queue; the
|
|
1550
|
-
* original error still surfaces to the caller that initiated it.
|
|
1287
|
+
* The engine writes through the single connection `engine.writerConn`,
|
|
1288
|
+
* which SQLite limits to one open transaction. Without this queue, two
|
|
1289
|
+
* concurrent `engine.transaction(fn)` calls would both send `BEGIN` on that
|
|
1290
|
+
* connection, so SQLite would reject the second with 'cannot start a
|
|
1291
|
+
* transaction within a transaction'. The queue catches each rejection so
|
|
1292
|
+
* that a later transaction still starts after an earlier one fails, while
|
|
1293
|
+
* the caller that started the failed transaction still receives its error.
|
|
1551
1294
|
*/
|
|
1552
1295
|
transactionQueue = Promise.resolve();
|
|
1553
1296
|
async executeLocally(sql, params, options) {
|
|
@@ -1558,11 +1301,9 @@ var LocalExecutor = class {
|
|
|
1558
1301
|
}
|
|
1559
1302
|
const txId = randomUUID();
|
|
1560
1303
|
const droppedTable = isDdl ? extractDroppedTable(sql) : null;
|
|
1561
|
-
const result = await engine.writerConn
|
|
1304
|
+
const result = await runWriterTransaction(engine.writerConn, async (tx) => {
|
|
1562
1305
|
const seqBefore = await engine.log.getLocalSeq();
|
|
1563
|
-
const
|
|
1564
|
-
const stmt = await tx.prepare(sql);
|
|
1565
|
-
const r = await stmt.run(...bindValues);
|
|
1306
|
+
const r = await execute(tx, sql, params);
|
|
1566
1307
|
if (isDdl) {
|
|
1567
1308
|
await insertDdlChange(tx, CHANGES_TABLE, {
|
|
1568
1309
|
ddlStatement: sql,
|
|
@@ -1592,12 +1333,13 @@ var LocalExecutor = class {
|
|
|
1592
1333
|
}
|
|
1593
1334
|
return result;
|
|
1594
1335
|
}
|
|
1595
|
-
async
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1336
|
+
async executeBatchLocally(sql, paramsBatch, options) {
|
|
1337
|
+
const { results, newSeq } = await this.executeInOneTransaction(paramsBatch.map((params) => ({ sql, params })));
|
|
1338
|
+
await this.waitForWriteConcernOf(newSeq, options?.writeConcern);
|
|
1339
|
+
return results;
|
|
1340
|
+
}
|
|
1341
|
+
async executeForwardedLocally(statements, statedWriteConcern) {
|
|
1342
|
+
const hook = this.engine.config.onBeforeForwardedQuery;
|
|
1601
1343
|
for (const { sql } of statements) {
|
|
1602
1344
|
if (!SAFE_SQL_PREFIX_RE.test(sql)) {
|
|
1603
1345
|
throw new ReplicationError("Forwarded statement rejected: only DML and safe DDL are allowed");
|
|
@@ -1608,22 +1350,37 @@ var LocalExecutor = class {
|
|
|
1608
1350
|
hook(sql, params);
|
|
1609
1351
|
}
|
|
1610
1352
|
}
|
|
1353
|
+
const { results, newSeq } = await this.executeInOneTransaction(statements);
|
|
1354
|
+
await this.waitForWriteConcernOf(newSeq, statedWriteConcern);
|
|
1355
|
+
return {
|
|
1356
|
+
results: results.map((r) => ({
|
|
1357
|
+
changes: r.changes,
|
|
1358
|
+
lastInsertRowId: typeof r.lastInsertRowId === "bigint" ? r.lastInsertRowId.toString() : r.lastInsertRowId
|
|
1359
|
+
})),
|
|
1360
|
+
requestId: randomUUID()
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
async waitForWriteConcernOf(seq, stated) {
|
|
1364
|
+
const writeConcern = resolveWriteConcern(this.engine, stated);
|
|
1365
|
+
if (writeConcern) {
|
|
1366
|
+
await waitForWriteConcern(this.engine, seq, writeConcern);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async executeInOneTransaction(statements) {
|
|
1370
|
+
const engine = this.engine;
|
|
1371
|
+
const results = [];
|
|
1372
|
+
const txId = randomUUID();
|
|
1611
1373
|
let sawDdl = false;
|
|
1612
1374
|
const droppedTables = [];
|
|
1613
|
-
await engine.writerConn
|
|
1375
|
+
await runWriterTransaction(engine.writerConn, async (tx) => {
|
|
1614
1376
|
const seqBefore = await engine.log.getLocalSeq();
|
|
1615
1377
|
for (const { sql, params } of statements) {
|
|
1616
1378
|
const isDdl = DDL_PREFIX_RE.test(sql);
|
|
1617
1379
|
if (isDdl && sql.includes(";")) {
|
|
1618
1380
|
throw new ReplicationError("DDL statements containing semicolons are not allowed for replication safety");
|
|
1619
1381
|
}
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
const r = await stmt.run(...bindValues);
|
|
1623
|
-
results.push({
|
|
1624
|
-
changes: r.changes,
|
|
1625
|
-
lastInsertRowId: typeof r.lastInsertRowId === "bigint" ? r.lastInsertRowId.toString() : r.lastInsertRowId
|
|
1626
|
-
});
|
|
1382
|
+
const r = await execute(tx, sql, params);
|
|
1383
|
+
results.push({ changes: r.changes, lastInsertRowId: r.lastInsertRowId });
|
|
1627
1384
|
if (isDdl) {
|
|
1628
1385
|
sawDdl = true;
|
|
1629
1386
|
const droppedTable = extractDroppedTable(sql);
|
|
@@ -1654,7 +1411,7 @@ var LocalExecutor = class {
|
|
|
1654
1411
|
}
|
|
1655
1412
|
await refreshTriggersAfterDdl(engine);
|
|
1656
1413
|
}
|
|
1657
|
-
return { results,
|
|
1414
|
+
return { results, newSeq };
|
|
1658
1415
|
}
|
|
1659
1416
|
async executeTransactionLocally(fn, options) {
|
|
1660
1417
|
const ticket = this.transactionQueue.then(
|
|
@@ -1674,7 +1431,7 @@ var LocalExecutor = class {
|
|
|
1674
1431
|
throw new ReplicationError("Internal error: DDL hook invoked outside an active transaction");
|
|
1675
1432
|
}
|
|
1676
1433
|
};
|
|
1677
|
-
const userResult = await engine.writerConn
|
|
1434
|
+
const userResult = await runWriterTransaction(engine.writerConn, async (tx) => {
|
|
1678
1435
|
const seqBefore = await engine.log.getLocalSeq();
|
|
1679
1436
|
hooks.onDdl = async (sql) => {
|
|
1680
1437
|
const droppedTable = extractDroppedTable(sql);
|
|
@@ -1705,167 +1462,485 @@ var LocalExecutor = class {
|
|
|
1705
1462
|
if (hooks.droppedTables.length > 0 && engine.tracker) {
|
|
1706
1463
|
await engine.tracker.pruneDroppedTables(engine.writerConn, hooks.droppedTables);
|
|
1707
1464
|
}
|
|
1708
|
-
await refreshTriggersAfterDdl(engine);
|
|
1709
|
-
}
|
|
1710
|
-
const writeConcern = resolveWriteConcern(engine, options?.writeConcern);
|
|
1711
|
-
if (writeConcern) {
|
|
1712
|
-
await waitForWriteConcern(engine, newSeq, writeConcern);
|
|
1465
|
+
await refreshTriggersAfterDdl(engine);
|
|
1466
|
+
}
|
|
1467
|
+
const writeConcern = resolveWriteConcern(engine, options?.writeConcern);
|
|
1468
|
+
if (writeConcern) {
|
|
1469
|
+
await waitForWriteConcern(engine, newSeq, writeConcern);
|
|
1470
|
+
}
|
|
1471
|
+
return userResult;
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
// src/replication/engine/node-health.ts
|
|
1476
|
+
function isSidelined(state, nodeId) {
|
|
1477
|
+
return state.drainingNodeIds.includes(nodeId) || state.repairingNodeIds.includes(nodeId) || state.faultedNodeIds.includes(nodeId);
|
|
1478
|
+
}
|
|
1479
|
+
function excludingReason(state, nodeId) {
|
|
1480
|
+
if (state.drainingNodeIds.includes(nodeId)) return "draining";
|
|
1481
|
+
if (state.faultedNodeIds.includes(nodeId)) return "faulted";
|
|
1482
|
+
return "sync-pending";
|
|
1483
|
+
}
|
|
1484
|
+
function staticHealth(engine, phase) {
|
|
1485
|
+
if (phase !== "ready") {
|
|
1486
|
+
return { state: "unavailable", reason: "sync-pending", canRead: false, canWrite: false };
|
|
1487
|
+
}
|
|
1488
|
+
return { state: "healthy", reason: "in-sync", canRead: true, canWrite: engine.config.topology.canWrite() };
|
|
1489
|
+
}
|
|
1490
|
+
function computeNodeHealth(engine) {
|
|
1491
|
+
const phase = engine.syncState.phase;
|
|
1492
|
+
if (phase === "syncing" || phase === "catching-up") {
|
|
1493
|
+
return { state: "syncing", reason: "sync-pending", canRead: false, canWrite: false };
|
|
1494
|
+
}
|
|
1495
|
+
if (!engine.isCoordinatorMode()) {
|
|
1496
|
+
return staticHealth(engine, phase);
|
|
1497
|
+
}
|
|
1498
|
+
const state = engine.coordinatorState;
|
|
1499
|
+
if (!state) {
|
|
1500
|
+
return { state: "unavailable", reason: "no-group-state", canRead: false, canWrite: false };
|
|
1501
|
+
}
|
|
1502
|
+
const nodeId = engine.nodeId;
|
|
1503
|
+
const canRead = phase === "ready" && !isSidelined(state, nodeId);
|
|
1504
|
+
const canWrite = canRead && engine.coordinatorAuthority;
|
|
1505
|
+
if (state.repairingNodeIds.includes(nodeId)) {
|
|
1506
|
+
return { state: "repairing", reason: "repairing", canRead, canWrite };
|
|
1507
|
+
}
|
|
1508
|
+
if (engine.coordinatorAuthority && !canWrite) {
|
|
1509
|
+
return { state: "failing_over", reason: excludingReason(state, nodeId), canRead, canWrite };
|
|
1510
|
+
}
|
|
1511
|
+
if (!canRead) {
|
|
1512
|
+
return { state: "unavailable", reason: excludingReason(state, nodeId), canRead, canWrite };
|
|
1513
|
+
}
|
|
1514
|
+
if (!isCoordinatorConnected(engine)) {
|
|
1515
|
+
return { state: "degraded", reason: "coordinator-unreachable", canRead, canWrite };
|
|
1516
|
+
}
|
|
1517
|
+
if (!state.inSyncNodeIds.includes(nodeId)) {
|
|
1518
|
+
return { state: "degraded", reason: "lagging", canRead, canWrite };
|
|
1519
|
+
}
|
|
1520
|
+
return { state: "healthy", reason: "in-sync", canRead, canWrite };
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/replication/engine/sender-loop.ts
|
|
1524
|
+
var SenderLoop = class {
|
|
1525
|
+
constructor(engine) {
|
|
1526
|
+
this.engine = engine;
|
|
1527
|
+
}
|
|
1528
|
+
senderTimer = null;
|
|
1529
|
+
start() {
|
|
1530
|
+
const engine = this.engine;
|
|
1531
|
+
if (!engine.running) return;
|
|
1532
|
+
const timer = setTimeout(async () => {
|
|
1533
|
+
if (!engine.running) return;
|
|
1534
|
+
try {
|
|
1535
|
+
engine.lastSentSeq = await engine.log.getLocalSeq();
|
|
1536
|
+
await this.sendPendingBatches();
|
|
1537
|
+
await this.updatePruneBoundary();
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1540
|
+
engine.emitError({ error: wrappedErr, operation: "sender-loop", recoverable: true });
|
|
1541
|
+
}
|
|
1542
|
+
this.start();
|
|
1543
|
+
}, engine.batchIntervalMs);
|
|
1544
|
+
timer.unref?.();
|
|
1545
|
+
this.senderTimer = timer;
|
|
1546
|
+
}
|
|
1547
|
+
stop() {
|
|
1548
|
+
if (this.senderTimer !== null) {
|
|
1549
|
+
clearTimeout(this.senderTimer);
|
|
1550
|
+
this.senderTimer = null;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async updatePruneBoundary() {
|
|
1554
|
+
const engine = this.engine;
|
|
1555
|
+
if (!engine.tracker) return;
|
|
1556
|
+
const minAcked = await engine.log.getMinAckedSeq();
|
|
1557
|
+
if (minAcked === null) {
|
|
1558
|
+
engine.tracker.clearPruneBoundary("replication");
|
|
1559
|
+
} else {
|
|
1560
|
+
engine.tracker.setPruneBoundary("replication", minAcked);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async sendPendingBatches() {
|
|
1564
|
+
const engine = this.engine;
|
|
1565
|
+
const peers = engine.config.transport.peers();
|
|
1566
|
+
const nowMs = Date.now();
|
|
1567
|
+
for (const [peerId, peerInfo] of peers) {
|
|
1568
|
+
if (!this.shouldReplicateTo(peerId, peerInfo.role)) {
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
const peerState = engine.peerTracker.getPeerState(peerId);
|
|
1572
|
+
if (peerState) {
|
|
1573
|
+
engine.peerTracker.expireTimedOutBatches(peerId, nowMs, engine.ackTimeoutMs);
|
|
1574
|
+
}
|
|
1575
|
+
if (peerState && peerState.pendingBatches >= engine.maxPendingBatches) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const fromSeq = peerState?.lastSentSeq ?? engine.lastSentSeq;
|
|
1579
|
+
const rawBatch = await engine.log.readBatch(fromSeq, engine.batchSize);
|
|
1580
|
+
if (!rawBatch) continue;
|
|
1581
|
+
const batch = engine.decorate(rawBatch);
|
|
1582
|
+
const previousSeq = peerState?.lastSentSeq ?? 0n;
|
|
1583
|
+
if (peerState) {
|
|
1584
|
+
peerState.pendingBatches += 1;
|
|
1585
|
+
peerState.lastSentSeq = batch.toSeq;
|
|
1586
|
+
engine.peerTracker.recordInFlightBatch(peerId, {
|
|
1587
|
+
batchId: batch.batchId,
|
|
1588
|
+
fromSeq: batch.fromSeq,
|
|
1589
|
+
toSeq: batch.toSeq,
|
|
1590
|
+
sentAt: nowMs
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
engine.config.transport.send(peerId, batch).catch((err) => {
|
|
1594
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1595
|
+
engine.emitError({ error: wrappedErr, operation: "transport-send", peerId, recoverable: true });
|
|
1596
|
+
if (peerState) {
|
|
1597
|
+
const idx = peerState.inFlightBatches.findIndex((b) => b.batchId === batch.batchId);
|
|
1598
|
+
if (idx >= 0) {
|
|
1599
|
+
peerState.inFlightBatches.splice(idx, 1);
|
|
1600
|
+
}
|
|
1601
|
+
if (peerState.pendingBatches > 0) {
|
|
1602
|
+
peerState.pendingBatches -= 1;
|
|
1603
|
+
}
|
|
1604
|
+
if (previousSeq < peerState.lastSentSeq) {
|
|
1605
|
+
peerState.lastSentSeq = previousSeq;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
if (engine.config.flowControl?.maxLagSeconds && peerState) {
|
|
1610
|
+
const lagMs = Number(batch.toSeq - peerState.lastAckedSeq) * engine.batchIntervalMs;
|
|
1611
|
+
const maxLagMs = engine.config.flowControl.maxLagSeconds * 1e3;
|
|
1612
|
+
if (lagMs > maxLagMs && engine.config.flowControl.onLagExceeded) {
|
|
1613
|
+
engine.config.flowControl.onLagExceeded(peerId, lagMs);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
shouldReplicateTo(peerId, peerRole) {
|
|
1619
|
+
const engine = this.engine;
|
|
1620
|
+
if (!engine.isCoordinatorMode()) {
|
|
1621
|
+
return engine.config.topology.shouldReplicateTo(peerId, peerRole);
|
|
1622
|
+
}
|
|
1623
|
+
return engine.coordinatorAuthority && peerId !== engine.nodeId;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
// src/replication/engine/timers.ts
|
|
1628
|
+
function unrefTimer(timer) {
|
|
1629
|
+
const unref = timer.unref;
|
|
1630
|
+
unref?.call(timer);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/replication/engine/controller-loop.ts
|
|
1634
|
+
var DEFAULT_CONTROLLER_LEASE_TTL_MS = 1e4;
|
|
1635
|
+
var DEFAULT_CONTROLLER_TICK_INTERVAL_MS = 1e3;
|
|
1636
|
+
async function startControllerLoop(engine) {
|
|
1637
|
+
const config = engine.config.coordinator;
|
|
1638
|
+
if (!config) return;
|
|
1639
|
+
const controllerConfig = typeof config.controller === "object" ? config.controller : {};
|
|
1640
|
+
const enabled = typeof config.controller === "boolean" ? config.controller : controllerConfig.enabled ?? true;
|
|
1641
|
+
if (!enabled) {
|
|
1642
|
+
engine.controllerState = "disabled";
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
engine.controllerState = "standby";
|
|
1646
|
+
const bid = {
|
|
1647
|
+
holderId: controllerConfig.holderId ?? engine.nodeId,
|
|
1648
|
+
ttlMs: controllerConfig.leaseTtlMs ?? DEFAULT_CONTROLLER_LEASE_TTL_MS
|
|
1649
|
+
};
|
|
1650
|
+
const coordinator = config.coordinator;
|
|
1651
|
+
if (coordinator.watchControllerLease) {
|
|
1652
|
+
engine.controllerLeaseWatchDisposer = await coordinator.watchControllerLease(config.clusterId, (lease) => {
|
|
1653
|
+
engine.observedControllerLease = lease;
|
|
1654
|
+
if (lease === null) {
|
|
1655
|
+
bidForControllerLease(engine, bid).catch((err) => reportControllerFailure(engine, err));
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
const timer = setInterval(() => {
|
|
1660
|
+
controllerTick(engine, bid).catch((err) => reportControllerFailure(engine, err));
|
|
1661
|
+
}, controllerConfig.tickIntervalMs ?? DEFAULT_CONTROLLER_TICK_INTERVAL_MS);
|
|
1662
|
+
unrefTimer(timer);
|
|
1663
|
+
engine.controllerTimer = timer;
|
|
1664
|
+
}
|
|
1665
|
+
async function stopControllerLeaseWatch(engine) {
|
|
1666
|
+
const dispose = engine.controllerLeaseWatchDisposer;
|
|
1667
|
+
engine.controllerLeaseWatchDisposer = null;
|
|
1668
|
+
engine.observedControllerLease = null;
|
|
1669
|
+
if (dispose) {
|
|
1670
|
+
await dispose();
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
async function controllerTick(engine, bid) {
|
|
1674
|
+
const config = engine.config.coordinator;
|
|
1675
|
+
if (!config) return;
|
|
1676
|
+
if (engine.controllerLeaseId) {
|
|
1677
|
+
const renewed = await config.coordinator.renewLease(engine.controllerLeaseId, bid.ttlMs);
|
|
1678
|
+
if (!renewed) {
|
|
1679
|
+
engine.controllerLeaseId = null;
|
|
1680
|
+
engine.controllerState = "lost";
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
noteCoordinatorContact(engine);
|
|
1684
|
+
engine.controllerState = "active";
|
|
1685
|
+
await runControllerPromotionCheck(engine);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (watchShowsLiveHolder(engine)) {
|
|
1689
|
+
engine.controllerState = "standby";
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
await bidForControllerLease(engine, bid);
|
|
1693
|
+
}
|
|
1694
|
+
function watchShowsLiveHolder(engine) {
|
|
1695
|
+
const observed = engine.observedControllerLease;
|
|
1696
|
+
return engine.controllerLeaseWatchDisposer !== null && observed !== null && observed.expiresAtMs > Date.now();
|
|
1697
|
+
}
|
|
1698
|
+
async function bidForControllerLease(engine, bid) {
|
|
1699
|
+
const config = engine.config.coordinator;
|
|
1700
|
+
if (!config || !engine.running || engine.controllerLeaseId || engine.controllerBidding) return;
|
|
1701
|
+
engine.controllerBidding = true;
|
|
1702
|
+
try {
|
|
1703
|
+
const acquired = await config.coordinator.tryAcquireControllerLease({
|
|
1704
|
+
clusterId: config.clusterId,
|
|
1705
|
+
holderId: bid.holderId,
|
|
1706
|
+
ttlMs: bid.ttlMs
|
|
1707
|
+
});
|
|
1708
|
+
noteCoordinatorContact(engine);
|
|
1709
|
+
if (!acquired.acquired) {
|
|
1710
|
+
engine.controllerState = "standby";
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (!engine.running) {
|
|
1714
|
+
await config.coordinator.releaseLease(acquired.lease.id);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
engine.controllerLeaseId = acquired.lease.id;
|
|
1718
|
+
engine.controllerState = "active";
|
|
1719
|
+
} finally {
|
|
1720
|
+
engine.controllerBidding = false;
|
|
1721
|
+
}
|
|
1722
|
+
await runControllerPromotionCheck(engine);
|
|
1723
|
+
}
|
|
1724
|
+
function reportControllerFailure(engine, err) {
|
|
1725
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1726
|
+
engine.controllerState = "lost";
|
|
1727
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-controller", recoverable: true });
|
|
1728
|
+
}
|
|
1729
|
+
async function runControllerPromotionCheck(engine) {
|
|
1730
|
+
const config = engine.config.coordinator;
|
|
1731
|
+
if (!config) return;
|
|
1732
|
+
const state = await refreshCoordinatorState(engine);
|
|
1733
|
+
if (!state) return;
|
|
1734
|
+
const primaryNodeId = state.currentPrimary?.nodeId;
|
|
1735
|
+
const primaryLive = primaryNodeId ? await config.coordinator.getLiveNodeSession(config.clusterId, primaryNodeId) : null;
|
|
1736
|
+
const primaryCanKeepDuty = primaryNodeId && primaryLive && compatibilityAllowsPromotion(state.compatibility, primaryLive.compatibility) && !state.drainingNodeIds.includes(primaryNodeId) && !state.repairingNodeIds.includes(primaryNodeId) && !state.faultedNodeIds.includes(primaryNodeId);
|
|
1737
|
+
if (primaryCanKeepDuty) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
const promoted = await config.coordinator.promoteEligibleReplica({
|
|
1742
|
+
clusterId: config.clusterId,
|
|
1743
|
+
groupId: config.groupId,
|
|
1744
|
+
excludeNodeIds: primaryNodeId ? [primaryNodeId] : []
|
|
1745
|
+
});
|
|
1746
|
+
engine.coordinatorState = promoted;
|
|
1747
|
+
engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, promoted);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1750
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-promotion", recoverable: true });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/replication/engine/coordinator-lifecycle.ts
|
|
1755
|
+
var IN_SYNC_RECONCILE_INTERVAL_MS = 1e3;
|
|
1756
|
+
function localCoordinatorPrimary(engine) {
|
|
1757
|
+
const endpoint = engine.config.coordinator?.endpoint;
|
|
1758
|
+
return endpoint ? { nodeId: engine.nodeId, endpoint } : { nodeId: engine.nodeId };
|
|
1759
|
+
}
|
|
1760
|
+
async function startCoordinatorMode(engine) {
|
|
1761
|
+
const config = engine.config.coordinator;
|
|
1762
|
+
if (!config) return;
|
|
1763
|
+
const coordinator = config.coordinator;
|
|
1764
|
+
let state = await coordinator.getReplicationGroupState(config.clusterId, config.groupId);
|
|
1765
|
+
if (!state && config.votingDataBearingNodeIds) {
|
|
1766
|
+
state = await coordinator.setReplicationGroupState({
|
|
1767
|
+
clusterId: config.clusterId,
|
|
1768
|
+
groupId: config.groupId,
|
|
1769
|
+
votingDataBearingNodeIds: config.votingDataBearingNodeIds,
|
|
1770
|
+
currentPrimary: engine.config.topology.role === "primary" ? localCoordinatorPrimary(engine) : null,
|
|
1771
|
+
primaryTerm: 1n,
|
|
1772
|
+
inSyncNodeIds: [engine.nodeId],
|
|
1773
|
+
compatibility: config.compatibility
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
if (!state) {
|
|
1777
|
+
throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
|
|
1778
|
+
}
|
|
1779
|
+
noteCoordinatorContact(engine);
|
|
1780
|
+
engine.coordinatorState = state;
|
|
1781
|
+
engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, state);
|
|
1782
|
+
const session = await coordinator.registerNodeSession({
|
|
1783
|
+
clusterId: config.clusterId,
|
|
1784
|
+
nodeId: engine.nodeId,
|
|
1785
|
+
ttlMs: config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS,
|
|
1786
|
+
endpoint: config.endpoint,
|
|
1787
|
+
groupIds: [config.groupId],
|
|
1788
|
+
dataBearing: true,
|
|
1789
|
+
voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
|
|
1790
|
+
compatibility: config.compatibility
|
|
1791
|
+
});
|
|
1792
|
+
engine.nodeSessionLeaseId = session.lease.id;
|
|
1793
|
+
noteCoordinatorContact(engine);
|
|
1794
|
+
engine.coordinatorWatchDisposer = await coordinator.watchReplicationGroup(config.clusterId, config.groupId, (next) => {
|
|
1795
|
+
handleCoordinatorStateUpdate(engine, next);
|
|
1796
|
+
});
|
|
1797
|
+
if (coordinator.watchNodeSessions) {
|
|
1798
|
+
engine.nodeSessionWatchDisposer = await coordinator.watchNodeSessions(config.clusterId, (liveNodeIds) => {
|
|
1799
|
+
engine.liveNodeIds = [...liveNodeIds];
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
startCoordinatorLeaseRenewal(engine);
|
|
1803
|
+
startInSyncReconcileLoop(engine);
|
|
1804
|
+
await startControllerLoop(engine);
|
|
1805
|
+
}
|
|
1806
|
+
function startInSyncReconcileLoop(engine) {
|
|
1807
|
+
const timer = setInterval(() => {
|
|
1808
|
+
reconcileInSyncSet(engine).catch((err) => {
|
|
1809
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1810
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-in-sync-reconcile", recoverable: true });
|
|
1811
|
+
});
|
|
1812
|
+
}, IN_SYNC_RECONCILE_INTERVAL_MS);
|
|
1813
|
+
unrefTimer(timer);
|
|
1814
|
+
engine.inSyncReconcileTimer = timer;
|
|
1815
|
+
}
|
|
1816
|
+
function handleCoordinatorStateUpdate(engine, next) {
|
|
1817
|
+
const previous = engine.coordinatorState;
|
|
1818
|
+
const wasPrimary = previous ? hasCurrentPrimaryAuthorityFor(engine, previous) : engine.coordinatorAuthority;
|
|
1819
|
+
noteCoordinatorContact(engine);
|
|
1820
|
+
engine.coordinatorState = next;
|
|
1821
|
+
engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, next);
|
|
1822
|
+
if (wasPrimary && !engine.coordinatorAuthority && next.primaryTerm > (previous?.primaryTerm ?? 0n)) {
|
|
1823
|
+
handleFormerPrimaryDemotion(engine, next).catch((err) => {
|
|
1824
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1825
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-former-primary-demotion", recoverable: false });
|
|
1826
|
+
});
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
startCoordinatorRejoinSyncIfReady(engine, next);
|
|
1830
|
+
}
|
|
1831
|
+
function startCoordinatorLeaseRenewal(engine) {
|
|
1832
|
+
const config = engine.config.coordinator;
|
|
1833
|
+
if (!config || !engine.nodeSessionLeaseId) return;
|
|
1834
|
+
const ttlMs = config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS;
|
|
1835
|
+
const timer = setInterval(
|
|
1836
|
+
() => {
|
|
1837
|
+
void keepCoordinatorSessionAlive(engine, ttlMs);
|
|
1838
|
+
},
|
|
1839
|
+
Math.max(1e3, Math.floor(ttlMs / 3))
|
|
1840
|
+
);
|
|
1841
|
+
unrefTimer(timer);
|
|
1842
|
+
engine.coordinatorLeaseTimer = timer;
|
|
1843
|
+
}
|
|
1844
|
+
function applyAuthorityFromKnownState(engine) {
|
|
1845
|
+
const state = engine.coordinatorState;
|
|
1846
|
+
engine.coordinatorAuthority = state ? hasCurrentPrimaryAuthorityFor(engine, state) : false;
|
|
1847
|
+
}
|
|
1848
|
+
async function keepCoordinatorSessionAlive(engine, ttlMs) {
|
|
1849
|
+
const config = engine.config.coordinator;
|
|
1850
|
+
if (!config || !engine.running || engine.coordinatorSessionRestoring) return;
|
|
1851
|
+
const leaseId = engine.nodeSessionLeaseId;
|
|
1852
|
+
if (leaseId) {
|
|
1853
|
+
try {
|
|
1854
|
+
if (await config.coordinator.renewLease(leaseId, ttlMs)) {
|
|
1855
|
+
noteCoordinatorContact(engine);
|
|
1856
|
+
applyAuthorityFromKnownState(engine);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
} catch (err) {
|
|
1860
|
+
engine.coordinatorAuthority = false;
|
|
1861
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1862
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-session-renew", recoverable: true });
|
|
1863
|
+
return;
|
|
1713
1864
|
}
|
|
1714
|
-
return userResult;
|
|
1715
1865
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
function isSidelined(state, nodeId) {
|
|
1720
|
-
return state.drainingNodeIds.includes(nodeId) || state.repairingNodeIds.includes(nodeId) || state.faultedNodeIds.includes(nodeId);
|
|
1721
|
-
}
|
|
1722
|
-
function excludingReason(state, nodeId) {
|
|
1723
|
-
if (state.drainingNodeIds.includes(nodeId)) return "draining";
|
|
1724
|
-
if (state.faultedNodeIds.includes(nodeId)) return "faulted";
|
|
1725
|
-
return "sync-pending";
|
|
1866
|
+
engine.coordinatorAuthority = false;
|
|
1867
|
+
engine.nodeSessionLeaseId = null;
|
|
1868
|
+
await restoreCoordinatorSession(engine, ttlMs);
|
|
1726
1869
|
}
|
|
1727
|
-
function
|
|
1728
|
-
|
|
1729
|
-
|
|
1870
|
+
async function restoreCoordinatorSession(engine, ttlMs) {
|
|
1871
|
+
const config = engine.config.coordinator;
|
|
1872
|
+
if (!config) return;
|
|
1873
|
+
engine.coordinatorSessionRestoring = true;
|
|
1874
|
+
try {
|
|
1875
|
+
const state = await config.coordinator.getReplicationGroupState(config.clusterId, config.groupId);
|
|
1876
|
+
if (!state) {
|
|
1877
|
+
throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
|
|
1878
|
+
}
|
|
1879
|
+
const session = await config.coordinator.registerNodeSession({
|
|
1880
|
+
clusterId: config.clusterId,
|
|
1881
|
+
nodeId: engine.nodeId,
|
|
1882
|
+
ttlMs,
|
|
1883
|
+
endpoint: config.endpoint,
|
|
1884
|
+
groupIds: [config.groupId],
|
|
1885
|
+
dataBearing: true,
|
|
1886
|
+
voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
|
|
1887
|
+
compatibility: config.compatibility
|
|
1888
|
+
});
|
|
1889
|
+
if (!engine.running) {
|
|
1890
|
+
await config.coordinator.releaseLease(session.lease.id).catch(() => void 0);
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
engine.nodeSessionLeaseId = session.lease.id;
|
|
1894
|
+
engine.coordinatorState = state;
|
|
1895
|
+
noteCoordinatorContact(engine);
|
|
1896
|
+
applyAuthorityFromKnownState(engine);
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1899
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-session-restore", recoverable: true });
|
|
1900
|
+
} finally {
|
|
1901
|
+
engine.coordinatorSessionRestoring = false;
|
|
1730
1902
|
}
|
|
1731
|
-
return { state: "healthy", reason: "in-sync", canRead: true, canWrite: engine.config.topology.canWrite() };
|
|
1732
1903
|
}
|
|
1733
|
-
function
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1738
|
-
if (!engine.isCoordinatorMode()) {
|
|
1739
|
-
return staticHealth(engine, phase);
|
|
1740
|
-
}
|
|
1741
|
-
const state = engine.coordinatorState;
|
|
1742
|
-
if (!state) {
|
|
1743
|
-
return { state: "unavailable", reason: "no-group-state", canRead: false, canWrite: false };
|
|
1744
|
-
}
|
|
1745
|
-
const nodeId = engine.nodeId;
|
|
1746
|
-
const canRead = phase === "ready" && !isSidelined(state, nodeId);
|
|
1747
|
-
const canWrite = canRead && engine.coordinatorAuthority;
|
|
1748
|
-
if (state.repairingNodeIds.includes(nodeId)) {
|
|
1749
|
-
return { state: "repairing", reason: "repairing", canRead, canWrite };
|
|
1750
|
-
}
|
|
1751
|
-
if (engine.coordinatorAuthority && !canWrite) {
|
|
1752
|
-
return { state: "failing_over", reason: excludingReason(state, nodeId), canRead, canWrite };
|
|
1753
|
-
}
|
|
1754
|
-
if (!canRead) {
|
|
1755
|
-
return { state: "unavailable", reason: excludingReason(state, nodeId), canRead, canWrite };
|
|
1904
|
+
function stopCoordinatorTimers(engine) {
|
|
1905
|
+
if (engine.coordinatorLeaseTimer) {
|
|
1906
|
+
clearInterval(engine.coordinatorLeaseTimer);
|
|
1907
|
+
engine.coordinatorLeaseTimer = null;
|
|
1756
1908
|
}
|
|
1757
|
-
if (
|
|
1758
|
-
|
|
1909
|
+
if (engine.inSyncReconcileTimer) {
|
|
1910
|
+
clearInterval(engine.inSyncReconcileTimer);
|
|
1911
|
+
engine.inSyncReconcileTimer = null;
|
|
1759
1912
|
}
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1913
|
+
if (engine.controllerTimer) {
|
|
1914
|
+
clearInterval(engine.controllerTimer);
|
|
1915
|
+
engine.controllerTimer = null;
|
|
1762
1916
|
}
|
|
1763
|
-
return { state: "healthy", reason: "in-sync", canRead, canWrite };
|
|
1764
1917
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
senderTimer = null;
|
|
1772
|
-
start() {
|
|
1773
|
-
const engine = this.engine;
|
|
1774
|
-
if (!engine.running) return;
|
|
1775
|
-
const timer = setTimeout(async () => {
|
|
1776
|
-
if (!engine.running) return;
|
|
1777
|
-
try {
|
|
1778
|
-
engine.lastSentSeq = await engine.log.getLocalSeq();
|
|
1779
|
-
await this.sendPendingBatches();
|
|
1780
|
-
await this.updatePruneBoundary();
|
|
1781
|
-
} catch (err) {
|
|
1782
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1783
|
-
engine.emitError({ error: wrappedErr, operation: "sender-loop", recoverable: true });
|
|
1784
|
-
}
|
|
1785
|
-
this.start();
|
|
1786
|
-
}, engine.batchIntervalMs);
|
|
1787
|
-
timer.unref?.();
|
|
1788
|
-
this.senderTimer = timer;
|
|
1789
|
-
}
|
|
1790
|
-
stop() {
|
|
1791
|
-
if (this.senderTimer !== null) {
|
|
1792
|
-
clearTimeout(this.senderTimer);
|
|
1793
|
-
this.senderTimer = null;
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
async updatePruneBoundary() {
|
|
1797
|
-
const engine = this.engine;
|
|
1798
|
-
if (!engine.tracker) return;
|
|
1799
|
-
const minAcked = await engine.log.getMinAckedSeq();
|
|
1800
|
-
if (minAcked === null) {
|
|
1801
|
-
engine.tracker.clearPruneBoundary();
|
|
1802
|
-
} else {
|
|
1803
|
-
engine.tracker.setPruneBoundary(minAcked);
|
|
1804
|
-
}
|
|
1918
|
+
async function stopCoordinatorMode(engine) {
|
|
1919
|
+
const config = engine.config.coordinator;
|
|
1920
|
+
if (!config) return;
|
|
1921
|
+
if (engine.coordinatorWatchDisposer) {
|
|
1922
|
+
await engine.coordinatorWatchDisposer();
|
|
1923
|
+
engine.coordinatorWatchDisposer = null;
|
|
1805
1924
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
const nowMs = Date.now();
|
|
1810
|
-
for (const [peerId, peerInfo] of peers) {
|
|
1811
|
-
if (!this.shouldReplicateTo(peerId, peerInfo.role)) {
|
|
1812
|
-
continue;
|
|
1813
|
-
}
|
|
1814
|
-
const peerState = engine.peerTracker.getPeerState(peerId);
|
|
1815
|
-
if (peerState) {
|
|
1816
|
-
engine.peerTracker.expireTimedOutBatches(peerId, nowMs, engine.ackTimeoutMs);
|
|
1817
|
-
}
|
|
1818
|
-
if (peerState && peerState.pendingBatches >= engine.maxPendingBatches) {
|
|
1819
|
-
continue;
|
|
1820
|
-
}
|
|
1821
|
-
const fromSeq = peerState?.lastSentSeq ?? engine.lastSentSeq;
|
|
1822
|
-
const rawBatch = await engine.log.readBatch(fromSeq, engine.batchSize);
|
|
1823
|
-
if (!rawBatch) continue;
|
|
1824
|
-
const batch = engine.decorate(rawBatch);
|
|
1825
|
-
const previousSeq = peerState?.lastSentSeq ?? 0n;
|
|
1826
|
-
if (peerState) {
|
|
1827
|
-
peerState.pendingBatches += 1;
|
|
1828
|
-
peerState.lastSentSeq = batch.toSeq;
|
|
1829
|
-
engine.peerTracker.recordInFlightBatch(peerId, {
|
|
1830
|
-
batchId: batch.batchId,
|
|
1831
|
-
fromSeq: batch.fromSeq,
|
|
1832
|
-
toSeq: batch.toSeq,
|
|
1833
|
-
sentAt: nowMs
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
engine.config.transport.send(peerId, batch).catch((err) => {
|
|
1837
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1838
|
-
engine.emitError({ error: wrappedErr, operation: "transport-send", peerId, recoverable: true });
|
|
1839
|
-
if (peerState) {
|
|
1840
|
-
const idx = peerState.inFlightBatches.findIndex((b) => b.batchId === batch.batchId);
|
|
1841
|
-
if (idx >= 0) {
|
|
1842
|
-
peerState.inFlightBatches.splice(idx, 1);
|
|
1843
|
-
}
|
|
1844
|
-
if (peerState.pendingBatches > 0) {
|
|
1845
|
-
peerState.pendingBatches -= 1;
|
|
1846
|
-
}
|
|
1847
|
-
if (previousSeq < peerState.lastSentSeq) {
|
|
1848
|
-
peerState.lastSentSeq = previousSeq;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
});
|
|
1852
|
-
if (engine.config.flowControl?.maxLagSeconds && peerState) {
|
|
1853
|
-
const lagMs = Number(batch.toSeq - peerState.lastAckedSeq) * engine.batchIntervalMs;
|
|
1854
|
-
const maxLagMs = engine.config.flowControl.maxLagSeconds * 1e3;
|
|
1855
|
-
if (lagMs > maxLagMs && engine.config.flowControl.onLagExceeded) {
|
|
1856
|
-
engine.config.flowControl.onLagExceeded(peerId, lagMs);
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1925
|
+
if (engine.nodeSessionWatchDisposer) {
|
|
1926
|
+
await engine.nodeSessionWatchDisposer();
|
|
1927
|
+
engine.nodeSessionWatchDisposer = null;
|
|
1860
1928
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1929
|
+
engine.liveNodeIds = null;
|
|
1930
|
+
await stopControllerLeaseWatch(engine);
|
|
1931
|
+
if (engine.controllerLeaseId) {
|
|
1932
|
+
const leaseId = engine.controllerLeaseId;
|
|
1933
|
+
engine.controllerLeaseId = null;
|
|
1934
|
+
await config.coordinator.releaseLease(leaseId).catch((err) => {
|
|
1935
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1936
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-controller-release", recoverable: true });
|
|
1937
|
+
});
|
|
1867
1938
|
}
|
|
1868
|
-
|
|
1939
|
+
await config.coordinator.deregisterNodeSession(config.clusterId, engine.nodeId).catch((err) => {
|
|
1940
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
1941
|
+
engine.emitError({ error: wrappedErr, operation: "coordinator-session-deregister", recoverable: true });
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1869
1944
|
|
|
1870
1945
|
// src/replication/engine/test-hooks.ts
|
|
1871
1946
|
var ackDelayByEngine = /* @__PURE__ */ new WeakMap();
|
|
@@ -1958,6 +2033,7 @@ function wireTransportHandlers(engine) {
|
|
|
1958
2033
|
await refreshTriggersAfterDdl(engine);
|
|
1959
2034
|
}
|
|
1960
2035
|
await engine.log.setLastAppliedSeq(fromPeerId, batch.toSeq);
|
|
2036
|
+
engine.peerTracker.onBatchApplied(fromPeerId, batch.hlcRange.max);
|
|
1961
2037
|
const previousApplied = engine.appliedSeqByPeer.get(fromPeerId) ?? 0n;
|
|
1962
2038
|
if (batch.toSeq > previousApplied) {
|
|
1963
2039
|
engine.appliedSeqByPeer.set(fromPeerId, batch.toSeq);
|
|
@@ -2018,7 +2094,7 @@ function wireTransportHandlers(engine) {
|
|
|
2018
2094
|
throw new ReplicationError(`Rejected forward from unknown peer: ${fromPeerId}`);
|
|
2019
2095
|
}
|
|
2020
2096
|
try {
|
|
2021
|
-
const result = await engine.localExecutor.executeForwardedLocally(request.statements);
|
|
2097
|
+
const result = await engine.localExecutor.executeForwardedLocally(request.statements, request.writeConcern);
|
|
2022
2098
|
return engine.decorate(result);
|
|
2023
2099
|
} catch (err) {
|
|
2024
2100
|
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
@@ -2205,6 +2281,27 @@ async function startEngine(engine) {
|
|
|
2205
2281
|
}
|
|
2206
2282
|
engine.senderLoop.start();
|
|
2207
2283
|
}
|
|
2284
|
+
async function stopEngine(engine) {
|
|
2285
|
+
if (!engine.running) return;
|
|
2286
|
+
engine.running = false;
|
|
2287
|
+
stopCoordinatorTimers(engine);
|
|
2288
|
+
engine.syncJoiner.stopTimers();
|
|
2289
|
+
engine.syncServer.abortAll();
|
|
2290
|
+
if (engine.syncState.phase === "syncing") {
|
|
2291
|
+
try {
|
|
2292
|
+
await setForeignKeysEnabled(engine.writerConn, true);
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
2295
|
+
engine.emitError({ error: wrappedErr, operation: "engine-stop-pragma-restore", recoverable: false });
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
engine.senderLoop.stop();
|
|
2299
|
+
if (engine.tracker) {
|
|
2300
|
+
engine.tracker.clearPruneBoundary("replication");
|
|
2301
|
+
}
|
|
2302
|
+
await stopCoordinatorMode(engine);
|
|
2303
|
+
await engine.config.transport.disconnect();
|
|
2304
|
+
}
|
|
2208
2305
|
|
|
2209
2306
|
// src/replication/engine/sync-recovery.ts
|
|
2210
2307
|
var SYNC_RETRY_MIN_DELAY_MS = 1e3;
|
|
@@ -2984,7 +3081,7 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
2984
3081
|
/** @internal */
|
|
2985
3082
|
config;
|
|
2986
3083
|
/**
|
|
2987
|
-
*
|
|
3084
|
+
* Identifies this node, and the engine stamps it on every change that this node writes.
|
|
2988
3085
|
*/
|
|
2989
3086
|
nodeId;
|
|
2990
3087
|
/** @internal */
|
|
@@ -3042,6 +3139,16 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
3042
3139
|
/** @internal */
|
|
3043
3140
|
coordinatorWatchDisposer = null;
|
|
3044
3141
|
/** @internal */
|
|
3142
|
+
nodeSessionWatchDisposer = null;
|
|
3143
|
+
/** @internal */
|
|
3144
|
+
controllerLeaseWatchDisposer = null;
|
|
3145
|
+
/** @internal */
|
|
3146
|
+
observedControllerLease = null;
|
|
3147
|
+
/** @internal */
|
|
3148
|
+
controllerBidding = false;
|
|
3149
|
+
/** @internal */
|
|
3150
|
+
liveNodeIds = null;
|
|
3151
|
+
/** @internal */
|
|
3045
3152
|
coordinatorLeaseTimer = null;
|
|
3046
3153
|
/** @internal */
|
|
3047
3154
|
controllerTimer = null;
|
|
@@ -3112,39 +3219,28 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
3112
3219
|
installTestHooks(this);
|
|
3113
3220
|
}
|
|
3114
3221
|
/**
|
|
3115
|
-
* Connects the transport
|
|
3222
|
+
* Connects the transport and starts replicating. When this node is a replica without a complete, current copy, it
|
|
3223
|
+
* also requests a full copy from a source peer, and the returned promise resolves before that copy finishes.
|
|
3116
3224
|
*/
|
|
3117
|
-
start() {
|
|
3118
|
-
|
|
3225
|
+
async start() {
|
|
3226
|
+
try {
|
|
3227
|
+
await startEngine(this);
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
await this.stop().catch(() => {
|
|
3230
|
+
});
|
|
3231
|
+
throw err;
|
|
3232
|
+
}
|
|
3119
3233
|
}
|
|
3120
3234
|
/**
|
|
3121
|
-
* Stops replicating,
|
|
3235
|
+
* Stops replicating, aborts every first sync that this node serves, and disconnects the transport.
|
|
3122
3236
|
*/
|
|
3123
3237
|
async stop() {
|
|
3124
|
-
|
|
3125
|
-
this.running = false;
|
|
3126
|
-
stopCoordinatorTimers(this);
|
|
3127
|
-
this.syncJoiner.stopTimers();
|
|
3128
|
-
this.syncServer.abortAll();
|
|
3129
|
-
if (this.syncState.phase === "syncing") {
|
|
3130
|
-
try {
|
|
3131
|
-
await setForeignKeysEnabled(this.writerConn, true);
|
|
3132
|
-
} catch (err) {
|
|
3133
|
-
const wrappedErr = err instanceof Error ? err : new Error(String(err));
|
|
3134
|
-
this.emitError({ error: wrappedErr, operation: "engine-stop-pragma-restore", recoverable: false });
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
this.senderLoop.stop();
|
|
3138
|
-
if (this.tracker) {
|
|
3139
|
-
this.tracker.clearPruneBoundary();
|
|
3140
|
-
}
|
|
3141
|
-
await stopCoordinatorMode(this);
|
|
3142
|
-
await this.config.transport.disconnect();
|
|
3238
|
+
await stopEngine(this);
|
|
3143
3239
|
}
|
|
3144
3240
|
/**
|
|
3145
|
-
*
|
|
3241
|
+
* Returns this node's replication status.
|
|
3146
3242
|
*
|
|
3147
|
-
* @returns The node's role,
|
|
3243
|
+
* @returns The node's role, peers, sequence position, sync state, health, and coordinator group state.
|
|
3148
3244
|
*/
|
|
3149
3245
|
status() {
|
|
3150
3246
|
return {
|
|
@@ -3159,74 +3255,78 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
3159
3255
|
};
|
|
3160
3256
|
}
|
|
3161
3257
|
/**
|
|
3162
|
-
* Returns the highest
|
|
3258
|
+
* Returns the highest position in this node's local change log.
|
|
3163
3259
|
*
|
|
3164
|
-
* @returns That position, which a caller
|
|
3260
|
+
* @returns That position, which a caller can wait for a replica to reach.
|
|
3165
3261
|
*/
|
|
3166
3262
|
getCurrentSeq() {
|
|
3167
3263
|
return this.lastLocalSeq;
|
|
3168
3264
|
}
|
|
3169
3265
|
/**
|
|
3170
|
-
* Returns
|
|
3266
|
+
* Returns the highest applied position from one peer's change log.
|
|
3171
3267
|
*
|
|
3172
|
-
* @param peerId -
|
|
3173
|
-
* @returns
|
|
3268
|
+
* @param peerId - The peer's node ID.
|
|
3269
|
+
* @returns That position, or `0n` before this node applies any change from that peer.
|
|
3174
3270
|
*/
|
|
3175
3271
|
getAppliedSeq(peerId) {
|
|
3176
3272
|
return this.appliedSeqByPeer.get(peerId) ?? 0n;
|
|
3177
3273
|
}
|
|
3178
3274
|
/**
|
|
3179
|
-
*
|
|
3275
|
+
* Executes a read, and throws when this node is still syncing or cannot meet the read concern.
|
|
3180
3276
|
*
|
|
3181
|
-
* @param sql - The statement to
|
|
3182
|
-
* @param params -
|
|
3183
|
-
* @param options -
|
|
3184
|
-
* @returns The rows the statement
|
|
3277
|
+
* @param sql - The statement to execute.
|
|
3278
|
+
* @param params - The values to bind to the statement, named or positional.
|
|
3279
|
+
* @param options - The read concern for this statement.
|
|
3280
|
+
* @returns The rows that the statement returns.
|
|
3185
3281
|
*/
|
|
3186
3282
|
query(sql, params, options) {
|
|
3187
3283
|
return query(this, sql, params, options);
|
|
3188
3284
|
}
|
|
3189
3285
|
/**
|
|
3190
|
-
*
|
|
3286
|
+
* Executes one write on this node, or forwards it to the primary when this node cannot accept writes and
|
|
3287
|
+
* `writeForwarding` is on.
|
|
3191
3288
|
*
|
|
3192
|
-
* @param sql - The statement to
|
|
3193
|
-
* @param params -
|
|
3194
|
-
* @param options -
|
|
3195
|
-
* @returns
|
|
3289
|
+
* @param sql - The statement to execute.
|
|
3290
|
+
* @param params - The values to bind to the statement, named or positional.
|
|
3291
|
+
* @param options - The write concern for this statement, which the primary also applies to a forwarded write before it answers.
|
|
3292
|
+
* @returns The number of rows that changed and the ID of the last inserted row.
|
|
3196
3293
|
*/
|
|
3197
3294
|
execute(sql, params, options) {
|
|
3198
|
-
return
|
|
3295
|
+
return execute2(this, sql, params, options);
|
|
3199
3296
|
}
|
|
3200
3297
|
/**
|
|
3201
|
-
*
|
|
3298
|
+
* Executes one statement once for each parameter set and commits every set in one transaction, on this node or, when
|
|
3299
|
+
* this node forwards the batch, on the primary.
|
|
3202
3300
|
*
|
|
3203
|
-
* @param sql - The statement to
|
|
3204
|
-
* @param paramsBatch - One parameter set per
|
|
3205
|
-
* @param options -
|
|
3301
|
+
* @param sql - The statement to execute for each parameter set.
|
|
3302
|
+
* @param paramsBatch - One parameter set per execution.
|
|
3303
|
+
* @param options - The write concern for the batch, which the primary also applies to a forwarded batch before it answers.
|
|
3206
3304
|
* @returns One result per parameter set, in order.
|
|
3207
3305
|
*/
|
|
3208
3306
|
executeBatch(sql, paramsBatch, options) {
|
|
3209
3307
|
return executeBatch(this, sql, paramsBatch, options);
|
|
3210
3308
|
}
|
|
3211
3309
|
/**
|
|
3212
|
-
*
|
|
3310
|
+
* Calls `fn` inside one transaction on this node. On a node that cannot accept writes, it throws a `TopologyError`,
|
|
3311
|
+
* since the engine forwards only `execute` and `executeBatch` writes to the primary.
|
|
3213
3312
|
*
|
|
3214
|
-
* @param fn -
|
|
3215
|
-
* @param options -
|
|
3216
|
-
* @returns
|
|
3313
|
+
* @param fn - The function that receives the transaction and executes statements on it.
|
|
3314
|
+
* @param options - The write concern to wait for after the transaction commits.
|
|
3315
|
+
* @returns The value that `fn` resolves to.
|
|
3217
3316
|
*/
|
|
3218
3317
|
transaction(fn, options) {
|
|
3219
3318
|
return transaction(this, fn, options);
|
|
3220
3319
|
}
|
|
3221
3320
|
/**
|
|
3222
|
-
* Sends
|
|
3321
|
+
* Sends the statements to the primary, which executes them in one transaction, and returns the primary's result.
|
|
3322
|
+
* When this node is the primary, it executes them here in the same way.
|
|
3223
3323
|
*
|
|
3224
|
-
* @param statements - The statements to
|
|
3225
|
-
* @param options -
|
|
3226
|
-
* @returns
|
|
3324
|
+
* @param statements - The statements to execute, in order, each with its own parameters.
|
|
3325
|
+
* @param options - The write concern that the primary applies to the statements before it answers.
|
|
3326
|
+
* @returns The result of each statement, in order, and the request ID.
|
|
3227
3327
|
*/
|
|
3228
3328
|
forwardStatements(statements, options) {
|
|
3229
|
-
return forwardStatements(this, statements);
|
|
3329
|
+
return forwardStatements(this, statements, options);
|
|
3230
3330
|
}
|
|
3231
3331
|
/** @internal */
|
|
3232
3332
|
emitError(event) {
|
|
@@ -3254,7 +3354,7 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
3254
3354
|
return getForwardingPrimaryPeerId(this);
|
|
3255
3355
|
}
|
|
3256
3356
|
/**
|
|
3257
|
-
*
|
|
3357
|
+
* Returns a copy of an outgoing replication message with this node's group ID and primary term added.
|
|
3258
3358
|
*
|
|
3259
3359
|
* @internal
|
|
3260
3360
|
*/
|
|
@@ -3266,7 +3366,7 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
3266
3366
|
// src/replication/topology/primary-replica.ts
|
|
3267
3367
|
var PrimaryReplicaTopology = class {
|
|
3268
3368
|
/**
|
|
3269
|
-
*
|
|
3369
|
+
* Records whether this node is the primary, which accepts writes, or a replica, which serves reads.
|
|
3270
3370
|
*/
|
|
3271
3371
|
role;
|
|
3272
3372
|
constructor(role) {
|
|
@@ -3291,7 +3391,7 @@ var PrimaryReplicaTopology = class {
|
|
|
3291
3391
|
return this.role === "primary" && peerRole === "replica";
|
|
3292
3392
|
}
|
|
3293
3393
|
/**
|
|
3294
|
-
* Reports whether this node applies changes
|
|
3394
|
+
* Reports whether this node applies the changes that a given peer sends.
|
|
3295
3395
|
*
|
|
3296
3396
|
* @param _peerId - Identifier of the peer, which this topology ignores.
|
|
3297
3397
|
* @param peerRole - Role of the peer.
|
|
@@ -3301,7 +3401,8 @@ var PrimaryReplicaTopology = class {
|
|
|
3301
3401
|
return this.role === "replica" && peerRole === "primary";
|
|
3302
3402
|
}
|
|
3303
3403
|
/**
|
|
3304
|
-
* Reports whether
|
|
3404
|
+
* Reports whether the engine has to resolve conflicts for this topology. Only the primary writes, so the answer is
|
|
3405
|
+
* always false.
|
|
3305
3406
|
*
|
|
3306
3407
|
* @returns False.
|
|
3307
3408
|
*/
|