@peerbit/shared-log 13.2.23 → 13.2.25
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/checked-prune.d.ts +12 -0
- package/dist/src/checked-prune.d.ts.map +1 -1
- package/dist/src/checked-prune.js +33 -0
- package/dist/src/checked-prune.js.map +1 -1
- package/dist/src/coordinate-persistence.d.ts +211 -0
- package/dist/src/coordinate-persistence.d.ts.map +1 -0
- package/dist/src/coordinate-persistence.js +1133 -0
- package/dist/src/coordinate-persistence.js.map +1 -0
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +19 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +139 -133
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1491 -4265
- package/dist/src/index.js.map +1 -1
- package/dist/src/instance-lifecycle.d.ts +117 -0
- package/dist/src/instance-lifecycle.d.ts.map +1 -0
- package/dist/src/instance-lifecycle.js +254 -0
- package/dist/src/instance-lifecycle.js.map +1 -0
- package/dist/src/join-warmup.d.ts +86 -0
- package/dist/src/join-warmup.d.ts.map +1 -0
- package/dist/src/join-warmup.js +351 -0
- package/dist/src/join-warmup.js.map +1 -0
- package/dist/src/leader-plan-cache.d.ts +33 -0
- package/dist/src/leader-plan-cache.d.ts.map +1 -0
- package/dist/src/leader-plan-cache.js +49 -0
- package/dist/src/leader-plan-cache.js.map +1 -0
- package/dist/src/native-write-through-block-store.d.ts +123 -0
- package/dist/src/native-write-through-block-store.d.ts.map +1 -0
- package/dist/src/native-write-through-block-store.js +989 -0
- package/dist/src/native-write-through-block-store.js.map +1 -0
- package/dist/src/peer-session.d.ts +117 -0
- package/dist/src/peer-session.d.ts.map +1 -0
- package/dist/src/peer-session.js +259 -0
- package/dist/src/peer-session.js.map +1 -0
- package/dist/src/ranges.d.ts +2 -2
- package/dist/src/ranges.d.ts.map +1 -1
- package/dist/src/ranges.js +10 -8
- package/dist/src/ranges.js.map +1 -1
- package/dist/src/replication-announcement.d.ts +116 -0
- package/dist/src/replication-announcement.d.ts.map +1 -0
- package/dist/src/replication-announcement.js +515 -0
- package/dist/src/replication-announcement.js.map +1 -0
- package/dist/src/replicator-liveness.d.ts +59 -0
- package/dist/src/replicator-liveness.d.ts.map +1 -0
- package/dist/src/replicator-liveness.js +304 -0
- package/dist/src/replicator-liveness.js.map +1 -0
- package/dist/src/sync/dispatch-lifecycle.d.ts +48 -0
- package/dist/src/sync/dispatch-lifecycle.d.ts.map +1 -0
- package/dist/src/sync/dispatch-lifecycle.js +141 -0
- package/dist/src/sync/dispatch-lifecycle.js.map +1 -0
- package/dist/src/sync/factory.d.ts +29 -0
- package/dist/src/sync/factory.d.ts.map +1 -0
- package/dist/src/sync/factory.js +87 -0
- package/dist/src/sync/factory.js.map +1 -0
- package/dist/src/sync/pending-sync-store.d.ts +105 -0
- package/dist/src/sync/pending-sync-store.d.ts.map +1 -0
- package/dist/src/sync/pending-sync-store.js +877 -0
- package/dist/src/sync/pending-sync-store.js.map +1 -0
- package/dist/src/sync/rateless-iblt.d.ts +4 -1
- package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.js +102 -106
- package/dist/src/sync/rateless-iblt.js.map +1 -1
- package/dist/src/sync/simple.d.ts +10 -41
- package/dist/src/sync/simple.d.ts.map +1 -1
- package/dist/src/sync/simple.js +259 -952
- package/dist/src/sync/simple.js.map +1 -1
- package/dist/src/sync/sync-peer-state.d.ts +16 -0
- package/dist/src/sync/sync-peer-state.d.ts.map +1 -0
- package/dist/src/sync/sync-peer-state.js +82 -0
- package/dist/src/sync/sync-peer-state.js.map +1 -0
- package/package.json +22 -21
- package/src/checked-prune.ts +35 -0
- package/src/coordinate-persistence.ts +1796 -0
- package/src/errors.ts +21 -0
- package/src/index.ts +6367 -10052
- package/src/instance-lifecycle.ts +351 -0
- package/src/join-warmup.ts +507 -0
- package/src/leader-plan-cache.ts +61 -0
- package/src/native-write-through-block-store.ts +1170 -0
- package/src/peer-session.ts +336 -0
- package/src/ranges.ts +22 -16
- package/src/replication-announcement.ts +744 -0
- package/src/replicator-liveness.ts +439 -0
- package/src/sync/dispatch-lifecycle.ts +230 -0
- package/src/sync/factory.ts +154 -0
- package/src/sync/pending-sync-store.ts +1085 -0
- package/src/sync/rateless-iblt.ts +122 -132
- package/src/sync/simple.ts +334 -1144
- package/src/sync/sync-peer-state.ts +107 -0
- package/dist/benchmark/adaptive-ingest.d.ts +0 -2
- package/dist/benchmark/adaptive-ingest.d.ts.map +0 -1
- package/dist/benchmark/adaptive-ingest.js +0 -275
- package/dist/benchmark/adaptive-ingest.js.map +0 -1
- package/dist/benchmark/get-samples.d.ts +0 -2
- package/dist/benchmark/get-samples.d.ts.map +0 -1
- package/dist/benchmark/get-samples.js +0 -194
- package/dist/benchmark/get-samples.js.map +0 -1
- package/dist/benchmark/index.d.ts +0 -2
- package/dist/benchmark/index.d.ts.map +0 -1
- package/dist/benchmark/index.js +0 -224
- package/dist/benchmark/index.js.map +0 -1
- package/dist/benchmark/join-backfill-repair.d.ts +0 -2
- package/dist/benchmark/join-backfill-repair.d.ts.map +0 -1
- package/dist/benchmark/join-backfill-repair.js +0 -288
- package/dist/benchmark/join-backfill-repair.js.map +0 -1
- package/dist/benchmark/memory/child.d.ts +0 -2
- package/dist/benchmark/memory/child.d.ts.map +0 -1
- package/dist/benchmark/memory/child.js +0 -204
- package/dist/benchmark/memory/child.js.map +0 -1
- package/dist/benchmark/memory/index.d.ts +0 -2
- package/dist/benchmark/memory/index.d.ts.map +0 -1
- package/dist/benchmark/memory/index.js +0 -81
- package/dist/benchmark/memory/index.js.map +0 -1
- package/dist/benchmark/memory/utils.d.ts +0 -13
- package/dist/benchmark/memory/utils.d.ts.map +0 -1
- package/dist/benchmark/memory/utils.js +0 -2
- package/dist/benchmark/memory/utils.js.map +0 -1
- package/dist/benchmark/native-graph.d.ts +0 -2
- package/dist/benchmark/native-graph.d.ts.map +0 -1
- package/dist/benchmark/native-graph.js +0 -359
- package/dist/benchmark/native-graph.js.map +0 -1
- package/dist/benchmark/network-preset-e2e.d.ts +0 -2
- package/dist/benchmark/network-preset-e2e.d.ts.map +0 -1
- package/dist/benchmark/network-preset-e2e.js +0 -497
- package/dist/benchmark/network-preset-e2e.js.map +0 -1
- package/dist/benchmark/partial-sync.d.ts +0 -3
- package/dist/benchmark/partial-sync.d.ts.map +0 -1
- package/dist/benchmark/partial-sync.js +0 -121
- package/dist/benchmark/partial-sync.js.map +0 -1
- package/dist/benchmark/pid-convergence.d.ts +0 -2
- package/dist/benchmark/pid-convergence.d.ts.map +0 -1
- package/dist/benchmark/pid-convergence.js +0 -138
- package/dist/benchmark/pid-convergence.js.map +0 -1
- package/dist/benchmark/rateless-iblt-sender-startsync.d.ts +0 -2
- package/dist/benchmark/rateless-iblt-sender-startsync.d.ts.map +0 -1
- package/dist/benchmark/rateless-iblt-sender-startsync.js +0 -104
- package/dist/benchmark/rateless-iblt-sender-startsync.js.map +0 -1
- package/dist/benchmark/rateless-iblt-startsync-cache.d.ts +0 -2
- package/dist/benchmark/rateless-iblt-startsync-cache.d.ts.map +0 -1
- package/dist/benchmark/rateless-iblt-startsync-cache.js +0 -112
- package/dist/benchmark/rateless-iblt-startsync-cache.js.map +0 -1
- package/dist/benchmark/receive-prune.d.ts +0 -2
- package/dist/benchmark/receive-prune.d.ts.map +0 -1
- package/dist/benchmark/receive-prune.js +0 -818
- package/dist/benchmark/receive-prune.js.map +0 -1
- package/dist/benchmark/replication-prune.d.ts +0 -2
- package/dist/benchmark/replication-prune.d.ts.map +0 -1
- package/dist/benchmark/replication-prune.js +0 -105
- package/dist/benchmark/replication-prune.js.map +0 -1
- package/dist/benchmark/replication.d.ts +0 -2
- package/dist/benchmark/replication.d.ts.map +0 -1
- package/dist/benchmark/replication.js +0 -88
- package/dist/benchmark/replication.js.map +0 -1
- package/dist/benchmark/sync-batch-sweep.d.ts +0 -2
- package/dist/benchmark/sync-batch-sweep.d.ts.map +0 -1
- package/dist/benchmark/sync-batch-sweep.js +0 -353
- package/dist/benchmark/sync-batch-sweep.js.map +0 -1
- package/dist/benchmark/sync-catchup.d.ts +0 -2
- package/dist/benchmark/sync-catchup.d.ts.map +0 -1
- package/dist/benchmark/sync-catchup.js +0 -277
- package/dist/benchmark/sync-catchup.js.map +0 -1
- package/dist/benchmark/sync-phase-profile.d.ts +0 -2
- package/dist/benchmark/sync-phase-profile.d.ts.map +0 -1
- package/dist/benchmark/sync-phase-profile.js +0 -303
- package/dist/benchmark/sync-phase-profile.js.map +0 -1
- package/dist/benchmark/to-rebalance.d.ts +0 -2
- package/dist/benchmark/to-rebalance.d.ts.map +0 -1
- package/dist/benchmark/to-rebalance.js +0 -117
- package/dist/benchmark/to-rebalance.js.map +0 -1
- package/dist/benchmark/utils.d.ts +0 -24
- package/dist/benchmark/utils.d.ts.map +0 -1
- package/dist/benchmark/utils.js +0 -47
- package/dist/benchmark/utils.js.map +0 -1
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
const JOIN_WARMUP_SEND_SPACING_MS = 250;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One WarmupSession per join-warmup attempt window for a target. The
|
|
5
|
+
* instance IS the legacy opaque generation token — always compared by
|
|
6
|
+
* identity, never inspected — now carrying diagnostics. Rotation points are
|
|
7
|
+
* unchanged: created on demand, dropped at cancelJoinWarmupTarget so the
|
|
8
|
+
* next touch mints a fresh identity. Deliberately NOT hosted on
|
|
9
|
+
* PeerSession: warmup windows rotate independently of subscription epochs
|
|
10
|
+
* (poison and committed removals leave the subscription session alone), and
|
|
11
|
+
* a warmup target may have no session at all — auto-creating one as a
|
|
12
|
+
* warmup side effect would flip `isCurrent(peer, null)` from true to false
|
|
13
|
+
* at every receive-admission seat.
|
|
14
|
+
*/
|
|
15
|
+
export class WarmupSession {
|
|
16
|
+
readonly target: string;
|
|
17
|
+
readonly createdAt = Date.now(); // diagnostics only
|
|
18
|
+
|
|
19
|
+
constructor(target: string) {
|
|
20
|
+
this.target = target;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type JoinWarmupSendState<E> = {
|
|
25
|
+
bypassKnownPeerHints: boolean;
|
|
26
|
+
entries: Map<string, E>;
|
|
27
|
+
// Per-TARGET send state adopts the session it is currently draining for.
|
|
28
|
+
// lastCompletedAt spacing survives session rotation by design; do NOT
|
|
29
|
+
// move send state onto the session.
|
|
30
|
+
session: WarmupSession;
|
|
31
|
+
lastCompletedAt: number;
|
|
32
|
+
pending: boolean;
|
|
33
|
+
running: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type JoinWarmupRetryTimer = {
|
|
37
|
+
handle: ReturnType<typeof setTimeout>;
|
|
38
|
+
resolve?: () => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type JoinWarmupScheduledRetryBatch<E> = {
|
|
42
|
+
bypassKnownPeerHints: boolean;
|
|
43
|
+
entries: Map<string, E>;
|
|
44
|
+
remainingAttempts: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type JoinWarmupScheduledRetryCohort<E> = {
|
|
48
|
+
batches: JoinWarmupScheduledRetryBatch<E>[];
|
|
49
|
+
dueAt: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type JoinWarmupScheduledRetrySlot<E> = {
|
|
53
|
+
cohorts: JoinWarmupScheduledRetryCohort<E>[];
|
|
54
|
+
head: number;
|
|
55
|
+
timer?: JoinWarmupRetryTimer;
|
|
56
|
+
timerDueAt?: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type JoinWarmupScheduledRetries<E> = {
|
|
60
|
+
session: WarmupSession;
|
|
61
|
+
slotsByDelay: Map<number, JoinWarmupScheduledRetrySlot<E>>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type JoinWarmupDeps<E> = {
|
|
65
|
+
isLifecycleActive: (controller: AbortController) => boolean;
|
|
66
|
+
getCurrentLifecycleController: () => AbortController;
|
|
67
|
+
getRepairRetryTimers: () => Set<ReturnType<typeof setTimeout>>;
|
|
68
|
+
isClosed: () => boolean;
|
|
69
|
+
onTargetCancelled: (target: string) => void;
|
|
70
|
+
bumpSimpleFallbackPasses: () => void;
|
|
71
|
+
sendEntriesSimple: (
|
|
72
|
+
target: string,
|
|
73
|
+
entries: ReadonlyMap<string, E>,
|
|
74
|
+
options: {
|
|
75
|
+
bypassKnownPeers: boolean;
|
|
76
|
+
bypassRecentKnownPeers: boolean;
|
|
77
|
+
isStillCurrent: () => boolean;
|
|
78
|
+
signal: AbortSignal;
|
|
79
|
+
},
|
|
80
|
+
) => Promise<void>;
|
|
81
|
+
logError: (error: any) => void;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export class JoinWarmupCoordinator<E> {
|
|
85
|
+
_warmupSessionsByTarget!: Map<string, WarmupSession>;
|
|
86
|
+
_joinWarmupSendStateByTarget!: Map<string, JoinWarmupSendState<E>>;
|
|
87
|
+
_joinWarmupRetryTimersByTarget!: Map<string, Set<JoinWarmupRetryTimer>>;
|
|
88
|
+
_joinWarmupScheduledRetriesByTarget!: Map<
|
|
89
|
+
string,
|
|
90
|
+
JoinWarmupScheduledRetries<E>
|
|
91
|
+
>;
|
|
92
|
+
_repairSweepWarmupSessionByTarget!: Map<string, WarmupSession>;
|
|
93
|
+
|
|
94
|
+
constructor(private readonly deps: JoinWarmupDeps<E>) {
|
|
95
|
+
this.reset();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
reset() {
|
|
99
|
+
this._warmupSessionsByTarget = new Map();
|
|
100
|
+
this._joinWarmupSendStateByTarget = new Map();
|
|
101
|
+
this._joinWarmupRetryTimersByTarget = new Map();
|
|
102
|
+
this._joinWarmupScheduledRetriesByTarget = new Map();
|
|
103
|
+
this._repairSweepWarmupSessionByTarget = new Map();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ensureWarmupSession(target: string): WarmupSession {
|
|
107
|
+
let session = this._warmupSessionsByTarget.get(target);
|
|
108
|
+
if (!session) {
|
|
109
|
+
session = new WarmupSession(target);
|
|
110
|
+
this._warmupSessionsByTarget.set(target, session);
|
|
111
|
+
}
|
|
112
|
+
return session;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
clearRepairSweepWarmupSessions(): void {
|
|
117
|
+
this._repairSweepWarmupSessionByTarget.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
trackJoinWarmupTimer(target: string, timer: JoinWarmupRetryTimer) {
|
|
121
|
+
let timers = this._joinWarmupRetryTimersByTarget.get(target);
|
|
122
|
+
if (!timers) {
|
|
123
|
+
timers = new Set();
|
|
124
|
+
this._joinWarmupRetryTimersByTarget.set(target, timers);
|
|
125
|
+
}
|
|
126
|
+
timers.add(timer);
|
|
127
|
+
this.deps.getRepairRetryTimers().add(timer.handle);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
untrackJoinWarmupTimer(target: string, timer: JoinWarmupRetryTimer) {
|
|
131
|
+
this.deps.getRepairRetryTimers().delete(timer.handle);
|
|
132
|
+
const timers = this._joinWarmupRetryTimersByTarget.get(target);
|
|
133
|
+
if (!timers) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
timers.delete(timer);
|
|
137
|
+
if (timers.size === 0) {
|
|
138
|
+
this._joinWarmupRetryTimersByTarget.delete(target);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
cancelJoinWarmupTimers(target: string) {
|
|
143
|
+
const timers = this._joinWarmupRetryTimersByTarget.get(target);
|
|
144
|
+
if (!timers) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const timer of [...timers]) {
|
|
148
|
+
clearTimeout(timer.handle);
|
|
149
|
+
timer.resolve?.();
|
|
150
|
+
this.untrackJoinWarmupTimer(target, timer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
cancelJoinWarmupTarget(target: string) {
|
|
155
|
+
this._warmupSessionsByTarget.delete(target);
|
|
156
|
+
this.deps.onTargetCancelled(target);
|
|
157
|
+
this._repairSweepWarmupSessionByTarget.delete(target);
|
|
158
|
+
this.cancelJoinWarmupTimers(target);
|
|
159
|
+
this._joinWarmupScheduledRetriesByTarget.delete(target);
|
|
160
|
+
const state = this._joinWarmupSendStateByTarget.get(target);
|
|
161
|
+
if (!state) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
state.bypassKnownPeerHints = false;
|
|
165
|
+
state.entries.clear();
|
|
166
|
+
state.pending = false;
|
|
167
|
+
if (!state.running) {
|
|
168
|
+
this._joinWarmupSendStateByTarget.delete(target);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
cancelAllJoinWarmupTargets() {
|
|
173
|
+
const targets = new Set([
|
|
174
|
+
...this._warmupSessionsByTarget.keys(),
|
|
175
|
+
...this._joinWarmupRetryTimersByTarget.keys(),
|
|
176
|
+
...this._joinWarmupScheduledRetriesByTarget.keys(),
|
|
177
|
+
...this._joinWarmupSendStateByTarget.keys(),
|
|
178
|
+
]);
|
|
179
|
+
for (const target of targets) {
|
|
180
|
+
this.cancelJoinWarmupTarget(target);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async sleepJoinWarmupTracked(
|
|
185
|
+
target: string,
|
|
186
|
+
delayMs: number,
|
|
187
|
+
repairLifecycleController: AbortController,
|
|
188
|
+
) {
|
|
189
|
+
if (delayMs <= 0) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await new Promise<void>((resolve) => {
|
|
193
|
+
let settled = false;
|
|
194
|
+
let trackedTimer!: JoinWarmupRetryTimer;
|
|
195
|
+
const settle = () => {
|
|
196
|
+
if (settled) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
settled = true;
|
|
200
|
+
if (
|
|
201
|
+
repairLifecycleController ===
|
|
202
|
+
this.deps.getCurrentLifecycleController()
|
|
203
|
+
) {
|
|
204
|
+
this.untrackJoinWarmupTimer(target, trackedTimer);
|
|
205
|
+
}
|
|
206
|
+
resolve();
|
|
207
|
+
};
|
|
208
|
+
const handle = setTimeout(settle, delayMs);
|
|
209
|
+
handle.unref?.();
|
|
210
|
+
trackedTimer = { handle, resolve: settle };
|
|
211
|
+
this.trackJoinWarmupTimer(target, trackedTimer);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
scheduleJoinWarmupRetries(
|
|
216
|
+
target: string,
|
|
217
|
+
session: WarmupSession,
|
|
218
|
+
delaysMs: Iterable<number>,
|
|
219
|
+
entries: ReadonlyMap<string, E>,
|
|
220
|
+
bypassKnownPeerHints: boolean,
|
|
221
|
+
repairLifecycleController: AbortController = this.deps.getCurrentLifecycleController(),
|
|
222
|
+
) {
|
|
223
|
+
if (
|
|
224
|
+
!this.deps.isLifecycleActive(repairLifecycleController) ||
|
|
225
|
+
this._warmupSessionsByTarget.get(target) !== session
|
|
226
|
+
) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let scheduled = this._joinWarmupScheduledRetriesByTarget.get(target);
|
|
230
|
+
if (scheduled?.session !== session) {
|
|
231
|
+
this.cancelJoinWarmupTimers(target);
|
|
232
|
+
this._joinWarmupScheduledRetriesByTarget.delete(target);
|
|
233
|
+
scheduled = undefined;
|
|
234
|
+
}
|
|
235
|
+
if (!scheduled) {
|
|
236
|
+
scheduled = {
|
|
237
|
+
session,
|
|
238
|
+
slotsByDelay: new Map(),
|
|
239
|
+
};
|
|
240
|
+
this._joinWarmupScheduledRetriesByTarget.set(target, scheduled);
|
|
241
|
+
}
|
|
242
|
+
const delays = [...new Set(delaysMs)];
|
|
243
|
+
const batch: JoinWarmupScheduledRetryBatch<E> = {
|
|
244
|
+
bypassKnownPeerHints,
|
|
245
|
+
entries: new Map(entries),
|
|
246
|
+
remainingAttempts: delays.length,
|
|
247
|
+
};
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
for (const delayMs of delays) {
|
|
250
|
+
let slot = scheduled.slotsByDelay.get(delayMs);
|
|
251
|
+
if (!slot) {
|
|
252
|
+
slot = { cohorts: [], head: 0 };
|
|
253
|
+
scheduled.slotsByDelay.set(delayMs, slot);
|
|
254
|
+
}
|
|
255
|
+
const tail = slot.cohorts.at(-1);
|
|
256
|
+
const dueAt = Math.max(tail?.dueAt ?? 0, now + delayMs);
|
|
257
|
+
if (tail?.dueAt === dueAt) {
|
|
258
|
+
tail.batches.push(batch);
|
|
259
|
+
} else {
|
|
260
|
+
slot.cohorts.push({
|
|
261
|
+
batches: [batch],
|
|
262
|
+
dueAt,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
this.armJoinWarmupRetrySlot(
|
|
266
|
+
target,
|
|
267
|
+
scheduled,
|
|
268
|
+
delayMs,
|
|
269
|
+
slot,
|
|
270
|
+
repairLifecycleController,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
armJoinWarmupRetrySlot(
|
|
276
|
+
target: string,
|
|
277
|
+
scheduled: JoinWarmupScheduledRetries<E>,
|
|
278
|
+
delayMs: number,
|
|
279
|
+
slot: JoinWarmupScheduledRetrySlot<E>,
|
|
280
|
+
repairLifecycleController: AbortController,
|
|
281
|
+
) {
|
|
282
|
+
if (!this.deps.isLifecycleActive(repairLifecycleController)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const nextDueAt = slot.cohorts[slot.head]?.dueAt;
|
|
286
|
+
if (nextDueAt == null) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (slot.timer && slot.timerDueAt === nextDueAt) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (slot.timer) {
|
|
293
|
+
clearTimeout(slot.timer.handle);
|
|
294
|
+
this.untrackJoinWarmupTimer(target, slot.timer);
|
|
295
|
+
}
|
|
296
|
+
let trackedTimer!: JoinWarmupRetryTimer;
|
|
297
|
+
const handle = setTimeout(
|
|
298
|
+
() => {
|
|
299
|
+
if (!this.deps.isLifecycleActive(repairLifecycleController)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.untrackJoinWarmupTimer(target, trackedTimer);
|
|
303
|
+
if (slot.timer !== trackedTimer) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
slot.timer = undefined;
|
|
307
|
+
slot.timerDueAt = undefined;
|
|
308
|
+
const current = this._joinWarmupScheduledRetriesByTarget.get(target);
|
|
309
|
+
if (
|
|
310
|
+
current !== scheduled ||
|
|
311
|
+
current.slotsByDelay.get(delayMs) !== slot
|
|
312
|
+
) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const dueEntries = new Map<string, E>();
|
|
317
|
+
let bypassKnownPeerHints = false;
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
while (
|
|
320
|
+
slot.head < slot.cohorts.length &&
|
|
321
|
+
slot.cohorts[slot.head].dueAt <= now
|
|
322
|
+
) {
|
|
323
|
+
const cohort = slot.cohorts[slot.head++];
|
|
324
|
+
for (const batch of cohort.batches) {
|
|
325
|
+
for (const [hash, entry] of batch.entries) {
|
|
326
|
+
dueEntries.set(hash, entry);
|
|
327
|
+
}
|
|
328
|
+
bypassKnownPeerHints ||= batch.bypassKnownPeerHints;
|
|
329
|
+
batch.remainingAttempts -= 1;
|
|
330
|
+
if (batch.remainingAttempts === 0) {
|
|
331
|
+
batch.entries.clear();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
cohort.batches.length = 0;
|
|
335
|
+
}
|
|
336
|
+
if (
|
|
337
|
+
dueEntries.size > 0 &&
|
|
338
|
+
!this.deps.isClosed() &&
|
|
339
|
+
this._warmupSessionsByTarget.get(target) === scheduled.session
|
|
340
|
+
) {
|
|
341
|
+
this.queueJoinWarmupSend(
|
|
342
|
+
target,
|
|
343
|
+
scheduled.session,
|
|
344
|
+
dueEntries,
|
|
345
|
+
bypassKnownPeerHints,
|
|
346
|
+
repairLifecycleController,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (slot.head === slot.cohorts.length) {
|
|
350
|
+
current.slotsByDelay.delete(delayMs);
|
|
351
|
+
if (current.slotsByDelay.size === 0) {
|
|
352
|
+
this._joinWarmupScheduledRetriesByTarget.delete(target);
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (slot.head >= 1_024 && slot.head * 2 >= slot.cohorts.length) {
|
|
357
|
+
slot.cohorts = slot.cohorts.slice(slot.head);
|
|
358
|
+
slot.head = 0;
|
|
359
|
+
}
|
|
360
|
+
this.armJoinWarmupRetrySlot(
|
|
361
|
+
target,
|
|
362
|
+
current,
|
|
363
|
+
delayMs,
|
|
364
|
+
slot,
|
|
365
|
+
repairLifecycleController,
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
Math.max(0, nextDueAt - Date.now()),
|
|
369
|
+
);
|
|
370
|
+
handle.unref?.();
|
|
371
|
+
trackedTimer = { handle };
|
|
372
|
+
slot.timer = trackedTimer;
|
|
373
|
+
slot.timerDueAt = nextDueAt;
|
|
374
|
+
this.trackJoinWarmupTimer(target, trackedTimer);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
queueJoinWarmupSend(
|
|
378
|
+
target: string,
|
|
379
|
+
session: WarmupSession,
|
|
380
|
+
entries: ReadonlyMap<string, E>,
|
|
381
|
+
bypassKnownPeerHints: boolean,
|
|
382
|
+
repairLifecycleController: AbortController = this.deps.getCurrentLifecycleController(),
|
|
383
|
+
) {
|
|
384
|
+
if (
|
|
385
|
+
!this.deps.isLifecycleActive(repairLifecycleController) ||
|
|
386
|
+
this._warmupSessionsByTarget.get(target) !== session
|
|
387
|
+
) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
let state = this._joinWarmupSendStateByTarget.get(target);
|
|
391
|
+
if (!state) {
|
|
392
|
+
state = {
|
|
393
|
+
bypassKnownPeerHints: false,
|
|
394
|
+
entries: new Map(),
|
|
395
|
+
session,
|
|
396
|
+
lastCompletedAt: Number.NEGATIVE_INFINITY,
|
|
397
|
+
pending: false,
|
|
398
|
+
running: false,
|
|
399
|
+
};
|
|
400
|
+
this._joinWarmupSendStateByTarget.set(target, state);
|
|
401
|
+
} else if (state.session !== session) {
|
|
402
|
+
state.bypassKnownPeerHints = false;
|
|
403
|
+
state.entries.clear();
|
|
404
|
+
state.pending = false;
|
|
405
|
+
}
|
|
406
|
+
for (const [hash, entry] of entries) {
|
|
407
|
+
state.entries.set(hash, entry);
|
|
408
|
+
}
|
|
409
|
+
state.bypassKnownPeerHints ||= bypassKnownPeerHints;
|
|
410
|
+
state.session = session;
|
|
411
|
+
state.pending = true;
|
|
412
|
+
if (state.running) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
void this.drainJoinWarmupSends(
|
|
416
|
+
target,
|
|
417
|
+
state,
|
|
418
|
+
repairLifecycleController,
|
|
419
|
+
).catch((error: any) => {
|
|
420
|
+
if (this.deps.isLifecycleActive(repairLifecycleController)) {
|
|
421
|
+
this.deps.logError(error);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async drainJoinWarmupSends(
|
|
427
|
+
target: string,
|
|
428
|
+
state: JoinWarmupSendState<E>,
|
|
429
|
+
repairLifecycleController: AbortController,
|
|
430
|
+
) {
|
|
431
|
+
if (
|
|
432
|
+
state.running ||
|
|
433
|
+
!this.deps.isLifecycleActive(repairLifecycleController)
|
|
434
|
+
) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
state.running = true;
|
|
438
|
+
try {
|
|
439
|
+
while (state.pending) {
|
|
440
|
+
if (!this.deps.isLifecycleActive(repairLifecycleController)) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
state.pending = false;
|
|
444
|
+
const session = state.session;
|
|
445
|
+
const entries = new Map(state.entries);
|
|
446
|
+
state.entries.clear();
|
|
447
|
+
const bypassKnownPeerHints = state.bypassKnownPeerHints;
|
|
448
|
+
state.bypassKnownPeerHints = false;
|
|
449
|
+
const spacingMs = Math.max(
|
|
450
|
+
0,
|
|
451
|
+
state.lastCompletedAt + JOIN_WARMUP_SEND_SPACING_MS - Date.now(),
|
|
452
|
+
);
|
|
453
|
+
await this.sleepJoinWarmupTracked(
|
|
454
|
+
target,
|
|
455
|
+
spacingMs,
|
|
456
|
+
repairLifecycleController,
|
|
457
|
+
);
|
|
458
|
+
if (
|
|
459
|
+
!this.deps.isLifecycleActive(repairLifecycleController) ||
|
|
460
|
+
state.session !== session ||
|
|
461
|
+
this._warmupSessionsByTarget.get(target) !== session
|
|
462
|
+
) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (entries.size === 0) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
this.deps.bumpSimpleFallbackPasses();
|
|
469
|
+
try {
|
|
470
|
+
await this.deps.sendEntriesSimple(target, entries, {
|
|
471
|
+
bypassKnownPeers: bypassKnownPeerHints,
|
|
472
|
+
bypassRecentKnownPeers: bypassKnownPeerHints,
|
|
473
|
+
isStillCurrent: () =>
|
|
474
|
+
this.deps.isLifecycleActive(repairLifecycleController),
|
|
475
|
+
signal: repairLifecycleController.signal,
|
|
476
|
+
});
|
|
477
|
+
} catch (error: any) {
|
|
478
|
+
if (this.deps.isLifecycleActive(repairLifecycleController)) {
|
|
479
|
+
this.deps.logError(error);
|
|
480
|
+
}
|
|
481
|
+
} finally {
|
|
482
|
+
if (this.deps.isLifecycleActive(repairLifecycleController)) {
|
|
483
|
+
state.lastCompletedAt = Date.now();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
state.running = false;
|
|
489
|
+
if (this._joinWarmupSendStateByTarget.get(target) === state) {
|
|
490
|
+
const currentRepairLifecycleController =
|
|
491
|
+
this.deps.getCurrentLifecycleController();
|
|
492
|
+
if (
|
|
493
|
+
state.pending &&
|
|
494
|
+
this.deps.isLifecycleActive(currentRepairLifecycleController)
|
|
495
|
+
) {
|
|
496
|
+
void this.drainJoinWarmupSends(
|
|
497
|
+
target,
|
|
498
|
+
state,
|
|
499
|
+
currentRepairLifecycleController,
|
|
500
|
+
).catch((error: any) => this.deps.logError(error));
|
|
501
|
+
} else if (!this._warmupSessionsByTarget.has(target)) {
|
|
502
|
+
this._joinWarmupSendStateByTarget.delete(target);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Cache } from "@peerbit/cache";
|
|
2
|
+
|
|
3
|
+
type LeaderMap = Map<string, { intersecting: boolean }>;
|
|
4
|
+
|
|
5
|
+
const cloneLeaders = (leaders: LeaderMap): LeaderMap => {
|
|
6
|
+
const cloned: LeaderMap = new Map();
|
|
7
|
+
for (const [hash, meta] of leaders) {
|
|
8
|
+
cloned.set(hash, { intersecting: meta.intersecting });
|
|
9
|
+
}
|
|
10
|
+
return cloned;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Memoizes leader-election plans between replication-topology changes.
|
|
15
|
+
*
|
|
16
|
+
* Coherence is structural, not temporal: a private monotonic version is
|
|
17
|
+
* bumped (and the store wiped) on every invalidation, and an async fill is
|
|
18
|
+
* only stored when the version captured before the computation still holds —
|
|
19
|
+
* a plan computed across a topology change is returned to its caller but
|
|
20
|
+
* never cached. The TTL bounds wall-clock maturity flips that fire no event
|
|
21
|
+
* (a range re-crossing maturity after the dynamic roleAge regrew has no
|
|
22
|
+
* pending timer), so it is load-bearing for that edge.
|
|
23
|
+
*
|
|
24
|
+
* Callers mutate returned maps and their value objects, so every hit and
|
|
25
|
+
* every store clones.
|
|
26
|
+
*/
|
|
27
|
+
export class LeaderPlanCache {
|
|
28
|
+
private cache: Cache<{ leaders: LeaderMap }>;
|
|
29
|
+
private version = 0;
|
|
30
|
+
|
|
31
|
+
constructor(options: { max: number; ttl: number }) {
|
|
32
|
+
this.cache = new Cache(options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
invalidate(): void {
|
|
36
|
+
this.version++;
|
|
37
|
+
this.cache.clear();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get(key: string): LeaderMap | undefined {
|
|
41
|
+
const entry = this.cache.get(key);
|
|
42
|
+
return entry ? cloneLeaders(entry.leaders) : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Capture the version before an awaited plan computation. */
|
|
46
|
+
capture(): number {
|
|
47
|
+
return this.version;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Store only if no invalidation happened since `capturedVersion`. */
|
|
51
|
+
put(key: string, leaders: LeaderMap, capturedVersion: number): void {
|
|
52
|
+
if (capturedVersion !== this.version) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.cache.add(
|
|
56
|
+
key,
|
|
57
|
+
{ leaders: cloneLeaders(leaders) },
|
|
58
|
+
Math.max(1, leaders.size),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|