@peerbit/shared-log 13.2.34 → 13.2.36
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 +26 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +307 -38
- 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 +21 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +106 -5
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +22 -1
- 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 +13 -13
- package/src/index.ts +371 -48
- package/src/peer-session.ts +8 -0
- package/src/replication-announcement.ts +17 -2
- package/src/replication-info-v2-receive.ts +135 -5
- package/src/replication-info-v2-send.ts +21 -1
- package/src/sync/factory.ts +1 -1
package/dist/src/index.js
CHANGED
|
@@ -370,6 +370,13 @@ export const WAIT_FOR_REPLICATOR_TIMEOUT = 20000;
|
|
|
370
370
|
export const WAIT_FOR_ROLE_MATURITY = 5000;
|
|
371
371
|
export const WAIT_FOR_REPLICATOR_REQUEST_INTERVAL = 1000;
|
|
372
372
|
export const WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS = 3;
|
|
373
|
+
// The V2 recovery scheduler is deliberately persistent (a subscribed peer is
|
|
374
|
+
// re-solicited for as long as its topic session stays open), but consecutive
|
|
375
|
+
// fruitless park/unpark cycles double the wait before the next unpark so a
|
|
376
|
+
// silent-but-subscribed peer converges to one bounded request cycle per cap
|
|
377
|
+
// window instead of one per base interval. Any applied V2 progress resets it.
|
|
378
|
+
export const REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_DELAY = 300_000;
|
|
379
|
+
const REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_EXPONENT = 20;
|
|
373
380
|
// TODO(prune): Investigate if/when a non-zero prune delay is required for correctness
|
|
374
381
|
// (e.g. responsibility/replication-info message reordering in multi-peer scenarios).
|
|
375
382
|
// Prefer making pruning robust without timing-based heuristics.
|
|
@@ -695,6 +702,11 @@ let SharedLog = (() => {
|
|
|
695
702
|
// reconnect barrier commits. See PeerSessionRegistry._replicationInfoBlockedPeers.
|
|
696
703
|
_replicationInfoRequestByPeer;
|
|
697
704
|
_replicationInfoApplyQueueByPeer;
|
|
705
|
+
// One in-flight targeted subscriber-snapshot request per session-less peer.
|
|
706
|
+
// A capability burst from a peer whose Subscribe has not been observed must
|
|
707
|
+
// coalesce into a single pubsub.requestSubscribers call (mirrors the
|
|
708
|
+
// waitForReplicator in-flight coalescing); a later burst may request again.
|
|
709
|
+
_subscriberSnapshotRequestsByPeer;
|
|
698
710
|
// Range ids are global primary keys while receive lanes are per peer. Keep
|
|
699
711
|
// reads and writes that decide one mutation in a single global lane.
|
|
700
712
|
_replicationRangeMutationTail = Promise.resolve();
|
|
@@ -1701,6 +1713,7 @@ let SharedLog = (() => {
|
|
|
1701
1713
|
queueCurrentReplicationStateAnnouncementRepair: () => this._announcements.queueCurrentReplicationStateAnnouncementRepair(),
|
|
1702
1714
|
queueCurrentReplicationStateAnnouncementRetry: (error) => this._announcements.queueCurrentReplicationStateAnnouncementRetry(error),
|
|
1703
1715
|
enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
|
|
1716
|
+
isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
|
|
1704
1717
|
isClosed: () => this.closed,
|
|
1705
1718
|
getCloseSignal: () => this._closeController.signal,
|
|
1706
1719
|
getMyReplicationSegments: () => this.getMyReplicationSegments(),
|
|
@@ -1874,6 +1887,7 @@ let SharedLog = (() => {
|
|
|
1874
1887
|
this._pendingIHaveCallbacks = new Set();
|
|
1875
1888
|
this.latestReplicationInfoMessage = new Map();
|
|
1876
1889
|
this._replicationInfoRequestByPeer = new Map();
|
|
1890
|
+
this._subscriberSnapshotRequestsByPeer = new Map();
|
|
1877
1891
|
this._replicationInfoApplyQueueByPeer = new Map();
|
|
1878
1892
|
// The registry constructor runs resetForOpen(), which creates the
|
|
1879
1893
|
// replication-info blocked set (fence B5) alongside the session maps —
|
|
@@ -1974,6 +1988,13 @@ let SharedLog = (() => {
|
|
|
1974
1988
|
get compatibility() {
|
|
1975
1989
|
return this._logProperties?.compatibility;
|
|
1976
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Legacy replication-info is an explicit compatibility fallback. Current
|
|
1993
|
+
* logs never infer or re-enable it from a remote peer's capabilities.
|
|
1994
|
+
*/
|
|
1995
|
+
get legacyReplicationInfoEnabled() {
|
|
1996
|
+
return this.compatibility !== undefined && this.compatibility < 10;
|
|
1997
|
+
}
|
|
1977
1998
|
get isAdaptiveReplicating() {
|
|
1978
1999
|
return this._isAdaptiveReplicating;
|
|
1979
2000
|
}
|
|
@@ -2379,6 +2400,9 @@ let SharedLog = (() => {
|
|
|
2379
2400
|
: previousTimestamp);
|
|
2380
2401
|
if (generationAdvanced) {
|
|
2381
2402
|
this._v2Send.advancePeerCapability(peerHash);
|
|
2403
|
+
// A fresh signed capability generation is V2 progress from the peer:
|
|
2404
|
+
// recovery re-solicitation may restart from the base interval.
|
|
2405
|
+
this.resetReplicationInfoV2RecoveryEscalation(peerHash);
|
|
2382
2406
|
}
|
|
2383
2407
|
return true;
|
|
2384
2408
|
}
|
|
@@ -2400,6 +2424,33 @@ let SharedLog = (() => {
|
|
|
2400
2424
|
capabilityTimestamp,
|
|
2401
2425
|
});
|
|
2402
2426
|
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Coalesced targeted subscriber-snapshot request for the
|
|
2429
|
+
* capability-before-Subscribe recovery path. The observed-capability gate
|
|
2430
|
+
* is sender-paced (any advancing timestamp passes), so a burst of frames
|
|
2431
|
+
* from one session-less peer must not fan out into one GetSubscribers
|
|
2432
|
+
* unicast per frame. One request per peer is in flight at a time; once it
|
|
2433
|
+
* settles, a genuinely new session-less capability may request again.
|
|
2434
|
+
*/
|
|
2435
|
+
requestSubscriberSnapshotForCapability(target) {
|
|
2436
|
+
const peerHash = target.hashcode();
|
|
2437
|
+
if (this._subscriberSnapshotRequestsByPeer.has(peerHash)) {
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
const request = Promise.resolve()
|
|
2441
|
+
.then(() => this.node.services.pubsub.requestSubscribers(this.topic, target))
|
|
2442
|
+
.catch((error) => {
|
|
2443
|
+
if (!isNotStartedError(error)) {
|
|
2444
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
2445
|
+
}
|
|
2446
|
+
})
|
|
2447
|
+
.finally(() => {
|
|
2448
|
+
if (this._subscriberSnapshotRequestsByPeer.get(peerHash) === request) {
|
|
2449
|
+
this._subscriberSnapshotRequestsByPeer.delete(peerHash);
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
this._subscriberSnapshotRequestsByPeer.set(peerHash, request);
|
|
2453
|
+
}
|
|
2403
2454
|
/**
|
|
2404
2455
|
* Live append gossip may use the raw exchange-heads path only when we
|
|
2405
2456
|
* opted into raw sync and every remote recipient advertised raw capability
|
|
@@ -9108,7 +9159,9 @@ let SharedLog = (() => {
|
|
|
9108
9159
|
this._logProperties = options;
|
|
9109
9160
|
this.domain = options?.domain
|
|
9110
9161
|
? options.domain(this)
|
|
9111
|
-
: createReplicationDomainHash(options?.compatibility && options
|
|
9162
|
+
: createReplicationDomainHash(options?.compatibility !== undefined && options.compatibility < 10
|
|
9163
|
+
? "u32"
|
|
9164
|
+
: "u64")(this);
|
|
9112
9165
|
this.indexableDomain = createIndexableDomainFromResolution(this.domain.resolution);
|
|
9113
9166
|
this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
|
|
9114
9167
|
this._checkedPrune = new CheckedPruneCoordinator();
|
|
@@ -9118,6 +9171,7 @@ let SharedLog = (() => {
|
|
|
9118
9171
|
this._pendingIHaveCallbacks = new Set();
|
|
9119
9172
|
this.latestReplicationInfoMessage = new Map();
|
|
9120
9173
|
this._replicationInfoRequestByPeer = new Map();
|
|
9174
|
+
this._subscriberSnapshotRequestsByPeer = new Map();
|
|
9121
9175
|
// Terminal close/drop drains the previous lifecycle before another open can
|
|
9122
9176
|
// install fresh lanes and opaque per-subscription ownership tokens.
|
|
9123
9177
|
this._replicationInfoApplyQueueByPeer = new Map();
|
|
@@ -9226,8 +9280,13 @@ let SharedLog = (() => {
|
|
|
9226
9280
|
throw new Error("waitForReplicatorRequestMaxAttempts must be a positive number");
|
|
9227
9281
|
}
|
|
9228
9282
|
this._closeController = new AbortController();
|
|
9229
|
-
this.
|
|
9230
|
-
|
|
9283
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
9284
|
+
this._announcements.setupReplicationAnnouncementRetryFunction();
|
|
9285
|
+
this._announcements.setupReplicationAnnouncementRepairFunction();
|
|
9286
|
+
}
|
|
9287
|
+
else {
|
|
9288
|
+
this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
|
|
9289
|
+
}
|
|
9231
9290
|
this._closeController.signal.addEventListener("abort", () => {
|
|
9232
9291
|
for (const [_peer, state] of this._replicationInfoRequestByPeer) {
|
|
9233
9292
|
if (state.timer)
|
|
@@ -10146,7 +10205,12 @@ let SharedLog = (() => {
|
|
|
10146
10205
|
}
|
|
10147
10206
|
async afterOpen() {
|
|
10148
10207
|
await super.afterOpen();
|
|
10149
|
-
|
|
10208
|
+
// Start the broader discovery eagerly, in parallel with rebalance, for its
|
|
10209
|
+
// routing/cache side effects. It also contains connected/provider/fanout
|
|
10210
|
+
// candidates that have not subscribed to this log, so only the authoritative
|
|
10211
|
+
// pubsub snapshot below may create subscription fallback sessions.
|
|
10212
|
+
const subscriberDiscoveryPromise = this._getTopicSubscribers(this.topic);
|
|
10213
|
+
const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(this.topic);
|
|
10150
10214
|
const replicationLifecycleController = this._instanceLifecycle?.membershipLifecycleController;
|
|
10151
10215
|
// We do this here, because these calls requires this.closed == false
|
|
10152
10216
|
void this.pruneOfflineReplicators()
|
|
@@ -10163,6 +10227,7 @@ let SharedLog = (() => {
|
|
|
10163
10227
|
});
|
|
10164
10228
|
this._liveness.startReplicatorLivenessSweep();
|
|
10165
10229
|
await this.rebalanceParticipation();
|
|
10230
|
+
await subscriberDiscoveryPromise;
|
|
10166
10231
|
// Take into account existing subscription
|
|
10167
10232
|
(await existingSubscribersPromise)?.forEach((v) => {
|
|
10168
10233
|
if (v.equals(this.node.identity.publicKey)) {
|
|
@@ -10171,6 +10236,14 @@ let SharedLog = (() => {
|
|
|
10171
10236
|
if (this.closed) {
|
|
10172
10237
|
return;
|
|
10173
10238
|
}
|
|
10239
|
+
// The live subscribe event and this after-open snapshot can report the
|
|
10240
|
+
// same initial transport generation. The live callback rotates its
|
|
10241
|
+
// PeerSession synchronously, so any current session here proves the
|
|
10242
|
+
// fallback is stale/duplicate. Rotating again would erase the signed
|
|
10243
|
+
// capability binding that the first callback just established.
|
|
10244
|
+
if (this._peerSessions.current(v.hashcode()) !== null) {
|
|
10245
|
+
return;
|
|
10246
|
+
}
|
|
10174
10247
|
void this.runSubscriptionChangeCallback(() => this.handleSubscriptionChange(v, [this.topic], true));
|
|
10175
10248
|
});
|
|
10176
10249
|
}
|
|
@@ -10309,7 +10382,9 @@ let SharedLog = (() => {
|
|
|
10309
10382
|
cleanupPeerDisconnectTracking(peerHash, ownershipLifecycleController = this.captureReplicationOwnershipLifecycle()) {
|
|
10310
10383
|
const peerSession = this._peerSessions.current(peerHash);
|
|
10311
10384
|
const preserveV2Session = peerSession?.phase === "open" && peerSession.isActive();
|
|
10312
|
-
this.
|
|
10385
|
+
if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
|
|
10386
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
10387
|
+
}
|
|
10313
10388
|
this._liveness._replicatorLivenessFailures.delete(peerHash);
|
|
10314
10389
|
this._liveness._replicatorLastActivityAt.delete(peerHash);
|
|
10315
10390
|
if (!preserveV2Session) {
|
|
@@ -11269,15 +11344,16 @@ let SharedLog = (() => {
|
|
|
11269
11344
|
}, 2_000);
|
|
11270
11345
|
try {
|
|
11271
11346
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
11272
|
-
|
|
11273
|
-
|
|
11347
|
+
const resets = [this._v2Send.sendTerminalReset(abort.signal)];
|
|
11348
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
11349
|
+
resets.push(this.rpc
|
|
11274
11350
|
.send(reset, {
|
|
11275
11351
|
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
11276
11352
|
signal: abort.signal,
|
|
11277
11353
|
})
|
|
11278
|
-
.catch(() => { })
|
|
11279
|
-
|
|
11280
|
-
|
|
11354
|
+
.catch(() => { }));
|
|
11355
|
+
}
|
|
11356
|
+
await Promise.all(resets);
|
|
11281
11357
|
}
|
|
11282
11358
|
finally {
|
|
11283
11359
|
clearTimeout(abortTimer);
|
|
@@ -11397,15 +11473,16 @@ let SharedLog = (() => {
|
|
|
11397
11473
|
}, 2_000);
|
|
11398
11474
|
try {
|
|
11399
11475
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
11400
|
-
|
|
11401
|
-
|
|
11476
|
+
const resets = [this._v2Send.sendTerminalReset(abort.signal)];
|
|
11477
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
11478
|
+
resets.push(this.rpc
|
|
11402
11479
|
.send(reset, {
|
|
11403
11480
|
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
11404
11481
|
signal: abort.signal,
|
|
11405
11482
|
})
|
|
11406
|
-
.catch(() => { })
|
|
11407
|
-
|
|
11408
|
-
|
|
11483
|
+
.catch(() => { }));
|
|
11484
|
+
}
|
|
11485
|
+
await Promise.all(resets);
|
|
11409
11486
|
}
|
|
11410
11487
|
finally {
|
|
11411
11488
|
clearTimeout(abortTimer);
|
|
@@ -11829,6 +11906,17 @@ let SharedLog = (() => {
|
|
|
11829
11906
|
if (!context.from) {
|
|
11830
11907
|
throw new Error("Missing from in update role message");
|
|
11831
11908
|
}
|
|
11909
|
+
if (!this.legacyReplicationInfoEnabled &&
|
|
11910
|
+
(msg instanceof RequestReplicationInfoMessage ||
|
|
11911
|
+
msg instanceof ResponseRoleMessage ||
|
|
11912
|
+
msg instanceof AllReplicatingSegmentsMessage ||
|
|
11913
|
+
msg instanceof AddedReplicationSegmentMessage ||
|
|
11914
|
+
msg instanceof StoppedReplicating)) {
|
|
11915
|
+
// These variants remain registered decode tombstones, but current logs
|
|
11916
|
+
// fail closed before leases, synchronizer work, liveness, watermarks or
|
|
11917
|
+
// mutations. Only an explicit pre-v10 compatibility open admits them.
|
|
11918
|
+
return;
|
|
11919
|
+
}
|
|
11832
11920
|
// Snapshot receive ownership before any async handler gets a chance to
|
|
11833
11921
|
// yield. Replication-info messages reach their branch only after the
|
|
11834
11922
|
// synchronizer declines them, and a U/S transition can happen meanwhile.
|
|
@@ -13243,14 +13331,24 @@ let SharedLog = (() => {
|
|
|
13243
13331
|
openingSession: receiveSession,
|
|
13244
13332
|
});
|
|
13245
13333
|
}
|
|
13246
|
-
else
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13334
|
+
else {
|
|
13335
|
+
const observed = this.observePeerSyncCapabilities({
|
|
13336
|
+
peerHash: receiveFromHash,
|
|
13337
|
+
capabilities: msg.capabilities,
|
|
13338
|
+
transportSession: capabilityTransportSession,
|
|
13339
|
+
timestamp: capabilityTimestamp,
|
|
13340
|
+
});
|
|
13341
|
+
if (observed && receiveSession?.phase === "open") {
|
|
13342
|
+
this.promoteReplicationInfoV2ReceiveCapability(context.from, receiveSession);
|
|
13343
|
+
}
|
|
13344
|
+
else if (observed && receiveSession === null) {
|
|
13345
|
+
// A capability can arrive before the sender's topic Subscribe after
|
|
13346
|
+
// reconnect. Ask that authenticated peer for its authoritative
|
|
13347
|
+
// subscriber snapshot; the resulting Subscribe creates the real
|
|
13348
|
+
// PeerSession and completes the symmetric capability handshake.
|
|
13349
|
+
// Never synthesize membership from capability traffic alone.
|
|
13350
|
+
this.requestSubscriberSnapshotForCapability(context.from);
|
|
13351
|
+
}
|
|
13254
13352
|
}
|
|
13255
13353
|
}
|
|
13256
13354
|
return;
|
|
@@ -13679,7 +13777,11 @@ let SharedLog = (() => {
|
|
|
13679
13777
|
return;
|
|
13680
13778
|
}
|
|
13681
13779
|
this._liveness.markReplicatorActivity(fromHash);
|
|
13682
|
-
|
|
13780
|
+
// A committed V2 announcement is applied progress: the peer answers,
|
|
13781
|
+
// so recovery re-solicitation may restart from the base interval.
|
|
13782
|
+
this.resetReplicationInfoV2RecoveryEscalation(fromHash);
|
|
13783
|
+
if (msg instanceof FullReplicationInfoV2Message &&
|
|
13784
|
+
this.legacyReplicationInfoEnabled) {
|
|
13683
13785
|
this.cancelReplicationInfoRequests(fromHash);
|
|
13684
13786
|
}
|
|
13685
13787
|
});
|
|
@@ -14183,9 +14285,30 @@ let SharedLog = (() => {
|
|
|
14183
14285
|
reject(new TimeoutError(`Timeout waiting for replicator ${key.hashcode()}`));
|
|
14184
14286
|
}, timeoutMs);
|
|
14185
14287
|
let requestAttempts = 0;
|
|
14288
|
+
let subscriberSnapshotInFlight;
|
|
14186
14289
|
const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
|
|
14187
14290
|
const maxRequestAttempts = this.waitForReplicatorRequestMaxAttempts ??
|
|
14188
14291
|
Math.max(WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS, Math.ceil(timeoutMs / requestIntervalMs));
|
|
14292
|
+
const requestSubscriberSnapshot = () => {
|
|
14293
|
+
if (subscriberSnapshotInFlight) {
|
|
14294
|
+
return;
|
|
14295
|
+
}
|
|
14296
|
+
subscriberSnapshotInFlight = Promise.resolve()
|
|
14297
|
+
.then(async () => {
|
|
14298
|
+
if (settled || this.closed) {
|
|
14299
|
+
return;
|
|
14300
|
+
}
|
|
14301
|
+
await this.node.services.pubsub.requestSubscribers(this.topic, key);
|
|
14302
|
+
})
|
|
14303
|
+
.catch((error) => {
|
|
14304
|
+
if (!isNotStartedError(error)) {
|
|
14305
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
14306
|
+
}
|
|
14307
|
+
})
|
|
14308
|
+
.finally(() => {
|
|
14309
|
+
subscriberSnapshotInFlight = undefined;
|
|
14310
|
+
});
|
|
14311
|
+
};
|
|
14189
14312
|
const requestReplicationInfo = () => {
|
|
14190
14313
|
if (settled || this.closed) {
|
|
14191
14314
|
return;
|
|
@@ -14194,17 +14317,37 @@ let SharedLog = (() => {
|
|
|
14194
14317
|
return;
|
|
14195
14318
|
}
|
|
14196
14319
|
requestAttempts++;
|
|
14197
|
-
this.
|
|
14198
|
-
.
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14320
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
14321
|
+
this.rpc
|
|
14322
|
+
.send(new RequestReplicationInfoMessage(), {
|
|
14323
|
+
mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
|
|
14324
|
+
})
|
|
14325
|
+
.catch((e) => {
|
|
14326
|
+
// Best-effort: missing peers / unopened RPC should not fail the wait logic.
|
|
14327
|
+
if (isNotStartedError(e)) {
|
|
14328
|
+
return;
|
|
14329
|
+
}
|
|
14330
|
+
logger.error(e?.toString?.() ?? String(e));
|
|
14331
|
+
});
|
|
14332
|
+
}
|
|
14333
|
+
else {
|
|
14334
|
+
const peerHash = key.hashcode();
|
|
14335
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
14336
|
+
if (peerSession?.phase === "open") {
|
|
14337
|
+
this._v2Receive.resumeParkedRequest({
|
|
14338
|
+
peerHash,
|
|
14339
|
+
peerSession,
|
|
14340
|
+
receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
|
|
14341
|
+
});
|
|
14205
14342
|
}
|
|
14206
|
-
|
|
14207
|
-
|
|
14343
|
+
else if (peerSession === null || peerSession.phase === "departing") {
|
|
14344
|
+
// A peer can be known to routing before its SharedLog topic
|
|
14345
|
+
// subscription has been observed. Legacy requests used to bootstrap
|
|
14346
|
+
// that case directly; V2 needs an authoritative Subscribe snapshot
|
|
14347
|
+
// before it can create a fenced PeerSession and request a Full.
|
|
14348
|
+
requestSubscriberSnapshot();
|
|
14349
|
+
}
|
|
14350
|
+
}
|
|
14208
14351
|
if (requestAttempts < maxRequestAttempts) {
|
|
14209
14352
|
requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
|
|
14210
14353
|
}
|
|
@@ -16016,12 +16159,114 @@ let SharedLog = (() => {
|
|
|
16016
16159
|
}
|
|
16017
16160
|
this._replicationInfoRequestByPeer.delete(peerHash);
|
|
16018
16161
|
}
|
|
16162
|
+
/**
|
|
16163
|
+
* Applied V2 progress from a peer (a committed Full/Added/Stopped, or a
|
|
16164
|
+
* rotated capability generation) proves the peer answers. Reset the
|
|
16165
|
+
* recovery scheduler's unpark escalation so a later stall restarts from
|
|
16166
|
+
* the base interval. Peer-session rotation resets implicitly: the recovery
|
|
16167
|
+
* scheduler creates a fresh per-session state.
|
|
16168
|
+
*/
|
|
16169
|
+
resetReplicationInfoV2RecoveryEscalation(peerHash) {
|
|
16170
|
+
const state = this._replicationInfoRequestByPeer.get(peerHash);
|
|
16171
|
+
if (!state || state.peerSession === undefined) {
|
|
16172
|
+
return;
|
|
16173
|
+
}
|
|
16174
|
+
state.attempts = 0;
|
|
16175
|
+
state.parkedSinceMs = undefined;
|
|
16176
|
+
}
|
|
16177
|
+
scheduleReplicationInfoV2Recovery(peer, replicationLifecycleController = this._instanceLifecycle
|
|
16178
|
+
?.membershipLifecycleController) {
|
|
16179
|
+
if (!replicationLifecycleController ||
|
|
16180
|
+
!this.isReplicationLifecycleActive(replicationLifecycleController)) {
|
|
16181
|
+
return;
|
|
16182
|
+
}
|
|
16183
|
+
const peerHash = peer.hashcode();
|
|
16184
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
16185
|
+
if (!peerSession || peerSession.phase !== "open") {
|
|
16186
|
+
return;
|
|
16187
|
+
}
|
|
16188
|
+
const requestStates = this._replicationInfoRequestByPeer;
|
|
16189
|
+
const existing = requestStates.get(peerHash);
|
|
16190
|
+
if (existing?.peerSession === peerSession) {
|
|
16191
|
+
return;
|
|
16192
|
+
}
|
|
16193
|
+
if (existing) {
|
|
16194
|
+
if (existing.timer) {
|
|
16195
|
+
clearTimeout(existing.timer);
|
|
16196
|
+
}
|
|
16197
|
+
requestStates.delete(peerHash);
|
|
16198
|
+
}
|
|
16199
|
+
const state = {
|
|
16200
|
+
attempts: 0,
|
|
16201
|
+
peerSession,
|
|
16202
|
+
};
|
|
16203
|
+
requestStates.set(peerHash, state);
|
|
16204
|
+
const cancel = () => {
|
|
16205
|
+
if (requestStates.get(peerHash) !== state) {
|
|
16206
|
+
return;
|
|
16207
|
+
}
|
|
16208
|
+
if (state.timer) {
|
|
16209
|
+
clearTimeout(state.timer);
|
|
16210
|
+
}
|
|
16211
|
+
requestStates.delete(peerHash);
|
|
16212
|
+
};
|
|
16213
|
+
const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
|
|
16214
|
+
const maxUnparkDelayMs = Math.max(intervalMs, REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_DELAY);
|
|
16215
|
+
const unparkDelayMs = () => Math.min(maxUnparkDelayMs, intervalMs *
|
|
16216
|
+
2 **
|
|
16217
|
+
Math.min(state.attempts, REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_EXPONENT));
|
|
16218
|
+
const arm = (delayMs) => {
|
|
16219
|
+
state.timer = setTimeout(tick, delayMs);
|
|
16220
|
+
state.timer.unref?.();
|
|
16221
|
+
};
|
|
16222
|
+
const tick = () => {
|
|
16223
|
+
if (!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
16224
|
+
peerSession.phase !== "open" ||
|
|
16225
|
+
!this._peerSessions.isCurrent(peerHash, peerSession)) {
|
|
16226
|
+
cancel();
|
|
16227
|
+
return;
|
|
16228
|
+
}
|
|
16229
|
+
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
16230
|
+
if (!this._v2Receive.isRequestParked({ peerHash, peerSession, receiveEpoch })) {
|
|
16231
|
+
// Active, or a bounded request cycle is still running its own
|
|
16232
|
+
// exponential retries. Keep polling for the next park.
|
|
16233
|
+
state.parkedSinceMs = undefined;
|
|
16234
|
+
arm(intervalMs);
|
|
16235
|
+
return;
|
|
16236
|
+
}
|
|
16237
|
+
const now = Date.now();
|
|
16238
|
+
if (state.parkedSinceMs === undefined) {
|
|
16239
|
+
state.parkedSinceMs = now;
|
|
16240
|
+
}
|
|
16241
|
+
const resumeAtMs = state.parkedSinceMs + unparkDelayMs();
|
|
16242
|
+
if (now < resumeAtMs) {
|
|
16243
|
+
arm(Math.max(50, resumeAtMs - now));
|
|
16244
|
+
return;
|
|
16245
|
+
}
|
|
16246
|
+
if (this._v2Receive.resumeParkedRequest({
|
|
16247
|
+
peerHash,
|
|
16248
|
+
peerSession,
|
|
16249
|
+
receiveEpoch,
|
|
16250
|
+
})) {
|
|
16251
|
+
// Fruitless until proven otherwise: escalate the next unpark wait.
|
|
16252
|
+
// Applied progress resets via resetReplicationInfoV2RecoveryEscalation.
|
|
16253
|
+
state.attempts++;
|
|
16254
|
+
state.parkedSinceMs = undefined;
|
|
16255
|
+
}
|
|
16256
|
+
arm(intervalMs);
|
|
16257
|
+
};
|
|
16258
|
+
tick();
|
|
16259
|
+
}
|
|
16019
16260
|
scheduleReplicationInfoRequests(peer, replicationLifecycleController = this._instanceLifecycle
|
|
16020
16261
|
?.membershipLifecycleController) {
|
|
16021
16262
|
if (!replicationLifecycleController ||
|
|
16022
16263
|
!this.isReplicationLifecycleActive(replicationLifecycleController)) {
|
|
16023
16264
|
return;
|
|
16024
16265
|
}
|
|
16266
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
16267
|
+
this.scheduleReplicationInfoV2Recovery(peer, replicationLifecycleController);
|
|
16268
|
+
return;
|
|
16269
|
+
}
|
|
16025
16270
|
const peerHash = peer.hashcode();
|
|
16026
16271
|
const requestStates = this._replicationInfoRequestByPeer;
|
|
16027
16272
|
if (requestStates.has(peerHash)) {
|
|
@@ -16072,7 +16317,7 @@ let SharedLog = (() => {
|
|
|
16072
16317
|
};
|
|
16073
16318
|
tick();
|
|
16074
16319
|
}
|
|
16075
|
-
async handleSubscriptionChange(publicKey, topics, subscribed, subscriptionEpoch) {
|
|
16320
|
+
async handleSubscriptionChange(publicKey, topics, subscribed, subscriptionEpoch, subscriptionTransportSession) {
|
|
16076
16321
|
if (!topics.includes(this.topic)) {
|
|
16077
16322
|
return;
|
|
16078
16323
|
}
|
|
@@ -16088,13 +16333,29 @@ let SharedLog = (() => {
|
|
|
16088
16333
|
if (!ownsSubscriptionEpoch()) {
|
|
16089
16334
|
return;
|
|
16090
16335
|
}
|
|
16336
|
+
// A reconnect can arrive before the previous exact-session recovery tick
|
|
16337
|
+
// observes its stale session. Retire that job synchronously so it cannot
|
|
16338
|
+
// suppress the replacement session's scheduler in the shared peer slot.
|
|
16339
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
16091
16340
|
// A destination stream is scoped to exactly one topic-subscription
|
|
16092
16341
|
// session. Abort the predecessor synchronously before either barrier can
|
|
16093
16342
|
// yield; a late queue completion must never enter the new session.
|
|
16094
16343
|
this._v2Receive.clearPeer(peerHash);
|
|
16095
16344
|
this._v2Send.clearPeer(peerHash);
|
|
16096
|
-
this._peerSyncCapabilitySessions.
|
|
16097
|
-
|
|
16345
|
+
const capabilityTransportSession = this._peerSyncCapabilitySessions.get(peerHash);
|
|
16346
|
+
const canInheritCapability = subscribed &&
|
|
16347
|
+
(subscriptionTransportSession !== undefined
|
|
16348
|
+
? capabilityTransportSession === subscriptionTransportSession
|
|
16349
|
+
: !expectedSubscriptionEpoch.hasPredecessor);
|
|
16350
|
+
if (!canInheritCapability) {
|
|
16351
|
+
// Departures and successor openings revoke the signed transport binding.
|
|
16352
|
+
// A live successor may inherit a capability that arrived just before its
|
|
16353
|
+
// Subscribe only when both signed frames belong to the same transport
|
|
16354
|
+
// generation. Snapshot fallbacks lack that proof, so only their first
|
|
16355
|
+
// opening may inherit pre-opening capability state.
|
|
16356
|
+
this._peerSyncCapabilitySessions.delete(peerHash);
|
|
16357
|
+
this._peerSyncCapabilityTimestamps.delete(peerHash);
|
|
16358
|
+
}
|
|
16098
16359
|
if (subscribed) {
|
|
16099
16360
|
const pendingOpeningCapabilities = this._openingSyncCapabilitiesByPeer.get(peerHash);
|
|
16100
16361
|
if (pendingOpeningCapabilities &&
|
|
@@ -16219,6 +16480,14 @@ let SharedLog = (() => {
|
|
|
16219
16480
|
receiveEpoch,
|
|
16220
16481
|
signal: replicationLifecycleController.signal,
|
|
16221
16482
|
});
|
|
16483
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
16484
|
+
// Current logs have no legacy startup work to order ahead of RequestV2.
|
|
16485
|
+
// Releasing is synchronous and exact-session fenced; the ACK may still
|
|
16486
|
+
// arrive later and promote readiness through the coordinator.
|
|
16487
|
+
localCapabilityAdvertisement.releaseLegacyBarrier();
|
|
16488
|
+
this.scheduleReplicationInfoV2Recovery(publicKey, replicationLifecycleController);
|
|
16489
|
+
return;
|
|
16490
|
+
}
|
|
16222
16491
|
try {
|
|
16223
16492
|
let replicationSegments;
|
|
16224
16493
|
try {
|
|
@@ -17275,7 +17544,7 @@ let SharedLog = (() => {
|
|
|
17275
17544
|
this.remoteBlocks.onReachable(evt.detail.from);
|
|
17276
17545
|
this._peerSessions.blockReplicationInfo(fromHash);
|
|
17277
17546
|
this.invalidateSharedLogTopicSubscribersCache();
|
|
17278
|
-
await this.handleSubscriptionChange(evt.detail.from, evt.detail.topics, true, subscriptionEpoch);
|
|
17547
|
+
await this.handleSubscriptionChange(evt.detail.from, evt.detail.topics, true, subscriptionEpoch, evt.detail.session);
|
|
17279
17548
|
}
|
|
17280
17549
|
async rebalanceParticipation(ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(), rebalanceParticipationDebounced = this.rebalanceParticipationDebounced) {
|
|
17281
17550
|
// Stage 3: the lifecycle owns all three identity terms. `lifecycle` may
|