@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
|
@@ -10,6 +10,7 @@ const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
|
|
|
10
10
|
const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
|
|
11
11
|
const DEFAULT_LEGACY_FALLBACK_DELAY_MS = 5_000;
|
|
12
12
|
const MAX_U64 = (1n << 64n) - 1n;
|
|
13
|
+
const MAX_BACKOFF_EXPONENT = 20;
|
|
13
14
|
const bytesEqual = (left, right) => {
|
|
14
15
|
if (left.byteLength !== right.byteLength) {
|
|
15
16
|
return false;
|
|
@@ -41,6 +42,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
41
42
|
_receiveStates;
|
|
42
43
|
_cutoverPeerSessions;
|
|
43
44
|
_localCapabilityReadyBySession;
|
|
45
|
+
_localCapabilityContextBySession;
|
|
46
|
+
_localCapabilityAdvertisementsByPeer;
|
|
44
47
|
_reservedAdmissionsByPeer;
|
|
45
48
|
now;
|
|
46
49
|
requestRetryMs;
|
|
@@ -57,6 +60,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
57
60
|
this._receiveStates = new Map();
|
|
58
61
|
this._cutoverPeerSessions = new WeakSet();
|
|
59
62
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
63
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
64
|
+
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
60
65
|
this._reservedAdmissionsByPeer = new Map();
|
|
61
66
|
}
|
|
62
67
|
resetForOpen() {
|
|
@@ -64,25 +69,41 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
64
69
|
this._receiveStates = new Map();
|
|
65
70
|
this._cutoverPeerSessions = new WeakSet();
|
|
66
71
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
72
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
73
|
+
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
67
74
|
this._reservedAdmissionsByPeer = new Map();
|
|
68
75
|
}
|
|
69
76
|
clearForClose() {
|
|
77
|
+
for (const advertisement of [
|
|
78
|
+
...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
|
|
79
|
+
]) {
|
|
80
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
81
|
+
}
|
|
82
|
+
this._localCapabilityAdvertisementsByPeer?.clear();
|
|
70
83
|
for (const state of this._receiveStates?.values() ?? []) {
|
|
71
84
|
this.clearState(state);
|
|
72
85
|
}
|
|
73
86
|
this._receiveStates?.clear();
|
|
74
87
|
this._cutoverPeerSessions = new WeakSet();
|
|
75
88
|
this._localCapabilityReadyBySession = new WeakMap();
|
|
89
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
76
90
|
}
|
|
77
91
|
clearPeer(peerHash, expectedSession) {
|
|
92
|
+
const advertisement = this._localCapabilityAdvertisementsByPeer.get(peerHash);
|
|
93
|
+
if (advertisement &&
|
|
94
|
+
(!expectedSession || advertisement.peerSession === expectedSession)) {
|
|
95
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
96
|
+
}
|
|
78
97
|
const state = this._receiveStates.get(peerHash);
|
|
79
98
|
if (state && (!expectedSession || state.peerSession === expectedSession)) {
|
|
80
99
|
this.clearState(state);
|
|
81
100
|
this._localCapabilityReadyBySession.delete(state.peerSession);
|
|
101
|
+
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
82
102
|
this._cutoverPeerSessions.delete(state.peerSession);
|
|
83
103
|
}
|
|
84
104
|
if (expectedSession) {
|
|
85
105
|
this._localCapabilityReadyBySession.delete(expectedSession);
|
|
106
|
+
this._localCapabilityContextBySession.delete(expectedSession);
|
|
86
107
|
this._cutoverPeerSessions.delete(expectedSession);
|
|
87
108
|
}
|
|
88
109
|
}
|
|
@@ -112,8 +133,345 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
112
133
|
this._receiveStates.delete(state.peerHash);
|
|
113
134
|
}
|
|
114
135
|
}
|
|
136
|
+
clearLocalCapabilityAdvertisement(state, options) {
|
|
137
|
+
if (state.timer) {
|
|
138
|
+
clearTimeout(state.timer);
|
|
139
|
+
state.timer = undefined;
|
|
140
|
+
}
|
|
141
|
+
state.lifecycleSignal.removeEventListener("abort", state.onLifecycleAbort);
|
|
142
|
+
state.controller.abort();
|
|
143
|
+
const wasMapped = this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state;
|
|
144
|
+
if (wasMapped) {
|
|
145
|
+
this._localCapabilityAdvertisementsByPeer.delete(state.peerHash);
|
|
146
|
+
}
|
|
147
|
+
const ready = this._localCapabilityReadyBySession.get(state.peerSession);
|
|
148
|
+
if (ready?.advertisement === state) {
|
|
149
|
+
this._localCapabilityReadyBySession.delete(state.peerSession);
|
|
150
|
+
}
|
|
151
|
+
if (wasMapped &&
|
|
152
|
+
!options?.preserveContext &&
|
|
153
|
+
this._localCapabilityContextBySession.get(state.peerSession) ===
|
|
154
|
+
state.context) {
|
|
155
|
+
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
isLocalCapabilityAdvertisementOwnerCurrent(state) {
|
|
159
|
+
return (this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state &&
|
|
160
|
+
this._localCapabilityContextBySession.get(state.peerSession) ===
|
|
161
|
+
state.context &&
|
|
162
|
+
state.context.peerHash === state.peerHash &&
|
|
163
|
+
state.context.lifecycleSignal === state.lifecycleSignal &&
|
|
164
|
+
state.context.target.equals(state.target) &&
|
|
165
|
+
!state.controller.signal.aborted &&
|
|
166
|
+
!state.lifecycleSignal.aborted &&
|
|
167
|
+
!this.deps.isClosed() &&
|
|
168
|
+
this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
|
|
169
|
+
(state.receiverTransportSession === undefined ||
|
|
170
|
+
this.deps.getReceiverTransportSession() ===
|
|
171
|
+
state.receiverTransportSession));
|
|
172
|
+
}
|
|
173
|
+
isLocalCapabilityAdvertisementGenerationCurrent(state) {
|
|
174
|
+
return (this.isLocalCapabilityAdvertisementOwnerCurrent(state) &&
|
|
175
|
+
this.deps.isReceiveEpochCurrent(state.peerHash, state.receiveEpoch));
|
|
176
|
+
}
|
|
177
|
+
isLocalCapabilityAdvertisementReadyOpen(state) {
|
|
178
|
+
return (this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
|
|
179
|
+
this.deps.isPeerStateCurrent(state.peerHash, state.peerSession, state.receiveEpoch));
|
|
180
|
+
}
|
|
181
|
+
localCapabilityRetryDelay(state) {
|
|
182
|
+
const exponent = Math.max(0, state.attempts - 1);
|
|
183
|
+
return Math.min(this.maxRequestRetryMs, this.requestRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT));
|
|
184
|
+
}
|
|
185
|
+
armLocalCapabilityAdvertisement(state) {
|
|
186
|
+
if (state.timer || state.inFlight || state.ready) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (state.acknowledgedReady && !state.context.legacyBarrierReleased) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
193
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
state.timer = setTimeout(() => {
|
|
200
|
+
state.timer = undefined;
|
|
201
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
202
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
209
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
void this.runLocalCapabilityAdvertisement(state);
|
|
213
|
+
}, this.localCapabilityRetryDelay(state));
|
|
214
|
+
state.timer.unref?.();
|
|
215
|
+
}
|
|
216
|
+
async runLocalCapabilityAdvertisement(state) {
|
|
217
|
+
if (state.inFlight) {
|
|
218
|
+
await state.inFlight;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (state.ready) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
225
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (state.acknowledgedReady) {
|
|
232
|
+
if (state.context.legacyBarrierReleased) {
|
|
233
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
238
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
state.attempts = Math.min(state.attempts + 1, MAX_BACKOFF_EXPONENT + 1);
|
|
242
|
+
let operation;
|
|
243
|
+
operation = (async () => {
|
|
244
|
+
const refreshed = await this.deps.refreshLocalCapability({
|
|
245
|
+
peerHash: state.peerHash,
|
|
246
|
+
target: state.target,
|
|
247
|
+
peerSession: state.peerSession,
|
|
248
|
+
receiveEpoch: state.receiveEpoch,
|
|
249
|
+
signal: AbortSignal.any([
|
|
250
|
+
state.controller.signal,
|
|
251
|
+
state.lifecycleSignal,
|
|
252
|
+
]),
|
|
253
|
+
});
|
|
254
|
+
if (!refreshed ||
|
|
255
|
+
!this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
|
|
256
|
+
this.deps.getReceiverTransportSession() !==
|
|
257
|
+
refreshed.receiverTransportSession) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
state.receiverTransportSession = refreshed.receiverTransportSession;
|
|
261
|
+
state.acknowledgedReady = {
|
|
262
|
+
peerHash: state.peerHash,
|
|
263
|
+
receiveEpoch: state.receiveEpoch,
|
|
264
|
+
receiverTransportSession: refreshed.receiverTransportSession,
|
|
265
|
+
requestNotBeforeMs: refreshed.requestNotBeforeMs,
|
|
266
|
+
advertisement: state,
|
|
267
|
+
};
|
|
268
|
+
state.attempts = 0;
|
|
269
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
270
|
+
})()
|
|
271
|
+
.catch((error) => {
|
|
272
|
+
if (!state.controller.signal.aborted &&
|
|
273
|
+
!state.lifecycleSignal.aborted &&
|
|
274
|
+
!this.deps.isClosed()) {
|
|
275
|
+
this.deps.onLocalCapabilityError?.(error);
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
.finally(() => {
|
|
279
|
+
if (state.inFlight === operation) {
|
|
280
|
+
state.inFlight = undefined;
|
|
281
|
+
}
|
|
282
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
283
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
284
|
+
}
|
|
285
|
+
else if (this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
|
|
286
|
+
!state.ready) {
|
|
287
|
+
if (!state.acknowledgedReady || state.context.legacyBarrierReleased) {
|
|
288
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
state.inFlight = operation;
|
|
293
|
+
await operation;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Start local authenticated-apply advertisement independently of the legacy
|
|
297
|
+
* join path. ACK and legacy publication are a two-phase barrier: the first
|
|
298
|
+
* attempt always settles independently, while readiness is promoted only
|
|
299
|
+
* after the host releases the legacy barrier. Failed attempts leave one
|
|
300
|
+
* exact-session worker retrying with capped exponential backoff.
|
|
301
|
+
*/
|
|
302
|
+
advertiseLocalCapability(properties) {
|
|
303
|
+
const peerHash = properties.target.hashcode();
|
|
304
|
+
if (properties.signal.aborted ||
|
|
305
|
+
this.deps.isClosed() ||
|
|
306
|
+
!this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
|
|
307
|
+
!this.deps.isReceiveEpochCurrent(peerHash, properties.receiveEpoch)) {
|
|
308
|
+
return {
|
|
309
|
+
firstAttempt: Promise.resolve(),
|
|
310
|
+
releaseLegacyBarrier: () => { },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
let context = this._localCapabilityContextBySession.get(properties.peerSession);
|
|
314
|
+
if (context &&
|
|
315
|
+
(context.peerHash !== peerHash ||
|
|
316
|
+
!context.target.equals(properties.target) ||
|
|
317
|
+
context.lifecycleSignal !== properties.signal)) {
|
|
318
|
+
return {
|
|
319
|
+
firstAttempt: Promise.resolve(),
|
|
320
|
+
releaseLegacyBarrier: () => { },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (!context) {
|
|
324
|
+
context = {
|
|
325
|
+
peerHash,
|
|
326
|
+
target: properties.target,
|
|
327
|
+
lifecycleSignal: properties.signal,
|
|
328
|
+
legacyBarrierReleased: false,
|
|
329
|
+
};
|
|
330
|
+
this._localCapabilityContextBySession.set(properties.peerSession, context);
|
|
331
|
+
}
|
|
332
|
+
let state = this._localCapabilityAdvertisementsByPeer.get(peerHash);
|
|
333
|
+
if (state &&
|
|
334
|
+
(state.peerSession !== properties.peerSession ||
|
|
335
|
+
state.context !== context ||
|
|
336
|
+
state.lifecycleSignal !== properties.signal ||
|
|
337
|
+
!state.target.equals(properties.target))) {
|
|
338
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
339
|
+
state = undefined;
|
|
340
|
+
}
|
|
341
|
+
if (state && state.receiveEpoch !== properties.receiveEpoch) {
|
|
342
|
+
this.clearLocalCapabilityAdvertisement(state, { preserveContext: true });
|
|
343
|
+
state = undefined;
|
|
344
|
+
}
|
|
345
|
+
if (state && !this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
346
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
347
|
+
state = undefined;
|
|
348
|
+
}
|
|
349
|
+
if (this._localCapabilityContextBySession.get(properties.peerSession) !==
|
|
350
|
+
context) {
|
|
351
|
+
return {
|
|
352
|
+
firstAttempt: Promise.resolve(),
|
|
353
|
+
releaseLegacyBarrier: () => { },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (!state) {
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
const advertisement = {
|
|
359
|
+
peerHash,
|
|
360
|
+
target: properties.target,
|
|
361
|
+
peerSession: properties.peerSession,
|
|
362
|
+
receiveEpoch: properties.receiveEpoch,
|
|
363
|
+
lifecycleSignal: properties.signal,
|
|
364
|
+
onLifecycleAbort: () => { },
|
|
365
|
+
controller,
|
|
366
|
+
context,
|
|
367
|
+
attempts: 0,
|
|
368
|
+
ready: false,
|
|
369
|
+
receiverTransportSession: this.deps.getReceiverTransportSession(),
|
|
370
|
+
};
|
|
371
|
+
advertisement.onLifecycleAbort = () => this.clearLocalCapabilityAdvertisement(advertisement);
|
|
372
|
+
properties.signal.addEventListener("abort", advertisement.onLifecycleAbort, {
|
|
373
|
+
once: true,
|
|
374
|
+
});
|
|
375
|
+
this._localCapabilityAdvertisementsByPeer.set(peerHash, advertisement);
|
|
376
|
+
state = advertisement;
|
|
377
|
+
}
|
|
378
|
+
const firstAttempt = state.firstAttempt ??
|
|
379
|
+
(state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
|
|
380
|
+
const advertisement = state;
|
|
381
|
+
return {
|
|
382
|
+
firstAttempt,
|
|
383
|
+
releaseLegacyBarrier: () => this.releaseLocalCapabilityLegacyBarrier(advertisement.peerSession, context),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
releaseLocalCapabilityLegacyBarrier(peerSession, context) {
|
|
387
|
+
if (context.legacyBarrierReleased) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (this._localCapabilityContextBySession.get(peerSession) !== context ||
|
|
391
|
+
context.lifecycleSignal.aborted ||
|
|
392
|
+
this.deps.isClosed() ||
|
|
393
|
+
!this.deps.isPeerSessionCurrent(context.peerHash, peerSession)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
context.legacyBarrierReleased = true;
|
|
397
|
+
const state = this._localCapabilityAdvertisementsByPeer.get(context.peerHash);
|
|
398
|
+
if (state?.peerSession === peerSession && state.context === context) {
|
|
399
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
400
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
promoteLocalCapabilityAdvertisement(state) {
|
|
407
|
+
const ready = state.acknowledgedReady;
|
|
408
|
+
if (state.ready ||
|
|
409
|
+
!state.context.legacyBarrierReleased ||
|
|
410
|
+
!ready ||
|
|
411
|
+
ready.receiveEpoch !== state.receiveEpoch ||
|
|
412
|
+
ready.advertisement !== state ||
|
|
413
|
+
!this.isLocalCapabilityAdvertisementOwnerCurrent(state) ||
|
|
414
|
+
!this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
|
|
415
|
+
this.deps.getReceiverTransportSession() !== ready.receiverTransportSession) {
|
|
416
|
+
return state.ready;
|
|
417
|
+
}
|
|
418
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
419
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (!this.recordLocalCapabilityReady({
|
|
423
|
+
peerHash: state.peerHash,
|
|
424
|
+
peerSession: state.peerSession,
|
|
425
|
+
receiveEpoch: state.receiveEpoch,
|
|
426
|
+
receiverTransportSession: ready.receiverTransportSession,
|
|
427
|
+
requestNotBeforeMs: ready.requestNotBeforeMs,
|
|
428
|
+
}, state)) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
state.ready = true;
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Re-advertise one exact current recovery epoch from the stable membership
|
|
436
|
+
* context captured during opening. The opening handle owns barrier release;
|
|
437
|
+
* recovery never bypasses a legacy snapshot or role publication still in
|
|
438
|
+
* progress.
|
|
439
|
+
*/
|
|
440
|
+
reAdvertiseLocalCapabilityForRecovery(properties) {
|
|
441
|
+
const context = this._localCapabilityContextBySession.get(properties.peerSession);
|
|
442
|
+
if (!context ||
|
|
443
|
+
context.peerHash !== properties.peerHash ||
|
|
444
|
+
context.lifecycleSignal.aborted ||
|
|
445
|
+
this.deps.isClosed() ||
|
|
446
|
+
!this.deps.isPeerSessionCurrent(properties.peerHash, properties.peerSession) ||
|
|
447
|
+
!this.deps.isReceiveEpochCurrent(properties.peerHash, properties.receiveEpoch)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
451
|
+
if (state?.peerSession === properties.peerSession &&
|
|
452
|
+
state.receiveEpoch === properties.receiveEpoch &&
|
|
453
|
+
state.receiverBinding !== undefined) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
const ready = this._localCapabilityReadyBySession.get(properties.peerSession);
|
|
457
|
+
if (ready?.peerHash === properties.peerHash &&
|
|
458
|
+
ready.receiveEpoch === properties.receiveEpoch &&
|
|
459
|
+
ready.receiverTransportSession === this.deps.getReceiverTransportSession()) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
this.advertiseLocalCapability({
|
|
463
|
+
target: context.target,
|
|
464
|
+
peerSession: properties.peerSession,
|
|
465
|
+
receiveEpoch: properties.receiveEpoch,
|
|
466
|
+
signal: context.lifecycleSignal,
|
|
467
|
+
});
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
115
470
|
/** Record success of this session's ACKed local APPLY advertisement. */
|
|
116
471
|
markLocalCapabilityReady(properties) {
|
|
472
|
+
return this.recordLocalCapabilityReady(properties);
|
|
473
|
+
}
|
|
474
|
+
recordLocalCapabilityReady(properties, advertisement) {
|
|
117
475
|
const { peerHash, peerSession, receiverTransportSession } = properties;
|
|
118
476
|
const state = this._receiveStates.get(peerHash);
|
|
119
477
|
if (this.deps.isClosed() ||
|
|
@@ -123,8 +481,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
123
481
|
}
|
|
124
482
|
const ready = {
|
|
125
483
|
peerHash,
|
|
484
|
+
receiveEpoch: properties.receiveEpoch,
|
|
126
485
|
receiverTransportSession,
|
|
127
486
|
requestNotBeforeMs: properties.requestNotBeforeMs,
|
|
487
|
+
advertisement,
|
|
128
488
|
};
|
|
129
489
|
this._localCapabilityReadyBySession.set(peerSession, ready);
|
|
130
490
|
if (state?.peerSession === peerSession &&
|
|
@@ -198,7 +558,9 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
198
558
|
});
|
|
199
559
|
}
|
|
200
560
|
const ready = this._localCapabilityReadyBySession.get(peerSession);
|
|
201
|
-
if (ready?.peerHash === peerHash &&
|
|
561
|
+
if (ready?.peerHash === peerHash &&
|
|
562
|
+
ready.receiveEpoch === receiveEpoch &&
|
|
563
|
+
state.receiverBinding === undefined) {
|
|
202
564
|
this.bindLocalCapability(state, ready);
|
|
203
565
|
}
|
|
204
566
|
if (state.phase !== "active") {
|
|
@@ -231,7 +593,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
231
593
|
};
|
|
232
594
|
this._receiveStates.set(peerHash, state);
|
|
233
595
|
const ready = this._localCapabilityReadyBySession.get(peerSession);
|
|
234
|
-
if (ready?.peerHash === peerHash) {
|
|
596
|
+
if (ready?.peerHash === peerHash && ready.receiveEpoch === receiveEpoch) {
|
|
235
597
|
this.bindLocalCapability(state, ready);
|
|
236
598
|
this.armRequest(state, 0);
|
|
237
599
|
}
|
|
@@ -260,6 +622,33 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
260
622
|
});
|
|
261
623
|
return true;
|
|
262
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Resume only a request generation that exhausted its bounded retries.
|
|
627
|
+
* Wait/liveness callers may nudge recovery without invalidating an active,
|
|
628
|
+
* timed or in-flight request and without advancing the receive epoch.
|
|
629
|
+
*/
|
|
630
|
+
resumeParkedRequest(properties) {
|
|
631
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
632
|
+
if (!state ||
|
|
633
|
+
state.peerSession !== properties.peerSession ||
|
|
634
|
+
state.receiveEpoch !== properties.receiveEpoch ||
|
|
635
|
+
!this.deps.isPeerStateCurrent(properties.peerHash, properties.peerSession, properties.receiveEpoch) ||
|
|
636
|
+
state.phase === "active" ||
|
|
637
|
+
!state.requestParked ||
|
|
638
|
+
state.requestTimer !== undefined ||
|
|
639
|
+
state.requestInFlight !== undefined ||
|
|
640
|
+
this._reservedAdmissionsByPeer.has(properties.peerHash) ||
|
|
641
|
+
state.receiverBinding === undefined ||
|
|
642
|
+
state.lastSequence === MAX_U64) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
state.requestAttempts = 0;
|
|
646
|
+
state.requestsSinceCapabilityRefresh = 0;
|
|
647
|
+
state.capabilityRefreshRequired = true;
|
|
648
|
+
state.requestParked = false;
|
|
649
|
+
this.armRequest(state, 0);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
263
652
|
transitionToResync(state, options) {
|
|
264
653
|
const shouldRestart = state.phase !== "resync" ||
|
|
265
654
|
options?.force === true ||
|
|
@@ -385,6 +774,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
385
774
|
const { state } = admission;
|
|
386
775
|
this._reservedAdmissionsByPeer.set(peerHash, admission);
|
|
387
776
|
state.reservedAdmission = admission;
|
|
777
|
+
if (state.requestTimer) {
|
|
778
|
+
clearTimeout(state.requestTimer);
|
|
779
|
+
state.requestTimer = undefined;
|
|
780
|
+
}
|
|
781
|
+
if (state.legacyFallbackTimer) {
|
|
782
|
+
clearTimeout(state.legacyFallbackTimer);
|
|
783
|
+
state.legacyFallbackTimer = undefined;
|
|
784
|
+
}
|
|
388
785
|
return admission;
|
|
389
786
|
}
|
|
390
787
|
release(admission) {
|
|
@@ -403,6 +800,27 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
403
800
|
(currentState !== state && currentState.phase !== "active"))) {
|
|
404
801
|
this.transitionToResync(currentState, { force: true });
|
|
405
802
|
}
|
|
803
|
+
else if (currentState === state &&
|
|
804
|
+
!admission.committed &&
|
|
805
|
+
this.isStateCurrent(state) &&
|
|
806
|
+
state.phase !== "active" &&
|
|
807
|
+
state.requestTimer === undefined &&
|
|
808
|
+
state.requestInFlight === undefined &&
|
|
809
|
+
!state.requestParked) {
|
|
810
|
+
// Reserving a Full pauses this generation's retry timer. If its host
|
|
811
|
+
// apply lane releases without committing, restore the bounded request
|
|
812
|
+
// worker so the exact current generation cannot become stranded.
|
|
813
|
+
this.armRequest(state, 0);
|
|
814
|
+
}
|
|
815
|
+
if (currentState === state &&
|
|
816
|
+
this.isStateCurrent(state) &&
|
|
817
|
+
state.phase === "active" &&
|
|
818
|
+
state.legacyFallbackFingerprint !== undefined) {
|
|
819
|
+
// A reservation also pauses compat sidecar recovery. Matching commits
|
|
820
|
+
// clear the evidence; an uncommitted or non-matching frame restores its
|
|
821
|
+
// bounded fallback without invalidating work in the host apply lane.
|
|
822
|
+
this.armLegacyFallback(state);
|
|
823
|
+
}
|
|
406
824
|
}
|
|
407
825
|
/**
|
|
408
826
|
* B9 can lose its sender grant while keeping the topic session open. Its
|
|
@@ -485,6 +903,16 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
485
903
|
if (state.legacyFallbackTimer) {
|
|
486
904
|
return true;
|
|
487
905
|
}
|
|
906
|
+
this.armLegacyFallback(state);
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
armLegacyFallback(state) {
|
|
910
|
+
if (state.legacyFallbackTimer ||
|
|
911
|
+
state.phase !== "active" ||
|
|
912
|
+
state.legacyFallbackFingerprint === undefined ||
|
|
913
|
+
this._reservedAdmissionsByPeer.has(state.peerHash)) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
488
916
|
state.legacyFallbackTimer = setTimeout(() => {
|
|
489
917
|
state.legacyFallbackTimer = undefined;
|
|
490
918
|
if (this.isStateCurrent(state) && state.phase === "active") {
|
|
@@ -495,7 +923,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
495
923
|
}
|
|
496
924
|
}, this.legacyFallbackDelayMs);
|
|
497
925
|
state.legacyFallbackTimer.unref?.();
|
|
498
|
-
return true;
|
|
499
926
|
}
|
|
500
927
|
clearLegacyFallback(state) {
|
|
501
928
|
if (state.legacyFallbackTimer) {
|
|
@@ -594,8 +1021,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
594
1021
|
if (this._receiveStates.get(state.peerHash) !== state ||
|
|
595
1022
|
state.controller.signal.aborted ||
|
|
596
1023
|
this.deps.isClosed() ||
|
|
597
|
-
|
|
598
|
-
this._reservedAdmissionsByPeer.get(state.peerHash)?.state !== state) ||
|
|
1024
|
+
this._reservedAdmissionsByPeer.has(state.peerHash) ||
|
|
599
1025
|
state.receiverBinding === undefined ||
|
|
600
1026
|
state.phase === "active" ||
|
|
601
1027
|
state.requestParked ||
|
|
@@ -614,6 +1040,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
614
1040
|
return Math.min(this.maxRequestRetryMs, this.requestRetryMs * 2 ** Math.min(exponent, 20));
|
|
615
1041
|
}
|
|
616
1042
|
async refreshGrant(state) {
|
|
1043
|
+
const version = state.version;
|
|
617
1044
|
const refreshed = await this.deps.refreshLocalCapability({
|
|
618
1045
|
peerHash: state.peerHash,
|
|
619
1046
|
target: state.target,
|
|
@@ -622,6 +1049,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
622
1049
|
signal: state.controller.signal,
|
|
623
1050
|
});
|
|
624
1051
|
if (!refreshed ||
|
|
1052
|
+
state.version !== version ||
|
|
1053
|
+
this._reservedAdmissionsByPeer.has(state.peerHash) ||
|
|
625
1054
|
!this.isStateCurrent(state) ||
|
|
626
1055
|
this.deps.getReceiverTransportSession() !==
|
|
627
1056
|
refreshed.receiverTransportSession) {
|
|
@@ -629,6 +1058,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
629
1058
|
}
|
|
630
1059
|
const ready = {
|
|
631
1060
|
peerHash: state.peerHash,
|
|
1061
|
+
receiveEpoch: state.receiveEpoch,
|
|
632
1062
|
receiverTransportSession: refreshed.receiverTransportSession,
|
|
633
1063
|
requestNotBeforeMs: refreshed.requestNotBeforeMs,
|
|
634
1064
|
};
|
|
@@ -647,7 +1077,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
647
1077
|
if (state.requestInFlight) {
|
|
648
1078
|
return;
|
|
649
1079
|
}
|
|
650
|
-
if (!this.isStateCurrent(state)
|
|
1080
|
+
if (!this.isStateCurrent(state) ||
|
|
1081
|
+
this._reservedAdmissionsByPeer.has(state.peerHash)) {
|
|
651
1082
|
return;
|
|
652
1083
|
}
|
|
653
1084
|
if (state.phase === "active" ||
|
|
@@ -667,12 +1098,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
667
1098
|
return;
|
|
668
1099
|
}
|
|
669
1100
|
}
|
|
670
|
-
if (!this.isStateCurrent(state)
|
|
1101
|
+
if (!this.isStateCurrent(state) ||
|
|
1102
|
+
this._reservedAdmissionsByPeer.has(state.peerHash)) {
|
|
671
1103
|
return;
|
|
672
1104
|
}
|
|
673
1105
|
const ready = this._localCapabilityReadyBySession.get(state.peerSession);
|
|
674
1106
|
if (!ready ||
|
|
675
1107
|
ready.peerHash !== state.peerHash ||
|
|
1108
|
+
ready.receiveEpoch !== state.receiveEpoch ||
|
|
676
1109
|
ready.receiverTransportSession !== state.receiverTransportSession) {
|
|
677
1110
|
return;
|
|
678
1111
|
}
|
|
@@ -705,6 +1138,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
705
1138
|
}
|
|
706
1139
|
if (this._receiveStates.get(state.peerHash) === state &&
|
|
707
1140
|
!state.controller.signal.aborted &&
|
|
1141
|
+
!this._reservedAdmissionsByPeer.has(state.peerHash) &&
|
|
708
1142
|
!state.requestTimer &&
|
|
709
1143
|
state.phase !== "active") {
|
|
710
1144
|
if (state.requestsSinceCapabilityRefresh >= 3) {
|