@peerbit/shared-log 13.2.33 → 13.2.35
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/dist/src/index.d.ts +7 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +284 -91
- package/dist/src/index.js.map +1 -1
- package/dist/src/peer-session.d.ts +2 -1
- package/dist/src/peer-session.d.ts.map +1 -1
- package/dist/src/peer-session.js +8 -2
- package/dist/src/peer-session.js.map +1 -1
- package/dist/src/replication-announcement.d.ts +1 -0
- package/dist/src/replication-announcement.d.ts.map +1 -1
- package/dist/src/replication-announcement.js +16 -2
- package/dist/src/replication-announcement.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +84 -4
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +441 -7
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +20 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +177 -55
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/dist/src/sync/factory.js +1 -1
- package/dist/src/sync/factory.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +350 -123
- package/src/peer-session.ts +8 -0
- package/src/replication-announcement.ts +17 -2
- package/src/replication-info-v2-receive.ts +634 -11
- package/src/replication-info-v2-send.ts +227 -65
- package/src/sync/factory.ts +1 -1
package/dist/src/index.js
CHANGED
|
@@ -1701,6 +1701,7 @@ let SharedLog = (() => {
|
|
|
1701
1701
|
queueCurrentReplicationStateAnnouncementRepair: () => this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
|
|
1702
1702
|
queueCurrentReplicationStateAnnouncementRetry: (error) => this._announcements.queueCurrentReplicationStateAnnouncementRetry(error),
|
|
1703
1703
|
enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
|
|
1704
|
+
isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
|
|
1704
1705
|
isClosed: () => this.closed,
|
|
1705
1706
|
getCloseSignal: () => this._closeController.signal,
|
|
1706
1707
|
getMyReplicationSegments: () => this.getMyReplicationSegments(),
|
|
@@ -1723,6 +1724,7 @@ let SharedLog = (() => {
|
|
|
1723
1724
|
getMyReplicationSegments: () => this.getMyReplicationSegments(),
|
|
1724
1725
|
validatePersistedReplicationRangeSnapshot: (ranges) => this.validatePersistedReplicationRangeSnapshot(ranges),
|
|
1725
1726
|
isClosed: () => this.closed,
|
|
1727
|
+
isPeerSessionCurrent: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession),
|
|
1726
1728
|
isPeerSessionOpen: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession) &&
|
|
1727
1729
|
peerSession.phase === "open" &&
|
|
1728
1730
|
!this._peerSessions.isReplicationInfoBlocked(peerHash) &&
|
|
@@ -1773,6 +1775,10 @@ let SharedLog = (() => {
|
|
|
1773
1775
|
getSelfKey: () => this.node.identity.publicKey,
|
|
1774
1776
|
getReceiverTransportSession: () => BigInt(this.node.services.pubsub.session),
|
|
1775
1777
|
isClosed: () => this.closed,
|
|
1778
|
+
isPeerSessionCurrent: (peerHash, peerSession) => this._peerSessions.isCurrent(peerHash, peerSession) &&
|
|
1779
|
+
peerSession.phase === "open" &&
|
|
1780
|
+
peerSession.isActive(),
|
|
1781
|
+
isReceiveEpochCurrent: (peerHash, receiveEpoch) => this._peerSessions.isReceiveEpochCurrent(peerHash, receiveEpoch),
|
|
1776
1782
|
isPeerStateCurrent: (peerHash, peerSession, receiveEpoch) => this._peerSessions.isCurrent(peerHash, peerSession) &&
|
|
1777
1783
|
peerSession.phase === "open" &&
|
|
1778
1784
|
peerSession.isActive() &&
|
|
@@ -1801,6 +1807,7 @@ let SharedLog = (() => {
|
|
|
1801
1807
|
}
|
|
1802
1808
|
logger.trace(`Replication-info V2 recovery request failed: ${error?.message ?? String(error)}`);
|
|
1803
1809
|
},
|
|
1810
|
+
onLocalCapabilityError: (error) => this.handleReplicationLifecycleSendError(error),
|
|
1804
1811
|
});
|
|
1805
1812
|
}
|
|
1806
1813
|
createReplicatorLivenessMonitor() {
|
|
@@ -1968,6 +1975,13 @@ let SharedLog = (() => {
|
|
|
1968
1975
|
get compatibility() {
|
|
1969
1976
|
return this._logProperties?.compatibility;
|
|
1970
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Legacy replication-info is an explicit compatibility fallback. Current
|
|
1980
|
+
* logs never infer or re-enable it from a remote peer's capabilities.
|
|
1981
|
+
*/
|
|
1982
|
+
get legacyReplicationInfoEnabled() {
|
|
1983
|
+
return this.compatibility !== undefined && this.compatibility < 10;
|
|
1984
|
+
}
|
|
1971
1985
|
get isAdaptiveReplicating() {
|
|
1972
1986
|
return this._isAdaptiveReplicating;
|
|
1973
1987
|
}
|
|
@@ -4039,10 +4053,16 @@ let SharedLog = (() => {
|
|
|
4039
4053
|
// deliver the authoritative Full that completes recovery.
|
|
4040
4054
|
const peerSession = this._peerSessions.current(keyHash);
|
|
4041
4055
|
if (peerSession?.phase === "open") {
|
|
4056
|
+
const receiveEpoch = this._peerSessions.receiveEpoch(keyHash);
|
|
4042
4057
|
this._v2Receive.advanceRecovery({
|
|
4043
4058
|
peerHash: keyHash,
|
|
4044
4059
|
peerSession,
|
|
4045
|
-
receiveEpoch
|
|
4060
|
+
receiveEpoch,
|
|
4061
|
+
});
|
|
4062
|
+
this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
|
|
4063
|
+
peerHash: keyHash,
|
|
4064
|
+
peerSession,
|
|
4065
|
+
receiveEpoch,
|
|
4046
4066
|
});
|
|
4047
4067
|
}
|
|
4048
4068
|
}
|
|
@@ -9096,7 +9116,9 @@ let SharedLog = (() => {
|
|
|
9096
9116
|
this._logProperties = options;
|
|
9097
9117
|
this.domain = options?.domain
|
|
9098
9118
|
? options.domain(this)
|
|
9099
|
-
: createReplicationDomainHash(options?.compatibility && options
|
|
9119
|
+
: createReplicationDomainHash(options?.compatibility !== undefined && options.compatibility < 10
|
|
9120
|
+
? "u32"
|
|
9121
|
+
: "u64")(this);
|
|
9100
9122
|
this.indexableDomain = createIndexableDomainFromResolution(this.domain.resolution);
|
|
9101
9123
|
this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
|
|
9102
9124
|
this._checkedPrune = new CheckedPruneCoordinator();
|
|
@@ -9214,8 +9236,13 @@ let SharedLog = (() => {
|
|
|
9214
9236
|
throw new Error("waitForReplicatorRequestMaxAttempts must be a positive number");
|
|
9215
9237
|
}
|
|
9216
9238
|
this._closeController = new AbortController();
|
|
9217
|
-
this.
|
|
9218
|
-
|
|
9239
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
9240
|
+
this._announcements.setupReplicationAnnouncementRetryFunction();
|
|
9241
|
+
this._announcements.setupReplicationAnnouncementRepairFunction();
|
|
9242
|
+
}
|
|
9243
|
+
else {
|
|
9244
|
+
this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
|
|
9245
|
+
}
|
|
9219
9246
|
this._closeController.signal.addEventListener("abort", () => {
|
|
9220
9247
|
for (const [_peer, state] of this._replicationInfoRequestByPeer) {
|
|
9221
9248
|
if (state.timer)
|
|
@@ -10134,7 +10161,12 @@ let SharedLog = (() => {
|
|
|
10134
10161
|
}
|
|
10135
10162
|
async afterOpen() {
|
|
10136
10163
|
await super.afterOpen();
|
|
10137
|
-
|
|
10164
|
+
// Start the broader discovery eagerly, in parallel with rebalance, for its
|
|
10165
|
+
// routing/cache side effects. It also contains connected/provider/fanout
|
|
10166
|
+
// candidates that have not subscribed to this log, so only the authoritative
|
|
10167
|
+
// pubsub snapshot below may create subscription fallback sessions.
|
|
10168
|
+
const subscriberDiscoveryPromise = this._getTopicSubscribers(this.topic);
|
|
10169
|
+
const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(this.topic);
|
|
10138
10170
|
const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
|
|
10139
10171
|
// We do this here, because these calls requires this.closed == false
|
|
10140
10172
|
void this.pruneOfflineReplicators()
|
|
@@ -10151,6 +10183,7 @@ let SharedLog = (() => {
|
|
|
10151
10183
|
});
|
|
10152
10184
|
this._liveness.startReplicatorLivenessSweep();
|
|
10153
10185
|
await this.rebalanceParticipation();
|
|
10186
|
+
await subscriberDiscoveryPromise;
|
|
10154
10187
|
// Take into account existing subscription
|
|
10155
10188
|
(await existingSubscribersPromise)?.forEach((v) => {
|
|
10156
10189
|
if (v.equals(this.node.identity.publicKey)) {
|
|
@@ -10159,6 +10192,14 @@ let SharedLog = (() => {
|
|
|
10159
10192
|
if (this.closed) {
|
|
10160
10193
|
return;
|
|
10161
10194
|
}
|
|
10195
|
+
// The live subscribe event and this after-open snapshot can report the
|
|
10196
|
+
// same initial transport generation. The live callback rotates its
|
|
10197
|
+
// PeerSession synchronously, so any current session here proves the
|
|
10198
|
+
// fallback is stale/duplicate. Rotating again would erase the signed
|
|
10199
|
+
// capability binding that the first callback just established.
|
|
10200
|
+
if (this._peerSessions.current(v.hashcode()) !== null) {
|
|
10201
|
+
return;
|
|
10202
|
+
}
|
|
10162
10203
|
void this.runSubscriptionChangeCallback(() => this.handleSubscriptionChange(v, [this.topic], true));
|
|
10163
10204
|
});
|
|
10164
10205
|
}
|
|
@@ -10297,7 +10338,9 @@ let SharedLog = (() => {
|
|
|
10297
10338
|
cleanupPeerDisconnectTracking(peerHash, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
|
|
10298
10339
|
const peerSession = this._peerSessions.current(peerHash);
|
|
10299
10340
|
const preserveV2Session = peerSession?.phase === "open" && peerSession.isActive();
|
|
10300
|
-
this.
|
|
10341
|
+
if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
|
|
10342
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
10343
|
+
}
|
|
10301
10344
|
this._liveness._replicatorLivenessFailures.delete(peerHash);
|
|
10302
10345
|
this._liveness._replicatorLastActivityAt.delete(peerHash);
|
|
10303
10346
|
if (!preserveV2Session) {
|
|
@@ -11257,15 +11300,16 @@ let SharedLog = (() => {
|
|
|
11257
11300
|
}, 2_000);
|
|
11258
11301
|
try {
|
|
11259
11302
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
11260
|
-
|
|
11261
|
-
|
|
11303
|
+
const resets = [this._v2Send.sendTerminalReset(abort.signal)];
|
|
11304
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
11305
|
+
resets.push(this.rpc
|
|
11262
11306
|
.send(reset, {
|
|
11263
11307
|
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
11264
11308
|
signal: abort.signal,
|
|
11265
11309
|
})
|
|
11266
|
-
.catch(() => { })
|
|
11267
|
-
|
|
11268
|
-
|
|
11310
|
+
.catch(() => { }));
|
|
11311
|
+
}
|
|
11312
|
+
await Promise.all(resets);
|
|
11269
11313
|
}
|
|
11270
11314
|
finally {
|
|
11271
11315
|
clearTimeout(abortTimer);
|
|
@@ -11385,15 +11429,16 @@ let SharedLog = (() => {
|
|
|
11385
11429
|
}, 2_000);
|
|
11386
11430
|
try {
|
|
11387
11431
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
11388
|
-
|
|
11389
|
-
|
|
11432
|
+
const resets = [this._v2Send.sendTerminalReset(abort.signal)];
|
|
11433
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
11434
|
+
resets.push(this.rpc
|
|
11390
11435
|
.send(reset, {
|
|
11391
11436
|
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
11392
11437
|
signal: abort.signal,
|
|
11393
11438
|
})
|
|
11394
|
-
.catch(() => { })
|
|
11395
|
-
|
|
11396
|
-
|
|
11439
|
+
.catch(() => { }));
|
|
11440
|
+
}
|
|
11441
|
+
await Promise.all(resets);
|
|
11397
11442
|
}
|
|
11398
11443
|
finally {
|
|
11399
11444
|
clearTimeout(abortTimer);
|
|
@@ -11817,6 +11862,17 @@ let SharedLog = (() => {
|
|
|
11817
11862
|
if (!context.from) {
|
|
11818
11863
|
throw new Error("Missing from in update role message");
|
|
11819
11864
|
}
|
|
11865
|
+
if (!this.legacyReplicationInfoEnabled &&
|
|
11866
|
+
(msg instanceof RequestReplicationInfoMessage ||
|
|
11867
|
+
msg instanceof ResponseRoleMessage ||
|
|
11868
|
+
msg instanceof AllReplicatingSegmentsMessage ||
|
|
11869
|
+
msg instanceof AddedReplicationSegmentMessage ||
|
|
11870
|
+
msg instanceof StoppedReplicating)) {
|
|
11871
|
+
// These variants remain registered decode tombstones, but current logs
|
|
11872
|
+
// fail closed before leases, synchronizer work, liveness, watermarks or
|
|
11873
|
+
// mutations. Only an explicit pre-v10 compatibility open admits them.
|
|
11874
|
+
return;
|
|
11875
|
+
}
|
|
11820
11876
|
// Snapshot receive ownership before any async handler gets a chance to
|
|
11821
11877
|
// yield. Replication-info messages reach their branch only after the
|
|
11822
11878
|
// synchronizer declines them, and a U/S transition can happen meanwhile.
|
|
@@ -13231,14 +13287,30 @@ let SharedLog = (() => {
|
|
|
13231
13287
|
openingSession: receiveSession,
|
|
13232
13288
|
});
|
|
13233
13289
|
}
|
|
13234
|
-
else
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13290
|
+
else {
|
|
13291
|
+
const observed = this.observePeerSyncCapabilities({
|
|
13292
|
+
peerHash: receiveFromHash,
|
|
13293
|
+
capabilities: msg.capabilities,
|
|
13294
|
+
transportSession: capabilityTransportSession,
|
|
13295
|
+
timestamp: capabilityTimestamp,
|
|
13296
|
+
});
|
|
13297
|
+
if (observed && receiveSession?.phase === "open") {
|
|
13298
|
+
this.promoteReplicationInfoV2ReceiveCapability(context.from, receiveSession);
|
|
13299
|
+
}
|
|
13300
|
+
else if (observed && receiveSession === null) {
|
|
13301
|
+
// A capability can arrive before the sender's topic Subscribe after
|
|
13302
|
+
// reconnect. Ask that authenticated peer for its authoritative
|
|
13303
|
+
// subscriber snapshot; the resulting Subscribe creates the real
|
|
13304
|
+
// PeerSession and completes the symmetric capability handshake.
|
|
13305
|
+
// Never synthesize membership from capability traffic alone.
|
|
13306
|
+
void Promise.resolve()
|
|
13307
|
+
.then(() => this.node.services.pubsub.requestSubscribers(this.topic, context.from))
|
|
13308
|
+
.catch((error) => {
|
|
13309
|
+
if (!isNotStartedError(error)) {
|
|
13310
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
13311
|
+
}
|
|
13312
|
+
});
|
|
13313
|
+
}
|
|
13242
13314
|
}
|
|
13243
13315
|
}
|
|
13244
13316
|
return;
|
|
@@ -13667,7 +13739,8 @@ let SharedLog = (() => {
|
|
|
13667
13739
|
return;
|
|
13668
13740
|
}
|
|
13669
13741
|
this._liveness.markReplicatorActivity(fromHash);
|
|
13670
|
-
if (msg instanceof FullReplicationInfoV2Message
|
|
13742
|
+
if (msg instanceof FullReplicationInfoV2Message &&
|
|
13743
|
+
this.legacyReplicationInfoEnabled) {
|
|
13671
13744
|
this.cancelReplicationInfoRequests(fromHash);
|
|
13672
13745
|
}
|
|
13673
13746
|
});
|
|
@@ -14171,9 +14244,30 @@ let SharedLog = (() => {
|
|
|
14171
14244
|
reject(new TimeoutError(`Timeout waiting for replicator ${key.hashcode()}`));
|
|
14172
14245
|
}, timeoutMs);
|
|
14173
14246
|
let requestAttempts = 0;
|
|
14247
|
+
let subscriberSnapshotInFlight;
|
|
14174
14248
|
const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
|
|
14175
14249
|
const maxRequestAttempts = this.waitForReplicatorRequestMaxAttempts ??
|
|
14176
14250
|
Math.max(WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS, Math.ceil(timeoutMs / requestIntervalMs));
|
|
14251
|
+
const requestSubscriberSnapshot = () => {
|
|
14252
|
+
if (subscriberSnapshotInFlight) {
|
|
14253
|
+
return;
|
|
14254
|
+
}
|
|
14255
|
+
subscriberSnapshotInFlight = Promise.resolve()
|
|
14256
|
+
.then(async () => {
|
|
14257
|
+
if (settled || this.closed) {
|
|
14258
|
+
return;
|
|
14259
|
+
}
|
|
14260
|
+
await this.node.services.pubsub.requestSubscribers(this.topic, key);
|
|
14261
|
+
})
|
|
14262
|
+
.catch((error) => {
|
|
14263
|
+
if (!isNotStartedError(error)) {
|
|
14264
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
14265
|
+
}
|
|
14266
|
+
})
|
|
14267
|
+
.finally(() => {
|
|
14268
|
+
subscriberSnapshotInFlight = undefined;
|
|
14269
|
+
});
|
|
14270
|
+
};
|
|
14177
14271
|
const requestReplicationInfo = () => {
|
|
14178
14272
|
if (settled || this.closed) {
|
|
14179
14273
|
return;
|
|
@@ -14182,17 +14276,37 @@ let SharedLog = (() => {
|
|
|
14182
14276
|
return;
|
|
14183
14277
|
}
|
|
14184
14278
|
requestAttempts++;
|
|
14185
|
-
this.
|
|
14186
|
-
.
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14279
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
14280
|
+
this.rpc
|
|
14281
|
+
.send(new RequestReplicationInfoMessage(), {
|
|
14282
|
+
mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
|
|
14283
|
+
})
|
|
14284
|
+
.catch((e) => {
|
|
14285
|
+
// Best-effort: missing peers / unopened RPC should not fail the wait logic.
|
|
14286
|
+
if (isNotStartedError(e)) {
|
|
14287
|
+
return;
|
|
14288
|
+
}
|
|
14289
|
+
logger.error(e?.toString?.() ?? String(e));
|
|
14290
|
+
});
|
|
14291
|
+
}
|
|
14292
|
+
else {
|
|
14293
|
+
const peerHash = key.hashcode();
|
|
14294
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
14295
|
+
if (peerSession?.phase === "open") {
|
|
14296
|
+
this._v2Receive.resumeParkedRequest({
|
|
14297
|
+
peerHash,
|
|
14298
|
+
peerSession,
|
|
14299
|
+
receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
|
|
14300
|
+
});
|
|
14193
14301
|
}
|
|
14194
|
-
|
|
14195
|
-
|
|
14302
|
+
else if (peerSession === null || peerSession.phase === "departing") {
|
|
14303
|
+
// A peer can be known to routing before its SharedLog topic
|
|
14304
|
+
// subscription has been observed. Legacy requests used to bootstrap
|
|
14305
|
+
// that case directly; V2 needs an authoritative Subscribe snapshot
|
|
14306
|
+
// before it can create a fenced PeerSession and request a Full.
|
|
14307
|
+
requestSubscriberSnapshot();
|
|
14308
|
+
}
|
|
14309
|
+
}
|
|
14196
14310
|
if (requestAttempts < maxRequestAttempts) {
|
|
14197
14311
|
requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
|
|
14198
14312
|
}
|
|
@@ -16004,12 +16118,71 @@ let SharedLog = (() => {
|
|
|
16004
16118
|
}
|
|
16005
16119
|
this._replicationInfoRequestByPeer.delete(peerHash);
|
|
16006
16120
|
}
|
|
16121
|
+
scheduleReplicationInfoV2Recovery(peer, replicationLifecycleController = this._instanceLifecycle
|
|
16122
|
+
?.membershipLifecycleController) {
|
|
16123
|
+
if (!replicationLifecycleController ||
|
|
16124
|
+
!this.isReplicationLifecycleActive(replicationLifecycleController)) {
|
|
16125
|
+
return;
|
|
16126
|
+
}
|
|
16127
|
+
const peerHash = peer.hashcode();
|
|
16128
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
16129
|
+
if (!peerSession || peerSession.phase !== "open") {
|
|
16130
|
+
return;
|
|
16131
|
+
}
|
|
16132
|
+
const requestStates = this._replicationInfoRequestByPeer;
|
|
16133
|
+
const existing = requestStates.get(peerHash);
|
|
16134
|
+
if (existing?.peerSession === peerSession) {
|
|
16135
|
+
return;
|
|
16136
|
+
}
|
|
16137
|
+
if (existing) {
|
|
16138
|
+
if (existing.timer) {
|
|
16139
|
+
clearTimeout(existing.timer);
|
|
16140
|
+
}
|
|
16141
|
+
requestStates.delete(peerHash);
|
|
16142
|
+
}
|
|
16143
|
+
const state = {
|
|
16144
|
+
attempts: 0,
|
|
16145
|
+
peerSession,
|
|
16146
|
+
};
|
|
16147
|
+
requestStates.set(peerHash, state);
|
|
16148
|
+
const cancel = () => {
|
|
16149
|
+
if (requestStates.get(peerHash) !== state) {
|
|
16150
|
+
return;
|
|
16151
|
+
}
|
|
16152
|
+
if (state.timer) {
|
|
16153
|
+
clearTimeout(state.timer);
|
|
16154
|
+
}
|
|
16155
|
+
requestStates.delete(peerHash);
|
|
16156
|
+
};
|
|
16157
|
+
const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
|
|
16158
|
+
const tick = () => {
|
|
16159
|
+
if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
16160
|
+
peerSession.phase !== "open" ||
|
|
16161
|
+
!this._peerSessions.isCurrent(peerHash, peerSession)) {
|
|
16162
|
+
cancel();
|
|
16163
|
+
return;
|
|
16164
|
+
}
|
|
16165
|
+
this._v2Receive.resumeParkedRequest({
|
|
16166
|
+
peerHash,
|
|
16167
|
+
peerSession,
|
|
16168
|
+
receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
|
|
16169
|
+
});
|
|
16170
|
+
state.attempts++;
|
|
16171
|
+
state.timer = setTimeout(tick, intervalMs);
|
|
16172
|
+
state.timer.unref?.();
|
|
16173
|
+
};
|
|
16174
|
+
tick();
|
|
16175
|
+
}
|
|
16007
16176
|
scheduleReplicationInfoRequests(peer, replicationLifecycleController = this._instanceLifecycle
|
|
16008
16177
|
?.membershipLifecycleController) {
|
|
16009
16178
|
if (!replicationLifecycleController ||
|
|
16010
16179
|
!this.isReplicationLifecycleActive(replicationLifecycleController)) {
|
|
16011
16180
|
return;
|
|
16012
16181
|
}
|
|
16182
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
16183
|
+
this.scheduleReplicationInfoV2Recovery(peer, replicationLifecycleController);
|
|
16184
|
+
return;
|
|
16185
|
+
}
|
|
16013
16186
|
const peerHash = peer.hashcode();
|
|
16014
16187
|
const requestStates = this._replicationInfoRequestByPeer;
|
|
16015
16188
|
if (requestStates.has(peerHash)) {
|
|
@@ -16060,7 +16233,7 @@ let SharedLog = (() => {
|
|
|
16060
16233
|
};
|
|
16061
16234
|
tick();
|
|
16062
16235
|
}
|
|
16063
|
-
async handleSubscriptionChange(publicKey, topics, subscribed, subscriptionEpoch) {
|
|
16236
|
+
async handleSubscriptionChange(publicKey, topics, subscribed, subscriptionEpoch, subscriptionTransportSession) {
|
|
16064
16237
|
if (!topics.includes(this.topic)) {
|
|
16065
16238
|
return;
|
|
16066
16239
|
}
|
|
@@ -16076,13 +16249,29 @@ let SharedLog = (() => {
|
|
|
16076
16249
|
if (!ownsSubscriptionEpoch()) {
|
|
16077
16250
|
return;
|
|
16078
16251
|
}
|
|
16252
|
+
// A reconnect can arrive before the previous exact-session recovery tick
|
|
16253
|
+
// observes its stale session. Retire that job synchronously so it cannot
|
|
16254
|
+
// suppress the replacement session's scheduler in the shared peer slot.
|
|
16255
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
16079
16256
|
// A destination stream is scoped to exactly one topic-subscription
|
|
16080
16257
|
// session. Abort the predecessor synchronously before either barrier can
|
|
16081
16258
|
// yield; a late queue completion must never enter the new session.
|
|
16082
16259
|
this._v2Receive.clearPeer(peerHash);
|
|
16083
16260
|
this._v2Send.clearPeer(peerHash);
|
|
16084
|
-
this._peerSyncCapabilitySessions.
|
|
16085
|
-
|
|
16261
|
+
const capabilityTransportSession = this._peerSyncCapabilitySessions.get(peerHash);
|
|
16262
|
+
const canInheritCapability = subscribed &&
|
|
16263
|
+
(subscriptionTransportSession !== undefined
|
|
16264
|
+
? capabilityTransportSession === subscriptionTransportSession
|
|
16265
|
+
: !expectedSubscriptionEpoch.hasPredecessor);
|
|
16266
|
+
if (!canInheritCapability) {
|
|
16267
|
+
// Departures and successor openings revoke the signed transport binding.
|
|
16268
|
+
// A live successor may inherit a capability that arrived just before its
|
|
16269
|
+
// Subscribe only when both signed frames belong to the same transport
|
|
16270
|
+
// generation. Snapshot fallbacks lack that proof, so only their first
|
|
16271
|
+
// opening may inherit pre-opening capability state.
|
|
16272
|
+
this._peerSyncCapabilitySessions.delete(peerHash);
|
|
16273
|
+
this._peerSyncCapabilityTimestamps.delete(peerHash);
|
|
16274
|
+
}
|
|
16086
16275
|
if (subscribed) {
|
|
16087
16276
|
const pendingOpeningCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
|
|
16088
16277
|
if (pendingOpeningCapabilities &&
|
|
@@ -16195,56 +16384,48 @@ let SharedLog = (() => {
|
|
|
16195
16384
|
this._liveness.markReplicatorActivity(peerHash);
|
|
16196
16385
|
this._peerSessions.markOpen(peerHash, expectedSubscriptionEpoch);
|
|
16197
16386
|
this.promoteReplicationInfoV2ReceiveCapability(publicKey, expectedSubscriptionEpoch);
|
|
16198
|
-
// Decode, sender and authenticated apply readiness are separate bits.
|
|
16199
|
-
//
|
|
16200
|
-
//
|
|
16201
|
-
//
|
|
16202
|
-
//
|
|
16387
|
+
// Decode, sender and authenticated apply readiness are separate bits. An
|
|
16388
|
+
// ACKed capability advert is the local half of receiver-led negotiation;
|
|
16389
|
+
// readiness is promoted only after the legacy startup path has completed.
|
|
16390
|
+
// This preserves mixed-version discovery without putting capability ACK
|
|
16391
|
+
// latency on the subscription callback's critical path.
|
|
16203
16392
|
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
16204
|
-
const localCapabilityAdvertisement = this.
|
|
16393
|
+
const localCapabilityAdvertisement = this._v2Receive.advertiseLocalCapability({
|
|
16205
16394
|
target: publicKey,
|
|
16206
16395
|
peerSession: expectedSubscriptionEpoch,
|
|
16207
16396
|
receiveEpoch,
|
|
16208
16397
|
signal: replicationLifecycleController.signal,
|
|
16209
|
-
}).catch((error) => {
|
|
16210
|
-
this.handleReplicationLifecycleSendError(error, replicationLifecycleController);
|
|
16211
|
-
return undefined;
|
|
16212
16398
|
});
|
|
16213
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
16216
|
-
|
|
16217
|
-
|
|
16218
|
-
|
|
16219
|
-
isNotStartedError(error)) {
|
|
16220
|
-
return;
|
|
16221
|
-
}
|
|
16222
|
-
throw error;
|
|
16223
|
-
}
|
|
16224
|
-
if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
16225
|
-
!ownsSubscriptionEpoch()) {
|
|
16399
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
16400
|
+
// Current logs have no legacy startup work to order ahead of RequestV2.
|
|
16401
|
+
// Releasing is synchronous and exact-session fenced; the ACK may still
|
|
16402
|
+
// arrive later and promote readiness through the coordinator.
|
|
16403
|
+
localCapabilityAdvertisement.releaseLegacyBarrier();
|
|
16404
|
+
this.scheduleReplicationInfoV2Recovery(publicKey, replicationLifecycleController);
|
|
16226
16405
|
return;
|
|
16227
16406
|
}
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16407
|
+
try {
|
|
16408
|
+
let replicationSegments;
|
|
16409
|
+
try {
|
|
16410
|
+
replicationSegments = await this.getMyReplicationSegments();
|
|
16411
|
+
}
|
|
16412
|
+
catch (error) {
|
|
16413
|
+
if (!this.isReplicationLifecycleActive(replicationLifecycleController) &&
|
|
16414
|
+
isNotStartedError(error)) {
|
|
16415
|
+
return;
|
|
16416
|
+
}
|
|
16417
|
+
throw error;
|
|
16418
|
+
}
|
|
16239
16419
|
if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
16240
16420
|
!ownsSubscriptionEpoch()) {
|
|
16241
16421
|
return;
|
|
16242
16422
|
}
|
|
16243
|
-
if (
|
|
16244
|
-
|
|
16423
|
+
if (replicationSegments.length > 0) {
|
|
16424
|
+
const segments = replicationSegments.map((x) => x.toReplicationRange());
|
|
16425
|
+
this.validatePersistedReplicationRangeSnapshot(segments);
|
|
16245
16426
|
await this.rpc
|
|
16246
|
-
.send(new
|
|
16247
|
-
|
|
16427
|
+
.send(new AllReplicatingSegmentsMessage({
|
|
16428
|
+
segments,
|
|
16248
16429
|
}), {
|
|
16249
16430
|
mode: new AcknowledgeDelivery({
|
|
16250
16431
|
redundancy: 1,
|
|
@@ -16253,23 +16434,35 @@ let SharedLog = (() => {
|
|
|
16253
16434
|
signal: replicationLifecycleController.signal,
|
|
16254
16435
|
})
|
|
16255
16436
|
.catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
|
|
16437
|
+
if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
16438
|
+
!ownsSubscriptionEpoch()) {
|
|
16439
|
+
return;
|
|
16440
|
+
}
|
|
16441
|
+
if (this.v8Behaviour) {
|
|
16442
|
+
// for backwards compatibility
|
|
16443
|
+
await this.rpc
|
|
16444
|
+
.send(new ResponseRoleMessage({
|
|
16445
|
+
role: this.getRoleFromReplicationSegments(replicationSegments),
|
|
16446
|
+
}), {
|
|
16447
|
+
mode: new AcknowledgeDelivery({
|
|
16448
|
+
redundancy: 1,
|
|
16449
|
+
to: [publicKey],
|
|
16450
|
+
}),
|
|
16451
|
+
signal: replicationLifecycleController.signal,
|
|
16452
|
+
})
|
|
16453
|
+
.catch((error) => this.handleReplicationLifecycleSendError(error, replicationLifecycleController));
|
|
16454
|
+
}
|
|
16455
|
+
}
|
|
16456
|
+
// Keep legacy request-based discovery independent of the capability ACK.
|
|
16457
|
+
// This makes mixed-version joins resilient to timing-sensitive delivery/order
|
|
16458
|
+
// issues where we may miss the remote peer's initial announcement.
|
|
16459
|
+
if (this.isReplicationLifecycleActive(replicationLifecycleController) &&
|
|
16460
|
+
ownsSubscriptionEpoch()) {
|
|
16461
|
+
this.scheduleReplicationInfoRequests(publicKey, replicationLifecycleController);
|
|
16256
16462
|
}
|
|
16257
16463
|
}
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
// issues where we may miss the remote peer's initial announcement.
|
|
16261
|
-
if (this.isReplicationLifecycleActive(replicationLifecycleController) &&
|
|
16262
|
-
ownsSubscriptionEpoch()) {
|
|
16263
|
-
this.scheduleReplicationInfoRequests(publicKey, replicationLifecycleController);
|
|
16264
|
-
}
|
|
16265
|
-
const localCapabilityReady = await localCapabilityAdvertisement;
|
|
16266
|
-
if (localCapabilityReady) {
|
|
16267
|
-
this._v2Receive.markLocalCapabilityReady({
|
|
16268
|
-
peerHash,
|
|
16269
|
-
peerSession: expectedSubscriptionEpoch,
|
|
16270
|
-
receiveEpoch,
|
|
16271
|
-
...localCapabilityReady,
|
|
16272
|
-
});
|
|
16464
|
+
finally {
|
|
16465
|
+
localCapabilityAdvertisement.releaseLegacyBarrier();
|
|
16273
16466
|
}
|
|
16274
16467
|
}
|
|
16275
16468
|
getClampedReplicas(customValue) {
|
|
@@ -17267,7 +17460,7 @@ let SharedLog = (() => {
|
|
|
17267
17460
|
this.remoteBlocks.onReachable(evt.detail.from);
|
|
17268
17461
|
this._peerSessions.blockReplicationInfo(fromHash);
|
|
17269
17462
|
this.invalidateSharedLogTopicSubscribersCache();
|
|
17270
|
-
await this.handleSubscriptionChange(evt.detail.from, evt.detail.topics, true, subscriptionEpoch);
|
|
17463
|
+
await this.handleSubscriptionChange(evt.detail.from, evt.detail.topics, true, subscriptionEpoch, evt.detail.session);
|
|
17271
17464
|
}
|
|
17272
17465
|
async rebalanceParticipation(ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(), rebalanceParticipationDebounced = this.rebalanceParticipationDebounced) {
|
|
17273
17466
|
// Stage 3: the lifecycle owns all three identity terms. `lifecycle` may
|