@peerbit/shared-log 16.0.25 → 16.0.26
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 +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +85 -19
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +22 -4
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +137 -25
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/package.json +10 -10
- package/src/index.ts +104 -16
- package/src/replication-info-v2-receive.ts +201 -32
package/src/index.ts
CHANGED
|
@@ -4087,10 +4087,15 @@ export class SharedLog<
|
|
|
4087
4087
|
: 0),
|
|
4088
4088
|
}),
|
|
4089
4089
|
{
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4090
|
+
// The transient rearm is only a bounded recovery hint. The ensuing
|
|
4091
|
+
// authenticated Full/Applied exchange is the readiness proof, so do not
|
|
4092
|
+
// let an ACK-delivery promise park the recovery scheduler itself.
|
|
4093
|
+
mode: properties.requestRemoteFullRearm
|
|
4094
|
+
? new SilentDelivery({ redundancy: 1, to: [properties.target] })
|
|
4095
|
+
: new AcknowledgeDelivery({
|
|
4096
|
+
redundancy: 1,
|
|
4097
|
+
to: [properties.target],
|
|
4098
|
+
}),
|
|
4094
4099
|
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
4095
4100
|
signal: properties.signal,
|
|
4096
4101
|
},
|
|
@@ -24381,6 +24386,11 @@ export class SharedLog<
|
|
|
24381
24386
|
private nudgePersistedReceiptPeerReadiness(
|
|
24382
24387
|
publicKey: PublicSignKey,
|
|
24383
24388
|
signal: AbortSignal,
|
|
24389
|
+
rearmCurrentUnconfirmed?: {
|
|
24390
|
+
peerSession: PeerSession;
|
|
24391
|
+
receiveEpoch: object | null;
|
|
24392
|
+
capabilitySession: bigint;
|
|
24393
|
+
},
|
|
24384
24394
|
): void {
|
|
24385
24395
|
if (this.closed || signal.aborted) return;
|
|
24386
24396
|
const peerHash = publicKey.hashcode();
|
|
@@ -24416,11 +24426,20 @@ export class SharedLog<
|
|
|
24416
24426
|
const receiptTarget = this.persistedReceiptPeerSession(peerHash);
|
|
24417
24427
|
if (
|
|
24418
24428
|
receiptTarget &&
|
|
24419
|
-
!this._v2Send.hasCurrentStateForPeer({
|
|
24429
|
+
(!this._v2Send.hasCurrentStateForPeer({
|
|
24420
24430
|
peerHash,
|
|
24421
24431
|
peerSession: receiptTarget.peerSession,
|
|
24422
24432
|
receiverTransportSession: receiptTarget.capabilitySession,
|
|
24423
|
-
})
|
|
24433
|
+
}) ||
|
|
24434
|
+
(rearmCurrentUnconfirmed?.peerSession === receiptTarget.peerSession &&
|
|
24435
|
+
rearmCurrentUnconfirmed.receiveEpoch === receiptTarget.receiveEpoch &&
|
|
24436
|
+
rearmCurrentUnconfirmed.capabilitySession ===
|
|
24437
|
+
receiptTarget.capabilitySession &&
|
|
24438
|
+
!this._v2Send.isLatestConfirmedForPeer({
|
|
24439
|
+
peerHash,
|
|
24440
|
+
peerSession: receiptTarget.peerSession,
|
|
24441
|
+
receiverTransportSession: receiptTarget.capabilitySession,
|
|
24442
|
+
})))
|
|
24424
24443
|
) {
|
|
24425
24444
|
this._v2Receive.reAdvertiseLocalCapabilityForRemoteFull({
|
|
24426
24445
|
peerHash,
|
|
@@ -24563,7 +24582,9 @@ export class SharedLog<
|
|
|
24563
24582
|
* Wait for a public key's current (or replacement) connection generation to
|
|
24564
24583
|
* become persisted-receipt ready. Transition listeners are installed before
|
|
24565
24584
|
* the first asynchronous inspection, and a bounded recovery tick repairs
|
|
24566
|
-
* missed subscriber/capability wakes without retaining stale PeerSessions.
|
|
24585
|
+
* missed subscriber/capability wakes without retaining stale PeerSessions. A
|
|
24586
|
+
* current sender gets one normal confirmation interval before an exact-
|
|
24587
|
+
* generation watchdog requests a bounded Full rearm.
|
|
24567
24588
|
* This waiter is advisory only; the following persisted delivery remains the
|
|
24568
24589
|
* operation that proves the requested remote durability quorum.
|
|
24569
24590
|
*/
|
|
@@ -24621,6 +24642,14 @@ export class SharedLog<
|
|
|
24621
24642
|
let rerun = false;
|
|
24622
24643
|
let recoveryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
24623
24644
|
let confirmationController: AbortController | undefined;
|
|
24645
|
+
let confirmationRecoveryTarget:
|
|
24646
|
+
| {
|
|
24647
|
+
peerSession: PeerSession;
|
|
24648
|
+
receiveEpoch: object | null;
|
|
24649
|
+
capabilitySession: bigint;
|
|
24650
|
+
notBefore: number;
|
|
24651
|
+
}
|
|
24652
|
+
| undefined;
|
|
24624
24653
|
let lastSnapshot: PersistedReceiptPeerReadiness | undefined;
|
|
24625
24654
|
const createTimeoutError = () => {
|
|
24626
24655
|
const suffix = lastSnapshot
|
|
@@ -24651,6 +24680,7 @@ export class SharedLog<
|
|
|
24651
24680
|
new AbortError("Persisted-receipt readiness generation changed"),
|
|
24652
24681
|
);
|
|
24653
24682
|
confirmationController = undefined;
|
|
24683
|
+
confirmationRecoveryTarget = undefined;
|
|
24654
24684
|
operationController.abort(
|
|
24655
24685
|
new AbortError("Persisted-receipt readiness wait settled"),
|
|
24656
24686
|
);
|
|
@@ -24706,7 +24736,14 @@ export class SharedLog<
|
|
|
24706
24736
|
recoveryTimer = setTimeout(() => {
|
|
24707
24737
|
recoveryTimer = undefined;
|
|
24708
24738
|
if (!continueWait()) return;
|
|
24709
|
-
|
|
24739
|
+
const recoveryTarget = confirmationRecoveryTarget;
|
|
24740
|
+
this.nudgePersistedReceiptPeerReadiness(
|
|
24741
|
+
key,
|
|
24742
|
+
operationSignal,
|
|
24743
|
+
recoveryTarget && Date.now() >= recoveryTarget.notBefore
|
|
24744
|
+
? recoveryTarget
|
|
24745
|
+
: undefined,
|
|
24746
|
+
);
|
|
24710
24747
|
if (checkInFlight) {
|
|
24711
24748
|
// Confirmation can legitimately span several recovery intervals.
|
|
24712
24749
|
// Keep repairing the exact current generation without aborting its
|
|
@@ -24719,6 +24756,34 @@ export class SharedLog<
|
|
|
24719
24756
|
}, recoveryDelayMs);
|
|
24720
24757
|
recoveryTimer.unref?.();
|
|
24721
24758
|
};
|
|
24759
|
+
const prepareConfirmationRecovery = () => {
|
|
24760
|
+
const target = this.persistedReceiptPeerSession(peerHash);
|
|
24761
|
+
if (!target) {
|
|
24762
|
+
confirmationRecoveryTarget = undefined;
|
|
24763
|
+
return undefined;
|
|
24764
|
+
}
|
|
24765
|
+
const retained = confirmationRecoveryTarget;
|
|
24766
|
+
if (
|
|
24767
|
+
retained?.peerSession !== target.peerSession ||
|
|
24768
|
+
retained.receiveEpoch !== target.receiveEpoch ||
|
|
24769
|
+
retained.capabilitySession !== target.capabilitySession
|
|
24770
|
+
) {
|
|
24771
|
+
confirmationRecoveryTarget = {
|
|
24772
|
+
peerSession: target.peerSession,
|
|
24773
|
+
receiveEpoch: target.receiveEpoch,
|
|
24774
|
+
capabilitySession: target.capabilitySession,
|
|
24775
|
+
notBefore: Date.now() + recoveryDelayMs,
|
|
24776
|
+
};
|
|
24777
|
+
}
|
|
24778
|
+
const recoveryTarget = confirmationRecoveryTarget!;
|
|
24779
|
+
this.nudgePersistedReceiptPeerReadiness(
|
|
24780
|
+
key,
|
|
24781
|
+
operationSignal,
|
|
24782
|
+
Date.now() >= recoveryTarget.notBefore ? recoveryTarget : undefined,
|
|
24783
|
+
);
|
|
24784
|
+
armRecoveryTick();
|
|
24785
|
+
return target;
|
|
24786
|
+
};
|
|
24722
24787
|
const runCheck = async () => {
|
|
24723
24788
|
checkScheduled = false;
|
|
24724
24789
|
if (!continueWait()) return;
|
|
@@ -24735,8 +24800,9 @@ export class SharedLog<
|
|
|
24735
24800
|
);
|
|
24736
24801
|
lastSnapshot = snapshot;
|
|
24737
24802
|
if (!continueWait()) return;
|
|
24738
|
-
if (rerun) return;
|
|
24739
24803
|
if (snapshot.status === "ready") {
|
|
24804
|
+
confirmationRecoveryTarget = undefined;
|
|
24805
|
+
if (rerun) return;
|
|
24740
24806
|
// A wake observed while the asynchronous inspection was running may
|
|
24741
24807
|
// already have invalidated this snapshot. Drain that coalesced wake
|
|
24742
24808
|
// before publishing readiness.
|
|
@@ -24747,15 +24813,17 @@ export class SharedLog<
|
|
|
24747
24813
|
snapshot.status === "pending" &&
|
|
24748
24814
|
snapshot.reason === "replication-confirmation-pending"
|
|
24749
24815
|
) {
|
|
24750
|
-
|
|
24816
|
+
// Retain one watchdog deadline for the exact generation across
|
|
24817
|
+
// coalesced inspection wakes. A sub-interval event stream must drive,
|
|
24818
|
+
// rather than indefinitely postpone, the mature recovery nudge.
|
|
24819
|
+
const target = prepareConfirmationRecovery();
|
|
24820
|
+
if (rerun) return;
|
|
24751
24821
|
if (target) {
|
|
24752
|
-
// A confirmation waiter cannot create an outbound V2 stream when
|
|
24753
|
-
// that exact session state is missing. Nudge first, then keep the
|
|
24754
|
-
// recovery tick alive while the single confirmation wait remains.
|
|
24755
|
-
this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
|
|
24756
|
-
armRecoveryTick();
|
|
24757
24822
|
const currentConfirmationController = new AbortController();
|
|
24758
24823
|
confirmationController = currentConfirmationController;
|
|
24824
|
+
const currentConfirmationRecoveryTarget =
|
|
24825
|
+
confirmationRecoveryTarget;
|
|
24826
|
+
let confirmationSucceeded = false;
|
|
24759
24827
|
try {
|
|
24760
24828
|
await this._v2Send.confirmLatestForPeer(
|
|
24761
24829
|
{
|
|
@@ -24771,6 +24839,7 @@ export class SharedLog<
|
|
|
24771
24839
|
]),
|
|
24772
24840
|
},
|
|
24773
24841
|
);
|
|
24842
|
+
confirmationSucceeded = true;
|
|
24774
24843
|
} catch (error) {
|
|
24775
24844
|
if (!continueWait()) return;
|
|
24776
24845
|
if (!(error instanceof AbortError)) {
|
|
@@ -24781,6 +24850,12 @@ export class SharedLog<
|
|
|
24781
24850
|
if (confirmationController === currentConfirmationController) {
|
|
24782
24851
|
confirmationController = undefined;
|
|
24783
24852
|
}
|
|
24853
|
+
if (
|
|
24854
|
+
confirmationSucceeded &&
|
|
24855
|
+
confirmationRecoveryTarget === currentConfirmationRecoveryTarget
|
|
24856
|
+
) {
|
|
24857
|
+
confirmationRecoveryTarget = undefined;
|
|
24858
|
+
}
|
|
24784
24859
|
}
|
|
24785
24860
|
if (!continueWait()) return;
|
|
24786
24861
|
snapshot = await this.inspectPersistedReceiptPeerReadiness(
|
|
@@ -24790,13 +24865,26 @@ export class SharedLog<
|
|
|
24790
24865
|
);
|
|
24791
24866
|
lastSnapshot = snapshot;
|
|
24792
24867
|
if (!continueWait()) return;
|
|
24793
|
-
if (rerun) return;
|
|
24794
24868
|
if (snapshot.status === "ready") {
|
|
24869
|
+
confirmationRecoveryTarget = undefined;
|
|
24870
|
+
if (rerun) return;
|
|
24795
24871
|
resolve(snapshot);
|
|
24796
24872
|
return;
|
|
24797
24873
|
}
|
|
24874
|
+
if (
|
|
24875
|
+
snapshot.status === "pending" &&
|
|
24876
|
+
snapshot.reason === "replication-confirmation-pending"
|
|
24877
|
+
) {
|
|
24878
|
+
prepareConfirmationRecovery();
|
|
24879
|
+
} else {
|
|
24880
|
+
confirmationRecoveryTarget = undefined;
|
|
24881
|
+
}
|
|
24882
|
+
if (rerun) return;
|
|
24798
24883
|
}
|
|
24884
|
+
} else {
|
|
24885
|
+
confirmationRecoveryTarget = undefined;
|
|
24799
24886
|
}
|
|
24887
|
+
if (rerun) return;
|
|
24800
24888
|
if (!continueWait()) return;
|
|
24801
24889
|
this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
|
|
24802
24890
|
} catch (error) {
|
|
@@ -25,6 +25,11 @@ const REQUIRED_SENDER_CAPABILITIES =
|
|
|
25
25
|
const DEFAULT_REQUEST_RETRY_MS = 1_000;
|
|
26
26
|
const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
|
|
27
27
|
const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
|
|
28
|
+
const DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS = 2_000;
|
|
29
|
+
const DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS = 5_000;
|
|
30
|
+
const MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION = 2;
|
|
31
|
+
const MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL = 64;
|
|
32
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
28
33
|
const MAX_U64 = (1n << 64n) - 1n;
|
|
29
34
|
const MAX_BACKOFF_EXPONENT = 20;
|
|
30
35
|
|
|
@@ -113,8 +118,13 @@ export type ReplicationInfoV2LocalCapabilityAdvertisement = {
|
|
|
113
118
|
};
|
|
114
119
|
|
|
115
120
|
type ReplicationInfoV2RemoteFullRearm = {
|
|
116
|
-
|
|
121
|
+
peerHash: string;
|
|
122
|
+
receiveEpoch: object | null;
|
|
123
|
+
receiverTransportSession: bigint;
|
|
124
|
+
nextAttemptAt: number;
|
|
125
|
+
outstandingAttempts: number;
|
|
117
126
|
inFlight?: Promise<void>;
|
|
127
|
+
controller?: AbortController;
|
|
118
128
|
};
|
|
119
129
|
|
|
120
130
|
export type ReplicationInfoV2LocalCapabilityRefresh = {
|
|
@@ -196,6 +206,9 @@ export type ReplicationInfoV2ReceiveDeps = {
|
|
|
196
206
|
requestRetryMs?: number;
|
|
197
207
|
maxRequestRetryMs?: number;
|
|
198
208
|
requestMaxAttempts?: number;
|
|
209
|
+
remoteFullRearmAttemptTimeoutMs?: number;
|
|
210
|
+
remoteFullRearmCooldownMs?: number;
|
|
211
|
+
maxRemoteFullRearmOutstandingGlobal?: number;
|
|
199
212
|
};
|
|
200
213
|
|
|
201
214
|
/**
|
|
@@ -224,12 +237,17 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
224
237
|
ReplicationInfoV2LocalCapabilityAdvertisement
|
|
225
238
|
>;
|
|
226
239
|
_remoteFullRearmBySession!: WeakMap<object, ReplicationInfoV2RemoteFullRearm>;
|
|
240
|
+
_remoteFullRearmsInFlight!: Set<ReplicationInfoV2RemoteFullRearm>;
|
|
241
|
+
_remoteFullRearmOutstanding!: Set<object>;
|
|
227
242
|
_reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
|
|
228
243
|
|
|
229
244
|
private readonly now: () => number;
|
|
230
245
|
private readonly requestRetryMs: number;
|
|
231
246
|
private readonly maxRequestRetryMs: number;
|
|
232
247
|
private readonly requestMaxAttempts: number;
|
|
248
|
+
private readonly remoteFullRearmAttemptTimeoutMs: number;
|
|
249
|
+
private readonly remoteFullRearmCooldownMs: number;
|
|
250
|
+
private readonly maxRemoteFullRearmOutstandingGlobal: number;
|
|
233
251
|
|
|
234
252
|
constructor(private readonly deps: ReplicationInfoV2ReceiveDeps) {
|
|
235
253
|
this.now = deps.now ?? Date.now;
|
|
@@ -245,12 +263,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
245
263
|
1,
|
|
246
264
|
Math.floor(deps.requestMaxAttempts ?? DEFAULT_REQUEST_MAX_ATTEMPTS),
|
|
247
265
|
);
|
|
266
|
+
this.remoteFullRearmAttemptTimeoutMs = Math.max(
|
|
267
|
+
1,
|
|
268
|
+
Math.min(
|
|
269
|
+
MAX_TIMER_MS,
|
|
270
|
+
Math.floor(
|
|
271
|
+
deps.remoteFullRearmAttemptTimeoutMs ??
|
|
272
|
+
DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS,
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
this.remoteFullRearmCooldownMs = Math.max(
|
|
277
|
+
this.requestRetryMs,
|
|
278
|
+
Math.min(
|
|
279
|
+
MAX_TIMER_MS,
|
|
280
|
+
Math.floor(
|
|
281
|
+
deps.remoteFullRearmCooldownMs ??
|
|
282
|
+
DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS,
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
// The injected value is a test seam that may only tighten this lifetime
|
|
287
|
+
// resource bound, never widen it.
|
|
288
|
+
this.maxRemoteFullRearmOutstandingGlobal = Math.min(
|
|
289
|
+
MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
|
|
290
|
+
Math.max(
|
|
291
|
+
1,
|
|
292
|
+
Math.floor(
|
|
293
|
+
deps.maxRemoteFullRearmOutstandingGlobal ??
|
|
294
|
+
MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
|
|
295
|
+
),
|
|
296
|
+
),
|
|
297
|
+
);
|
|
248
298
|
this._receiveStates = new Map();
|
|
249
299
|
this._cutoverPeerSessions = new WeakSet();
|
|
250
300
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
251
301
|
this._localCapabilityContextBySession = new WeakMap();
|
|
252
302
|
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
253
303
|
this._remoteFullRearmBySession = new WeakMap();
|
|
304
|
+
this._remoteFullRearmsInFlight = new Set();
|
|
305
|
+
this._remoteFullRearmOutstanding = new Set();
|
|
254
306
|
this._reservedAdmissionsByPeer = new Map();
|
|
255
307
|
}
|
|
256
308
|
|
|
@@ -262,10 +314,19 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
262
314
|
this._localCapabilityContextBySession = new WeakMap();
|
|
263
315
|
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
264
316
|
this._remoteFullRearmBySession = new WeakMap();
|
|
317
|
+
this._remoteFullRearmsInFlight = new Set();
|
|
265
318
|
this._reservedAdmissionsByPeer = new Map();
|
|
266
319
|
}
|
|
267
320
|
|
|
268
321
|
clearForClose(): void {
|
|
322
|
+
for (const rearm of this._remoteFullRearmsInFlight ?? []) {
|
|
323
|
+
rearm.controller?.abort(
|
|
324
|
+
new Error(
|
|
325
|
+
"Replication-info V2 receiver closed during remote Full rearm",
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
this._remoteFullRearmsInFlight?.clear();
|
|
269
330
|
for (const advertisement of [
|
|
270
331
|
...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
|
|
271
332
|
]) {
|
|
@@ -298,10 +359,28 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
298
359
|
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
299
360
|
this._cutoverPeerSessions.delete(state.peerSession);
|
|
300
361
|
}
|
|
362
|
+
if (!expectedSession) {
|
|
363
|
+
for (const rearm of this._remoteFullRearmsInFlight) {
|
|
364
|
+
if (rearm.peerHash === peerHash) {
|
|
365
|
+
rearm.controller?.abort(
|
|
366
|
+
new Error("Replication-info V2 peer cleared during rearm"),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const rearmSession =
|
|
372
|
+
expectedSession ?? state?.peerSession ?? advertisement?.peerSession;
|
|
373
|
+
if (rearmSession) {
|
|
374
|
+
this._remoteFullRearmBySession
|
|
375
|
+
.get(rearmSession)
|
|
376
|
+
?.controller?.abort(
|
|
377
|
+
new Error("Replication-info V2 peer session cleared during rearm"),
|
|
378
|
+
);
|
|
379
|
+
this._remoteFullRearmBySession.delete(rearmSession);
|
|
380
|
+
}
|
|
301
381
|
if (expectedSession) {
|
|
302
382
|
this._localCapabilityReadyBySession.delete(expectedSession);
|
|
303
383
|
this._localCapabilityContextBySession.delete(expectedSession);
|
|
304
|
-
this._remoteFullRearmBySession.delete(expectedSession);
|
|
305
384
|
this._cutoverPeerSessions.delete(expectedSession);
|
|
306
385
|
}
|
|
307
386
|
}
|
|
@@ -643,9 +722,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
643
722
|
* stream that was lost while both directions otherwise remained current.
|
|
644
723
|
*
|
|
645
724
|
* This deliberately does not mutate the steady advertisement worker. One
|
|
646
|
-
* attempt is coalesced per PeerSession and bound to
|
|
647
|
-
*
|
|
648
|
-
*
|
|
725
|
+
* attempt is coalesced per exact PeerSession/receive generation and bound to
|
|
726
|
+
* its own deadline as well as the caller's signal. A cooldown avoids rotating
|
|
727
|
+
* a healthy in-flight challenge on every recovery tick. The remote rotates
|
|
728
|
+
* only when its receive state is active; later hints during resync only nudge
|
|
729
|
+
* that same bounded request generation. At most two transports that disregard
|
|
730
|
+
* abort remain outstanding for one exact session (64 across this coordinator's
|
|
731
|
+
* lifetime); reaching either cap stops new hints while the persisted-readiness
|
|
732
|
+
* gate remains fail closed.
|
|
649
733
|
*/
|
|
650
734
|
reAdvertiseLocalCapabilityForRemoteFull(properties: {
|
|
651
735
|
peerHash: string;
|
|
@@ -679,47 +763,130 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
679
763
|
}
|
|
680
764
|
|
|
681
765
|
const now = this.now();
|
|
766
|
+
const receiverTransportSession = ready.receiverTransportSession;
|
|
682
767
|
let rearm = this._remoteFullRearmBySession.get(properties.peerSession);
|
|
768
|
+
if (
|
|
769
|
+
rearm &&
|
|
770
|
+
(rearm.peerHash !== properties.peerHash ||
|
|
771
|
+
rearm.receiveEpoch !== properties.receiveEpoch ||
|
|
772
|
+
rearm.receiverTransportSession !== receiverTransportSession)
|
|
773
|
+
) {
|
|
774
|
+
rearm.controller?.abort(
|
|
775
|
+
new Error("Replication-info V2 remote Full rearm generation changed"),
|
|
776
|
+
);
|
|
777
|
+
rearm = undefined;
|
|
778
|
+
}
|
|
683
779
|
if (rearm?.inFlight) {
|
|
684
780
|
return true;
|
|
685
781
|
}
|
|
782
|
+
if (rearm !== undefined && now < rearm.nextAttemptAt) {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
686
785
|
if (
|
|
687
|
-
rearm
|
|
688
|
-
|
|
786
|
+
(rearm?.outstandingAttempts ?? 0) >=
|
|
787
|
+
MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION ||
|
|
788
|
+
this._remoteFullRearmOutstanding.size >=
|
|
789
|
+
this.maxRemoteFullRearmOutstandingGlobal
|
|
689
790
|
) {
|
|
690
791
|
return true;
|
|
691
792
|
}
|
|
692
793
|
if (!rearm) {
|
|
693
|
-
rearm = {
|
|
794
|
+
rearm = {
|
|
795
|
+
peerHash: properties.peerHash,
|
|
796
|
+
receiveEpoch: properties.receiveEpoch,
|
|
797
|
+
receiverTransportSession,
|
|
798
|
+
nextAttemptAt: now + this.remoteFullRearmCooldownMs,
|
|
799
|
+
outstandingAttempts: 0,
|
|
800
|
+
};
|
|
694
801
|
this._remoteFullRearmBySession.set(properties.peerSession, rearm);
|
|
695
802
|
} else {
|
|
696
|
-
rearm.
|
|
697
|
-
}
|
|
803
|
+
rearm.nextAttemptAt = now + this.remoteFullRearmCooldownMs;
|
|
804
|
+
}
|
|
805
|
+
const attemptState = rearm;
|
|
806
|
+
const outstandingReservation = {};
|
|
807
|
+
attemptState.outstandingAttempts++;
|
|
808
|
+
this._remoteFullRearmOutstanding.add(outstandingReservation);
|
|
809
|
+
|
|
810
|
+
const attemptController = new AbortController();
|
|
811
|
+
const operationSignal = attemptController.signal;
|
|
812
|
+
attemptState.controller = attemptController;
|
|
813
|
+
this._remoteFullRearmsInFlight.add(attemptState);
|
|
814
|
+
const sourceSignals = Array.from(
|
|
815
|
+
new Set([context.lifecycleSignal, properties.signal]),
|
|
816
|
+
);
|
|
817
|
+
const onSourceAbort = (event: Event) => {
|
|
818
|
+
const source = event.currentTarget as AbortSignal;
|
|
819
|
+
attemptController.abort(source.reason);
|
|
820
|
+
};
|
|
821
|
+
for (const source of sourceSignals) {
|
|
822
|
+
source.addEventListener("abort", onSourceAbort, { once: true });
|
|
823
|
+
}
|
|
824
|
+
const attemptTimer = setTimeout(
|
|
825
|
+
() =>
|
|
826
|
+
attemptController.abort(
|
|
827
|
+
new Error("Replication-info V2 remote Full rearm attempt timed out"),
|
|
828
|
+
),
|
|
829
|
+
this.remoteFullRearmAttemptTimeoutMs,
|
|
830
|
+
);
|
|
831
|
+
attemptTimer.unref?.();
|
|
832
|
+
let releasedOutstanding = false;
|
|
833
|
+
const releaseOutstanding = () => {
|
|
834
|
+
if (releasedOutstanding) return;
|
|
835
|
+
releasedOutstanding = true;
|
|
836
|
+
this._remoteFullRearmOutstanding.delete(outstandingReservation);
|
|
837
|
+
attemptState.outstandingAttempts--;
|
|
838
|
+
};
|
|
839
|
+
const refresh = Promise.resolve().then(() => {
|
|
840
|
+
if (operationSignal.aborted) {
|
|
841
|
+
throw operationSignal.reason;
|
|
842
|
+
}
|
|
843
|
+
return this.deps.refreshLocalCapability({
|
|
844
|
+
peerHash: properties.peerHash,
|
|
845
|
+
target: context.target,
|
|
846
|
+
peerSession: properties.peerSession,
|
|
847
|
+
receiveEpoch: properties.receiveEpoch,
|
|
848
|
+
signal: operationSignal,
|
|
849
|
+
requestRemoteFullRearm: true,
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
void refresh.then(releaseOutstanding, releaseOutstanding);
|
|
698
853
|
|
|
699
|
-
const operationSignal = AbortSignal.any([
|
|
700
|
-
context.lifecycleSignal,
|
|
701
|
-
properties.signal,
|
|
702
|
-
]);
|
|
703
854
|
let operation: Promise<void>;
|
|
704
855
|
const detach = () => {
|
|
705
|
-
|
|
706
|
-
|
|
856
|
+
clearTimeout(attemptTimer);
|
|
857
|
+
operationSignal.removeEventListener("abort", detach);
|
|
858
|
+
for (const source of sourceSignals) {
|
|
859
|
+
source.removeEventListener("abort", onSourceAbort);
|
|
860
|
+
}
|
|
861
|
+
if (attemptState.inFlight === operation) {
|
|
862
|
+
this._remoteFullRearmsInFlight.delete(attemptState);
|
|
863
|
+
attemptState.inFlight = undefined;
|
|
864
|
+
attemptState.controller = undefined;
|
|
707
865
|
}
|
|
708
866
|
};
|
|
709
|
-
|
|
710
|
-
|
|
867
|
+
const raceWithAttemptSignal = <T>(promise: Promise<T>): Promise<T> =>
|
|
868
|
+
new Promise<T>((resolve, reject) => {
|
|
869
|
+
const onAbort = () => {
|
|
870
|
+
operationSignal.removeEventListener("abort", onAbort);
|
|
871
|
+
reject(operationSignal.reason);
|
|
872
|
+
};
|
|
873
|
+
operationSignal.addEventListener("abort", onAbort, { once: true });
|
|
711
874
|
if (operationSignal.aborted) {
|
|
712
|
-
|
|
875
|
+
onAbort();
|
|
876
|
+
return;
|
|
713
877
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
878
|
+
promise.then(
|
|
879
|
+
(value) => {
|
|
880
|
+
operationSignal.removeEventListener("abort", onAbort);
|
|
881
|
+
resolve(value);
|
|
882
|
+
},
|
|
883
|
+
(error) => {
|
|
884
|
+
operationSignal.removeEventListener("abort", onAbort);
|
|
885
|
+
reject(error);
|
|
886
|
+
},
|
|
887
|
+
);
|
|
888
|
+
});
|
|
889
|
+
operation = raceWithAttemptSignal(refresh)
|
|
723
890
|
.then(() => undefined)
|
|
724
891
|
.catch((error) => {
|
|
725
892
|
if (
|
|
@@ -735,13 +902,15 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
735
902
|
}
|
|
736
903
|
})
|
|
737
904
|
.finally(() => {
|
|
738
|
-
operationSignal.removeEventListener("abort", detach);
|
|
739
905
|
detach();
|
|
740
906
|
});
|
|
741
|
-
|
|
907
|
+
attemptState.inFlight = operation;
|
|
742
908
|
operationSignal.addEventListener("abort", detach, { once: true });
|
|
743
|
-
|
|
744
|
-
|
|
909
|
+
for (const source of sourceSignals) {
|
|
910
|
+
if (source.aborted) {
|
|
911
|
+
attemptController.abort(source.reason);
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
745
914
|
}
|
|
746
915
|
void operation;
|
|
747
916
|
return true;
|