@peerbit/shared-log 13.2.32 → 13.2.34
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 +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +613 -123
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-binding.d.ts +16 -0
- package/dist/src/replication-info-v2-binding.d.ts.map +1 -0
- package/dist/src/replication-info-v2-binding.js +30 -0
- package/dist/src/replication-info-v2-binding.js.map +1 -0
- package/dist/src/replication-info-v2-receive.d.ts +250 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -0
- package/dist/src/replication-info-v2-receive.js +1089 -0
- package/dist/src/replication-info-v2-receive.js.map +1 -0
- package/dist/src/replication-info-v2-send.d.ts +25 -17
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +170 -86
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +897 -260
- package/src/replication-info-v2-binding.ts +44 -0
- package/src/replication-info-v2-receive.ts +1577 -0
- package/src/replication-info-v2-send.ts +223 -109
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type PublicSignKey, randomBytes, sha256Sync } from "@peerbit/crypto";
|
|
1
|
+
import { type PublicSignKey, randomBytes } from "@peerbit/crypto";
|
|
3
2
|
import { logger as loggerFn } from "@peerbit/logger";
|
|
4
3
|
import type { RPC } from "@peerbit/rpc";
|
|
5
4
|
import {
|
|
@@ -8,7 +7,7 @@ import {
|
|
|
8
7
|
} from "@peerbit/stream-interface";
|
|
9
8
|
import type { TransportMessage } from "./message.js";
|
|
10
9
|
import type { ReplicationRangeIndexable } from "./ranges.js";
|
|
11
|
-
import {
|
|
10
|
+
import { deriveReplicationInfoV2ReceiverBinding } from "./replication-info-v2-binding.js";
|
|
12
11
|
import {
|
|
13
12
|
AddedReplicationInfoV2Message,
|
|
14
13
|
AddedReplicationSegmentMessage,
|
|
@@ -22,46 +21,11 @@ import {
|
|
|
22
21
|
const logger = loggerFn("peerbit:shared-log:replication-info-v2-send");
|
|
23
22
|
|
|
24
23
|
const MAX_U64 = (1n << 64n) - 1n;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const lengthPrefixed = (bytes: Uint8Array): Uint8Array => {
|
|
30
|
-
const length = new Uint8Array(4);
|
|
31
|
-
new DataView(length.buffer).setUint32(0, bytes.byteLength, true);
|
|
32
|
-
return concat([length, bytes]);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const u64LittleEndian = (value: bigint): Uint8Array => {
|
|
36
|
-
const bytes = new Uint8Array(8);
|
|
37
|
-
new DataView(bytes.buffer).setBigUint64(0, value, true);
|
|
38
|
-
return bytes;
|
|
39
|
-
};
|
|
24
|
+
const DEFAULT_SEND_RETRY_MS = 1_000;
|
|
25
|
+
const DEFAULT_MAX_SEND_RETRY_MS = 30_000;
|
|
26
|
+
const MAX_BACKOFF_EXPONENT = 20;
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
* Derive the token echoed by V2 data frames. Delivery recipients are unsigned,
|
|
43
|
-
* so the receiver's nonce alone is not a destination binding. Folding both
|
|
44
|
-
* authenticated identities and signed transport sessions into the 32-byte
|
|
45
|
-
* field prevents a copied request nonce from creating an interchangeable
|
|
46
|
-
* stream for another receiver.
|
|
47
|
-
*/
|
|
48
|
-
export const deriveReplicationInfoV2ReceiverBinding = (properties: {
|
|
49
|
-
receiverChallenge: Uint8Array;
|
|
50
|
-
receiver: PublicSignKey;
|
|
51
|
-
receiverTransportSession: bigint;
|
|
52
|
-
sender: PublicSignKey;
|
|
53
|
-
senderTransportSession: bigint;
|
|
54
|
-
}): Uint8Array =>
|
|
55
|
-
sha256Sync(
|
|
56
|
-
concat([
|
|
57
|
-
lengthPrefixed(RECEIVER_BINDING_DOMAIN),
|
|
58
|
-
lengthPrefixed(properties.receiverChallenge),
|
|
59
|
-
lengthPrefixed(serialize(properties.receiver)),
|
|
60
|
-
u64LittleEndian(properties.receiverTransportSession),
|
|
61
|
-
lengthPrefixed(serialize(properties.sender)),
|
|
62
|
-
u64LittleEndian(properties.senderTransportSession),
|
|
63
|
-
]),
|
|
64
|
-
);
|
|
28
|
+
export { deriveReplicationInfoV2ReceiverBinding } from "./replication-info-v2-binding.js";
|
|
65
29
|
|
|
66
30
|
export type LegacyReplicationInfoMessage =
|
|
67
31
|
| AllReplicatingSegmentsMessage
|
|
@@ -78,15 +42,19 @@ export type ReplicationInfoV2SendState = {
|
|
|
78
42
|
peerSession: object;
|
|
79
43
|
receiverTransportSession: bigint;
|
|
80
44
|
senderTransportSession: bigint;
|
|
45
|
+
capabilityTimestamp: bigint;
|
|
81
46
|
lastRequestTimestamp: bigint;
|
|
82
47
|
receiverRequestChallenge: Uint8Array;
|
|
83
48
|
receiverChallenge: Uint8Array;
|
|
84
49
|
senderEpoch: Uint8Array;
|
|
85
50
|
ownershipLifecycleController: AbortController;
|
|
51
|
+
ownershipAbortListener: () => void;
|
|
86
52
|
nextSequence: bigint;
|
|
87
53
|
established: boolean;
|
|
88
54
|
suspended: boolean;
|
|
89
55
|
inFlightSequence?: bigint;
|
|
56
|
+
retryTimer?: ReturnType<typeof setTimeout>;
|
|
57
|
+
retryAttempts: number;
|
|
90
58
|
controller: AbortController;
|
|
91
59
|
pending?: SendRequest;
|
|
92
60
|
worker?: Promise<void>;
|
|
@@ -101,11 +69,14 @@ export type ReplicationInfoV2SendDeps<R extends "u32" | "u64"> = {
|
|
|
101
69
|
ranges: readonly { mode: unknown }[],
|
|
102
70
|
) => void;
|
|
103
71
|
isClosed: () => boolean;
|
|
72
|
+
isPeerSessionCurrent: (peerHash: string, peerSession: object) => boolean;
|
|
104
73
|
isPeerSessionOpen: (peerHash: string, peerSession: object) => boolean;
|
|
105
74
|
captureReplicationOwnershipLifecycle: () => AbortController;
|
|
106
75
|
isReplicationOwnershipLifecycleActive: (
|
|
107
76
|
controller: AbortController,
|
|
108
77
|
) => boolean;
|
|
78
|
+
sendRetryMs?: number;
|
|
79
|
+
maxSendRetryMs?: number;
|
|
109
80
|
};
|
|
110
81
|
|
|
111
82
|
const bytesEqual = (left: Uint8Array, right: Uint8Array): boolean => {
|
|
@@ -131,7 +102,15 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
131
102
|
_spentPeerSessions!: WeakSet<object>;
|
|
132
103
|
_retiringWorkersByPeer!: Map<string, Promise<void>>;
|
|
133
104
|
|
|
105
|
+
private readonly sendRetryMs: number;
|
|
106
|
+
private readonly maxSendRetryMs: number;
|
|
107
|
+
|
|
134
108
|
constructor(private readonly deps: ReplicationInfoV2SendDeps<R>) {
|
|
109
|
+
this.sendRetryMs = Math.max(1, deps.sendRetryMs ?? DEFAULT_SEND_RETRY_MS);
|
|
110
|
+
this.maxSendRetryMs = Math.max(
|
|
111
|
+
this.sendRetryMs,
|
|
112
|
+
deps.maxSendRetryMs ?? DEFAULT_MAX_SEND_RETRY_MS,
|
|
113
|
+
);
|
|
135
114
|
this._senderEpoch = randomBytes(32);
|
|
136
115
|
this._sendStates = new Map();
|
|
137
116
|
this._spentPeerSessions = new WeakSet();
|
|
@@ -169,6 +148,14 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
169
148
|
|
|
170
149
|
private clearState(state: ReplicationInfoV2SendState): void {
|
|
171
150
|
this.trackRetiringWorker(state);
|
|
151
|
+
if (state.retryTimer) {
|
|
152
|
+
clearTimeout(state.retryTimer);
|
|
153
|
+
state.retryTimer = undefined;
|
|
154
|
+
}
|
|
155
|
+
state.ownershipLifecycleController.signal.removeEventListener(
|
|
156
|
+
"abort",
|
|
157
|
+
state.ownershipAbortListener,
|
|
158
|
+
);
|
|
172
159
|
state.controller.abort();
|
|
173
160
|
if (this._sendStates.get(state.peerHash) === state) {
|
|
174
161
|
this._sendStates.delete(state.peerHash);
|
|
@@ -201,25 +188,72 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
201
188
|
!this.deps.isClosed() &&
|
|
202
189
|
!state.controller.signal.aborted &&
|
|
203
190
|
this._sendStates.get(state.peerHash) === state &&
|
|
204
|
-
this.deps.
|
|
191
|
+
this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
|
|
205
192
|
this.deps.getSenderTransportSession() === state.senderTransportSession
|
|
206
193
|
);
|
|
207
194
|
}
|
|
208
195
|
|
|
209
|
-
private
|
|
196
|
+
private isDestinationReady(state: ReplicationInfoV2SendState): boolean {
|
|
210
197
|
return (
|
|
211
198
|
this.isDestinationCurrent(state) &&
|
|
199
|
+
this.deps.isPeerSessionOpen(state.peerHash, state.peerSession)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private isCurrent(state: ReplicationInfoV2SendState): boolean {
|
|
204
|
+
return (
|
|
205
|
+
this.isDestinationReady(state) &&
|
|
212
206
|
this.deps.isReplicationOwnershipLifecycleActive(
|
|
213
207
|
state.ownershipLifecycleController,
|
|
214
208
|
)
|
|
215
209
|
);
|
|
216
210
|
}
|
|
217
211
|
|
|
212
|
+
private retireSpentState(state: ReplicationInfoV2SendState): void {
|
|
213
|
+
this._spentPeerSessions.add(state.peerSession);
|
|
214
|
+
this.clearState(state);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Collapse every interrupted normal-send path to one authoritative snapshot.
|
|
219
|
+
* A closed readiness gate is temporary while the exact PeerSession remains
|
|
220
|
+
* current, so it parks instead of destroying the receiver binding. Sequence
|
|
221
|
+
* exhaustion is terminal even when readiness or ownership changed at the same
|
|
222
|
+
* time as an in-flight transport attempt.
|
|
223
|
+
*/
|
|
224
|
+
private parkSnapshotForRetry(state: ReplicationInfoV2SendState): void {
|
|
225
|
+
state.inFlightSequence = undefined;
|
|
226
|
+
if (state.nextSequence > MAX_U64) {
|
|
227
|
+
this.retireSpentState(state);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!this.isDestinationCurrent(state)) {
|
|
231
|
+
this.clearState(state);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
236
|
+
state.ownershipLifecycleController,
|
|
237
|
+
)
|
|
238
|
+
) {
|
|
239
|
+
if (state.retryTimer) {
|
|
240
|
+
clearTimeout(state.retryTimer);
|
|
241
|
+
state.retryTimer = undefined;
|
|
242
|
+
}
|
|
243
|
+
state.pending = undefined;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
state.suspended = true;
|
|
248
|
+
state.pending = { kind: "snapshot" };
|
|
249
|
+
this.scheduleRetry(state);
|
|
250
|
+
}
|
|
251
|
+
|
|
218
252
|
/**
|
|
219
253
|
* Accept a signed receiver request. An exact newer retry asks for another
|
|
220
254
|
* full snapshot without resetting the epoch/sequence. A different challenge
|
|
221
|
-
*
|
|
222
|
-
*
|
|
255
|
+
* can replace the binding only after a strictly newer signed capability;
|
|
256
|
+
* retiring work is drained before the replacement can start.
|
|
223
257
|
*/
|
|
224
258
|
acceptRequest(
|
|
225
259
|
request: RequestReplicationInfoV2Message,
|
|
@@ -227,6 +261,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
227
261
|
from: PublicSignKey;
|
|
228
262
|
peerSession: object;
|
|
229
263
|
receiverTransportSession: bigint;
|
|
264
|
+
capabilityTimestamp: bigint;
|
|
230
265
|
requestTimestamp: bigint;
|
|
231
266
|
},
|
|
232
267
|
): boolean {
|
|
@@ -244,6 +279,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
244
279
|
if (
|
|
245
280
|
this._retiringWorkersByPeer.has(peerHash) ||
|
|
246
281
|
this._spentPeerSessions.has(properties.peerSession) ||
|
|
282
|
+
!this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
|
|
247
283
|
!this.deps.isPeerSessionOpen(peerHash, properties.peerSession)
|
|
248
284
|
) {
|
|
249
285
|
return false;
|
|
@@ -261,6 +297,16 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
261
297
|
return false;
|
|
262
298
|
}
|
|
263
299
|
}
|
|
300
|
+
if (
|
|
301
|
+
previous &&
|
|
302
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
303
|
+
previous.ownershipLifecycleController,
|
|
304
|
+
)
|
|
305
|
+
) {
|
|
306
|
+
// Ownership teardown retains the binding only so close/drop can send its
|
|
307
|
+
// terminal empty Full. A later request must not revive normal delivery.
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
264
310
|
if (previous) {
|
|
265
311
|
const sameBinding =
|
|
266
312
|
previous.peerSession === properties.peerSession &&
|
|
@@ -275,22 +321,43 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
275
321
|
return false;
|
|
276
322
|
}
|
|
277
323
|
previous.lastRequestTimestamp = properties.requestTimestamp;
|
|
324
|
+
previous.capabilityTimestamp = properties.capabilityTimestamp;
|
|
325
|
+
if (previous.retryTimer) {
|
|
326
|
+
clearTimeout(previous.retryTimer);
|
|
327
|
+
previous.retryTimer = undefined;
|
|
328
|
+
}
|
|
278
329
|
previous.suspended = false;
|
|
330
|
+
previous.pending = { kind: "snapshot" };
|
|
279
331
|
this.enqueueState(previous, { kind: "snapshot" });
|
|
280
332
|
return true;
|
|
281
333
|
}
|
|
282
334
|
|
|
283
|
-
|
|
335
|
+
if (properties.capabilityTimestamp <= previous.capabilityTimestamp) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
this.clearState(previous);
|
|
339
|
+
previous = undefined;
|
|
340
|
+
if (this._retiringWorkersByPeer.has(peerHash)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
284
343
|
}
|
|
285
344
|
|
|
286
345
|
const ownershipLifecycleController =
|
|
287
346
|
this.deps.captureReplicationOwnershipLifecycle();
|
|
347
|
+
if (
|
|
348
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
349
|
+
ownershipLifecycleController,
|
|
350
|
+
)
|
|
351
|
+
) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
288
354
|
const state: ReplicationInfoV2SendState = {
|
|
289
355
|
peerHash,
|
|
290
356
|
target: properties.from,
|
|
291
357
|
peerSession: properties.peerSession,
|
|
292
358
|
receiverTransportSession: properties.receiverTransportSession,
|
|
293
359
|
senderTransportSession,
|
|
360
|
+
capabilityTimestamp: properties.capabilityTimestamp,
|
|
294
361
|
lastRequestTimestamp: properties.requestTimestamp,
|
|
295
362
|
receiverRequestChallenge: request.receiverChallenge.slice(),
|
|
296
363
|
receiverChallenge: deriveReplicationInfoV2ReceiverBinding({
|
|
@@ -301,12 +368,26 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
301
368
|
senderTransportSession,
|
|
302
369
|
}),
|
|
303
370
|
senderEpoch: this._senderEpoch.slice(),
|
|
371
|
+
ownershipAbortListener: () => {},
|
|
304
372
|
nextSequence: 1n,
|
|
305
373
|
established: false,
|
|
306
374
|
suspended: false,
|
|
375
|
+
retryAttempts: 0,
|
|
307
376
|
controller: new AbortController(),
|
|
308
377
|
ownershipLifecycleController,
|
|
309
378
|
};
|
|
379
|
+
state.ownershipAbortListener = () => {
|
|
380
|
+
if (state.retryTimer) {
|
|
381
|
+
clearTimeout(state.retryTimer);
|
|
382
|
+
state.retryTimer = undefined;
|
|
383
|
+
}
|
|
384
|
+
state.pending = undefined;
|
|
385
|
+
};
|
|
386
|
+
ownershipLifecycleController.signal.addEventListener(
|
|
387
|
+
"abort",
|
|
388
|
+
state.ownershipAbortListener,
|
|
389
|
+
{ once: true },
|
|
390
|
+
);
|
|
310
391
|
this._sendStates.set(peerHash, state);
|
|
311
392
|
this.enqueueState(state, { kind: "snapshot" });
|
|
312
393
|
return true;
|
|
@@ -329,20 +410,15 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
329
410
|
state: ReplicationInfoV2SendState,
|
|
330
411
|
request: SendRequest,
|
|
331
412
|
): void {
|
|
332
|
-
if (!this.isCurrent(state)) {
|
|
333
|
-
state
|
|
334
|
-
if (
|
|
335
|
-
!this.isDestinationCurrent(state) ||
|
|
336
|
-
this.deps.isReplicationOwnershipLifecycleActive(
|
|
337
|
-
state.ownershipLifecycleController,
|
|
338
|
-
)
|
|
339
|
-
) {
|
|
340
|
-
this.clearState(state);
|
|
341
|
-
}
|
|
413
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
414
|
+
this.parkSnapshotForRetry(state);
|
|
342
415
|
return;
|
|
343
416
|
}
|
|
344
417
|
if (state.suspended) {
|
|
345
|
-
|
|
418
|
+
// Delivery is ambiguous while the backoff is armed. Never retain a
|
|
419
|
+
// potentially stale delta: one authoritative Full represents every
|
|
420
|
+
// mutation that arrives before the retry fires.
|
|
421
|
+
this.parkSnapshotForRetry(state);
|
|
346
422
|
return;
|
|
347
423
|
}
|
|
348
424
|
|
|
@@ -352,12 +428,14 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
352
428
|
worker = Promise.resolve()
|
|
353
429
|
.then(() => this.runWorker(state))
|
|
354
430
|
.catch((error) => {
|
|
355
|
-
|
|
431
|
+
// Sequence cleanup and exhaustion fencing must happen before any
|
|
432
|
+
// readiness/ownership classification in the recovery path.
|
|
433
|
+
this.parkSnapshotForRetry(state);
|
|
434
|
+
if (
|
|
435
|
+
this._sendStates.get(state.peerHash) === state &&
|
|
356
436
|
this.deps.isReplicationOwnershipLifecycleActive(
|
|
357
437
|
state.ownershipLifecycleController,
|
|
358
|
-
)
|
|
359
|
-
if (
|
|
360
|
-
ownershipActive &&
|
|
438
|
+
) &&
|
|
361
439
|
!state.controller.signal.aborted &&
|
|
362
440
|
!this.deps.isClosed()
|
|
363
441
|
) {
|
|
@@ -367,28 +445,6 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
367
445
|
(error as Error)?.message ?? String(error),
|
|
368
446
|
);
|
|
369
447
|
}
|
|
370
|
-
state.pending = undefined;
|
|
371
|
-
const ordinaryFailure =
|
|
372
|
-
ownershipActive && this.isDestinationCurrent(state);
|
|
373
|
-
if (ordinaryFailure) {
|
|
374
|
-
if (state.inFlightSequence !== undefined) {
|
|
375
|
-
state.inFlightSequence = undefined;
|
|
376
|
-
if (state.nextSequence > MAX_U64) {
|
|
377
|
-
this._spentPeerSessions.add(state.peerSession);
|
|
378
|
-
this.clearState(state);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
// A delivery error is ambiguous: the receiver may already have
|
|
383
|
-
// applied this sequence. Retain the exact grant, stop ordinary
|
|
384
|
-
// deltas, and require a newer same-challenge request to resume
|
|
385
|
-
// with an authoritative Full at the next safe sequence.
|
|
386
|
-
state.suspended = true;
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (!this.isDestinationCurrent(state)) {
|
|
390
|
-
this.clearState(state);
|
|
391
|
-
}
|
|
392
448
|
})
|
|
393
449
|
.finally(() => {
|
|
394
450
|
if (state.worker === worker) {
|
|
@@ -397,7 +453,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
397
453
|
// before this promise reaction clears `worker`. Re-arm that item
|
|
398
454
|
// here so the one-slot bound cannot become a stranded queue.
|
|
399
455
|
const pending = state.pending;
|
|
400
|
-
if (pending
|
|
456
|
+
if (pending) {
|
|
401
457
|
state.pending = undefined;
|
|
402
458
|
this.enqueueState(state, pending);
|
|
403
459
|
}
|
|
@@ -417,6 +473,51 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
417
473
|
state.pending = { kind: "snapshot" };
|
|
418
474
|
}
|
|
419
475
|
|
|
476
|
+
private retryDelay(state: ReplicationInfoV2SendState): number {
|
|
477
|
+
const exponent = Math.max(0, state.retryAttempts - 1);
|
|
478
|
+
return Math.min(
|
|
479
|
+
this.maxSendRetryMs,
|
|
480
|
+
this.sendRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private scheduleRetry(state: ReplicationInfoV2SendState): void {
|
|
485
|
+
if (state.retryTimer) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (state.nextSequence > MAX_U64) {
|
|
489
|
+
this.retireSpentState(state);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!this.isDestinationCurrent(state)) {
|
|
493
|
+
this.clearState(state);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (
|
|
497
|
+
!this.deps.isReplicationOwnershipLifecycleActive(
|
|
498
|
+
state.ownershipLifecycleController,
|
|
499
|
+
)
|
|
500
|
+
) {
|
|
501
|
+
state.pending = undefined;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
state.retryAttempts = Math.min(
|
|
505
|
+
state.retryAttempts + 1,
|
|
506
|
+
MAX_BACKOFF_EXPONENT + 1,
|
|
507
|
+
);
|
|
508
|
+
state.retryTimer = setTimeout(() => {
|
|
509
|
+
state.retryTimer = undefined;
|
|
510
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
511
|
+
this.parkSnapshotForRetry(state);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
state.suspended = false;
|
|
515
|
+
state.pending = { kind: "snapshot" };
|
|
516
|
+
this.enqueueState(state, { kind: "snapshot" });
|
|
517
|
+
}, this.retryDelay(state));
|
|
518
|
+
state.retryTimer.unref?.();
|
|
519
|
+
}
|
|
520
|
+
|
|
420
521
|
private async createMessage(
|
|
421
522
|
state: ReplicationInfoV2SendState,
|
|
422
523
|
request: SendRequest,
|
|
@@ -455,19 +556,19 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
455
556
|
}
|
|
456
557
|
|
|
457
558
|
private async runWorker(state: ReplicationInfoV2SendState): Promise<void> {
|
|
458
|
-
while (
|
|
559
|
+
while (true) {
|
|
560
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
561
|
+
this.parkSnapshotForRetry(state);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
459
564
|
const request = state.pending;
|
|
460
565
|
if (!request) {
|
|
461
566
|
return;
|
|
462
567
|
}
|
|
463
568
|
state.pending = undefined;
|
|
464
|
-
if (state.nextSequence > MAX_U64) {
|
|
465
|
-
this.clearState(state);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
569
|
const message = await this.createMessage(state, request);
|
|
470
|
-
if (!this.isCurrent(state)) {
|
|
570
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
571
|
+
this.parkSnapshotForRetry(state);
|
|
471
572
|
return;
|
|
472
573
|
}
|
|
473
574
|
// Consume the sequence before the transport attempt. From this point on
|
|
@@ -488,15 +589,12 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
488
589
|
]),
|
|
489
590
|
});
|
|
490
591
|
state.inFlightSequence = undefined;
|
|
491
|
-
if (!this.isCurrent(state)) {
|
|
592
|
+
if (state.nextSequence > MAX_U64 || !this.isCurrent(state)) {
|
|
593
|
+
this.parkSnapshotForRetry(state);
|
|
492
594
|
return;
|
|
493
595
|
}
|
|
596
|
+
state.retryAttempts = 0;
|
|
494
597
|
state.established = true;
|
|
495
|
-
if (state.nextSequence > MAX_U64) {
|
|
496
|
-
this._spentPeerSessions.add(state.peerSession);
|
|
497
|
-
this.clearState(state);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
598
|
}
|
|
501
599
|
}
|
|
502
600
|
|
|
@@ -518,22 +616,38 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
|
518
616
|
continue;
|
|
519
617
|
}
|
|
520
618
|
state.pending = undefined;
|
|
619
|
+
const sequence = state.nextSequence;
|
|
620
|
+
state.nextSequence += 1n;
|
|
521
621
|
const message = new FullReplicationInfoV2Message({
|
|
522
622
|
receiverChallenge: state.receiverChallenge.slice(),
|
|
523
623
|
senderEpoch: state.senderEpoch.slice(),
|
|
524
|
-
sequence
|
|
624
|
+
sequence,
|
|
525
625
|
segments: [],
|
|
526
626
|
});
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
627
|
+
if (sequence === MAX_U64) {
|
|
628
|
+
// Retire the exhausted normal stream before transport invocation. The
|
|
629
|
+
// terminal attempt remains valid because it carries its caller-owned
|
|
630
|
+
// signal rather than the state controller that clearState aborts.
|
|
631
|
+
this.retireSpentState(state);
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
sends.push(
|
|
635
|
+
Promise.resolve(
|
|
636
|
+
this.deps.getRpc().send(message, {
|
|
637
|
+
mode: new AcknowledgeDelivery({
|
|
638
|
+
to: [state.target],
|
|
639
|
+
redundancy: 1,
|
|
640
|
+
}),
|
|
641
|
+
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
642
|
+
signal,
|
|
643
|
+
}),
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
// Keep one synchronous transport failure isolated to its destination;
|
|
648
|
+
// the sequence was already consumed and later peers must still reset.
|
|
649
|
+
sends.push(Promise.reject(error));
|
|
650
|
+
}
|
|
537
651
|
}
|
|
538
652
|
await Promise.allSettled(sends);
|
|
539
653
|
}
|