@peerbit/shared-log 16.0.33 → 16.0.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/README.md +45 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +175 -20
- package/dist/src/index.js.map +1 -1
- package/dist/src/sync/index.d.ts +2 -2
- package/dist/src/sync/persisted-delivery-profile.d.ts +18 -0
- package/dist/src/sync/persisted-delivery-profile.d.ts.map +1 -0
- package/dist/src/sync/persisted-delivery-profile.js +77 -0
- package/dist/src/sync/persisted-delivery-profile.js.map +1 -0
- package/package.json +16 -16
- package/src/index.ts +229 -33
- package/src/sync/index.ts +2 -2
- package/src/sync/persisted-delivery-profile.ts +119 -0
package/dist/src/sync/index.d.ts
CHANGED
|
@@ -75,8 +75,8 @@ export type SyncOptions<R extends "u32" | "u64"> = {
|
|
|
75
75
|
/**
|
|
76
76
|
* Optional profiling callback. It is only invoked when provided, and should
|
|
77
77
|
* avoid blocking because it runs inline during open, provider resolution,
|
|
78
|
-
* and sync paths
|
|
79
|
-
* not replication or durable
|
|
78
|
+
* and sync paths, including bounded persisted-delivery observations.
|
|
79
|
+
* Profiling events are not replication/readiness barriers or durable receipts.
|
|
80
80
|
*/
|
|
81
81
|
profile?: SyncProfileFn;
|
|
82
82
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DiagnosticValue } from "@peerbit/diagnostics";
|
|
2
|
+
import type { SyncProfileFn } from "./profile.js";
|
|
3
|
+
export declare const PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT = 16;
|
|
4
|
+
/** Bounded, invocation-owned diagnostics. No retained entries or peer objects. */
|
|
5
|
+
export declare const createPersistedDeliveryProfile: (sink: SyncProfileFn | undefined, entries: number, minAcks: number, leaderDegree: number) => {
|
|
6
|
+
now: () => number;
|
|
7
|
+
emit: (name: string, details: Record<string, DiagnosticValue>, peer?: string, phaseStartedAt?: number) => void;
|
|
8
|
+
phase: (phase: string, options: {
|
|
9
|
+
round: number;
|
|
10
|
+
peer: string;
|
|
11
|
+
requestedEntries: number;
|
|
12
|
+
attempt?: number;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
startedAt?: number;
|
|
15
|
+
}) => (outcome: string, acceptedEntries?: number) => void;
|
|
16
|
+
finish: (details: Record<string, DiagnosticValue>) => void;
|
|
17
|
+
} | undefined;
|
|
18
|
+
//# sourceMappingURL=persisted-delivery-profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persisted-delivery-profile.d.ts","sourceRoot":"","sources":["../../../src/sync/persisted-delivery-profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,sBAAsB,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAKlD,eAAO,MAAM,sCAAsC,KAAK,CAAC;AAEzD,kFAAkF;AAClF,eAAO,MAAM,8BAA8B,GAC1C,MAAM,aAAa,GAAG,SAAS,EAC/B,SAAS,MAAM,EACf,SAAS,MAAM,EACf,cAAc,MAAM;;iBAqDb,MAAM,WACH,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,SACjC,MAAM,mBACI,MAAM;mBAcf,MAAM,WACJ;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,MASO,SAAS,MAAM,EAAE,kBAAkB,MAAM;sBAQhC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC;aAUlD,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { diagnosticNow } from "@peerbit/diagnostics";
|
|
2
|
+
// Process-local correlation only, never a receipt, wire id or persistent key.
|
|
3
|
+
let nextTrace = 0;
|
|
4
|
+
const MAX_EVENTS = 256;
|
|
5
|
+
export const PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT = 16;
|
|
6
|
+
/** Bounded, invocation-owned diagnostics. No retained entries or peer objects. */
|
|
7
|
+
export const createPersistedDeliveryProfile = (sink, entries, minAcks, leaderDegree) => {
|
|
8
|
+
if (!sink)
|
|
9
|
+
return undefined;
|
|
10
|
+
nextTrace = (nextTrace % Number.MAX_SAFE_INTEGER) + 1;
|
|
11
|
+
const traceId = `persisted:${nextTrace}`;
|
|
12
|
+
const startedAt = diagnosticNow();
|
|
13
|
+
let emitted = 0;
|
|
14
|
+
let dropped = 0;
|
|
15
|
+
let closed = false;
|
|
16
|
+
const send = (name, details, peer, phaseStartedAt = startedAt) => {
|
|
17
|
+
try {
|
|
18
|
+
const now = diagnosticNow();
|
|
19
|
+
const result = sink({
|
|
20
|
+
name: `sharedLog.persistedDelivery.${name}`,
|
|
21
|
+
component: "shared-log",
|
|
22
|
+
traceId,
|
|
23
|
+
entries,
|
|
24
|
+
peer,
|
|
25
|
+
durationMs: Math.max(0, now - phaseStartedAt),
|
|
26
|
+
details: {
|
|
27
|
+
...details,
|
|
28
|
+
v: 1,
|
|
29
|
+
minAcks,
|
|
30
|
+
leaderDegree,
|
|
31
|
+
entrySampleWindow: Math.min(entries, PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT),
|
|
32
|
+
entriesOutsideSampleWindow: Math.max(0, entries - PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT),
|
|
33
|
+
elapsedMs: Math.max(0, now - startedAt),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
// A void callback can still be async in TypeScript. Never await it, and
|
|
37
|
+
// own its rejection just as we isolate a synchronous diagnostic throw.
|
|
38
|
+
if (result &&
|
|
39
|
+
typeof result.then === "function") {
|
|
40
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Diagnostics must not enter protocol recovery or replace its outcome.
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const emit = (name, details, peer, phaseStartedAt) => {
|
|
48
|
+
if (closed)
|
|
49
|
+
return;
|
|
50
|
+
if (emitted >= MAX_EVENTS) {
|
|
51
|
+
dropped = Math.min(Number.MAX_SAFE_INTEGER, dropped + 1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
emitted++;
|
|
55
|
+
send(name, details, peer, phaseStartedAt);
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
now: diagnosticNow,
|
|
59
|
+
emit,
|
|
60
|
+
phase: (phase, options) => {
|
|
61
|
+
const { peer, startedAt = diagnosticNow(), ...details } = options;
|
|
62
|
+
emit("peerPhase", { ...details, phase, edge: "start", outcome: "pending" }, peer, startedAt);
|
|
63
|
+
return (outcome, acceptedEntries) => emit("peerPhase", { ...details, phase, edge: "end", outcome, acceptedEntries }, peer, startedAt);
|
|
64
|
+
},
|
|
65
|
+
finish: (details) => {
|
|
66
|
+
if (closed)
|
|
67
|
+
return;
|
|
68
|
+
closed = true;
|
|
69
|
+
send("settle", {
|
|
70
|
+
...details,
|
|
71
|
+
emittedEvents: emitted + 1,
|
|
72
|
+
droppedEvents: dropped,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=persisted-delivery-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persisted-delivery-profile.js","sourceRoot":"","sources":["../../../src/sync/persisted-delivery-profile.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peerbit/shared-log",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.35",
|
|
4
4
|
"description": "Shared log",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -62,37 +62,37 @@
|
|
|
62
62
|
"pidusage": "^4.0.1",
|
|
63
63
|
"pino": "^9.4.0",
|
|
64
64
|
"uint8arrays": "^5.1.0",
|
|
65
|
+
"@peerbit/any-store": "2.2.17",
|
|
66
|
+
"@peerbit/cache": "3.1.1",
|
|
65
67
|
"@peerbit/blocks": "4.3.2",
|
|
66
68
|
"@peerbit/blocks-interface": "2.2.1",
|
|
67
69
|
"@peerbit/crypto": "3.1.6",
|
|
68
|
-
"@peerbit/
|
|
69
|
-
"@peerbit/any-store": "2.2.17",
|
|
70
|
-
"@peerbit/indexer-interface": "3.0.13",
|
|
71
|
-
"@peerbit/indexer-sqlite3": "3.0.20",
|
|
70
|
+
"@peerbit/indexer-interface": "3.1.0",
|
|
72
71
|
"@peerbit/diagnostics": "0.0.1",
|
|
73
|
-
"@peerbit/log": "6.2.
|
|
74
|
-
"@peerbit/program": "6.0.61",
|
|
72
|
+
"@peerbit/log": "6.2.35",
|
|
75
73
|
"@peerbit/logger": "2.0.2",
|
|
76
|
-
"@peerbit/
|
|
77
|
-
"@peerbit/
|
|
74
|
+
"@peerbit/indexer-sqlite3": "3.0.21",
|
|
75
|
+
"@peerbit/program": "6.0.63",
|
|
78
76
|
"@peerbit/riblt": "1.2.0",
|
|
79
|
-
"@peerbit/
|
|
77
|
+
"@peerbit/pubsub": "5.4.8",
|
|
78
|
+
"@peerbit/rpc": "6.1.31",
|
|
79
|
+
"@peerbit/pubsub-interface": "5.2.2",
|
|
80
80
|
"@peerbit/stream-interface": "6.0.16",
|
|
81
81
|
"@peerbit/time": "3.0.1"
|
|
82
82
|
},
|
|
83
83
|
"optionalDependencies": {
|
|
84
|
-
"@peerbit/native-backbone": "0.2.
|
|
85
|
-
"@peerbit/shared-log-rust": "0.1.
|
|
84
|
+
"@peerbit/native-backbone": "0.2.17",
|
|
85
|
+
"@peerbit/shared-log-rust": "0.1.8"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
89
89
|
"@types/pidusage": "^2.0.5",
|
|
90
90
|
"uuid": "^11.1.1",
|
|
91
|
-
"@peerbit/indexer-rust": "1.0.14",
|
|
92
91
|
"@peerbit/any-store-rust": "0.1.7",
|
|
93
|
-
"@peerbit/indexer-
|
|
94
|
-
"@peerbit/
|
|
95
|
-
"peerbit": "
|
|
92
|
+
"@peerbit/indexer-rust": "1.0.15",
|
|
93
|
+
"@peerbit/indexer-simple": "1.3.0",
|
|
94
|
+
"@peerbit/test-utils": "3.1.44",
|
|
95
|
+
"peerbit": "5.4.5"
|
|
96
96
|
},
|
|
97
97
|
"repository": {
|
|
98
98
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -307,6 +307,10 @@ import type {
|
|
|
307
307
|
SynchronizerConstructor,
|
|
308
308
|
Syncronizer,
|
|
309
309
|
} from "./sync/index.js";
|
|
310
|
+
import {
|
|
311
|
+
PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT,
|
|
312
|
+
createPersistedDeliveryProfile,
|
|
313
|
+
} from "./sync/persisted-delivery-profile.js";
|
|
310
314
|
import {
|
|
311
315
|
emitSyncProfileDuration,
|
|
312
316
|
emitSyncProfileEvent,
|
|
@@ -6356,6 +6360,18 @@ export class SharedLog<
|
|
|
6356
6360
|
records.size,
|
|
6357
6361
|
);
|
|
6358
6362
|
const signal = deadline.signal;
|
|
6363
|
+
const profile = createPersistedDeliveryProfile(
|
|
6364
|
+
this._logProperties?.sync?.profile,
|
|
6365
|
+
records.size,
|
|
6366
|
+
minAcks,
|
|
6367
|
+
replicas,
|
|
6368
|
+
);
|
|
6369
|
+
const profiledHashes = profile
|
|
6370
|
+
? committedHashes.slice(0, PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT)
|
|
6371
|
+
: undefined;
|
|
6372
|
+
let profileRounds = 0;
|
|
6373
|
+
let profileOutcome = "failed";
|
|
6374
|
+
let profileReason: string | undefined;
|
|
6359
6375
|
const recoveryController = new AbortController();
|
|
6360
6376
|
const recoverySignal = AbortSignal.any([signal, recoveryController.signal]);
|
|
6361
6377
|
const recoveryByPeer = new Map<
|
|
@@ -6469,6 +6485,7 @@ export class SharedLog<
|
|
|
6469
6485
|
};
|
|
6470
6486
|
try {
|
|
6471
6487
|
while (true) {
|
|
6488
|
+
const profileRound = profile ? ++profileRounds : 0;
|
|
6472
6489
|
if (signal.aborted) {
|
|
6473
6490
|
throw signal.reason ?? new AbortError();
|
|
6474
6491
|
}
|
|
@@ -6524,13 +6541,20 @@ export class SharedLog<
|
|
|
6524
6541
|
const selfHash = this.node.identity.publicKey.hashcode();
|
|
6525
6542
|
if (needsInitialLeaderCheck) {
|
|
6526
6543
|
needsInitialLeaderCheck = false;
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6544
|
+
const noRemoteIndex = leadersByEntry.findIndex(
|
|
6545
|
+
(leaders) =>
|
|
6546
|
+
leaders.size === 0 ||
|
|
6547
|
+
(leaders.size === 1 && leaders.has(selfHash)),
|
|
6548
|
+
);
|
|
6549
|
+
if (noRemoteIndex >= 0) {
|
|
6550
|
+
if (noRemoteIndex < PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT)
|
|
6551
|
+
profile?.emit("plan", {
|
|
6552
|
+
round: profileRound,
|
|
6553
|
+
entryIndex: noRemoteIndex,
|
|
6554
|
+
remoteLeaderCount: 0,
|
|
6555
|
+
selectedRequestPeerCount: 0,
|
|
6556
|
+
carriedAckCount: 0,
|
|
6557
|
+
});
|
|
6534
6558
|
throw new NoPeersError(this.rpc.topic, "persisted");
|
|
6535
6559
|
}
|
|
6536
6560
|
}
|
|
@@ -6542,6 +6566,10 @@ export class SharedLog<
|
|
|
6542
6566
|
}
|
|
6543
6567
|
if (!isRoundOwnershipCurrent()) break;
|
|
6544
6568
|
const acknowledgements = carriedAcknowledgements.get(hash)!;
|
|
6569
|
+
const entryProfile =
|
|
6570
|
+
index < PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT
|
|
6571
|
+
? profile
|
|
6572
|
+
: undefined;
|
|
6545
6573
|
for (const [peer, captured] of acknowledgements) {
|
|
6546
6574
|
const current = this.persistedReceiptPeerSession(peer);
|
|
6547
6575
|
if (
|
|
@@ -6551,19 +6579,67 @@ export class SharedLog<
|
|
|
6551
6579
|
current.peerSession !== captured.peerSession
|
|
6552
6580
|
) {
|
|
6553
6581
|
acknowledgements.delete(peer);
|
|
6582
|
+
} else {
|
|
6583
|
+
entryProfile?.emit(
|
|
6584
|
+
"candidate",
|
|
6585
|
+
{
|
|
6586
|
+
round: profileRound,
|
|
6587
|
+
entryIndex: index,
|
|
6588
|
+
status: "carried-receipt",
|
|
6589
|
+
},
|
|
6590
|
+
peer,
|
|
6591
|
+
);
|
|
6554
6592
|
}
|
|
6555
6593
|
}
|
|
6556
|
-
if (acknowledgements.size >= minAcks)
|
|
6594
|
+
if (acknowledgements.size >= minAcks) {
|
|
6595
|
+
entryProfile?.emit("plan", {
|
|
6596
|
+
round: profileRound,
|
|
6597
|
+
entryIndex: index,
|
|
6598
|
+
remoteLeaderCount: leaders.size - Number(leaders.has(selfHash)),
|
|
6599
|
+
selectedRequestPeerCount: 0,
|
|
6600
|
+
carriedAckCount: acknowledgements.size,
|
|
6601
|
+
});
|
|
6602
|
+
continue;
|
|
6603
|
+
}
|
|
6604
|
+
let selectedRequestPeerCount = 0;
|
|
6557
6605
|
for (const peer of leaders.keys()) {
|
|
6558
6606
|
if (peer === selfHash) continue;
|
|
6559
6607
|
recoveryCandidates.add(peer);
|
|
6560
6608
|
const current = this.persistedReceiptPeerSession(peer);
|
|
6561
|
-
if (!current)
|
|
6609
|
+
if (!current) {
|
|
6610
|
+
entryProfile?.emit(
|
|
6611
|
+
"candidate",
|
|
6612
|
+
{
|
|
6613
|
+
round: profileRound,
|
|
6614
|
+
entryIndex: index,
|
|
6615
|
+
status: "leader-no-current-session",
|
|
6616
|
+
},
|
|
6617
|
+
peer,
|
|
6618
|
+
);
|
|
6619
|
+
continue;
|
|
6620
|
+
}
|
|
6562
6621
|
if (acknowledgements.has(peer)) continue;
|
|
6563
6622
|
const hashes = hashesByPeer.get(peer) ?? [];
|
|
6564
6623
|
hashes.push(hash);
|
|
6565
6624
|
hashesByPeer.set(peer, hashes);
|
|
6625
|
+
selectedRequestPeerCount++;
|
|
6626
|
+
entryProfile?.emit(
|
|
6627
|
+
"candidate",
|
|
6628
|
+
{
|
|
6629
|
+
round: profileRound,
|
|
6630
|
+
entryIndex: index,
|
|
6631
|
+
status: "selected-for-request",
|
|
6632
|
+
},
|
|
6633
|
+
peer,
|
|
6634
|
+
);
|
|
6566
6635
|
}
|
|
6636
|
+
entryProfile?.emit("plan", {
|
|
6637
|
+
round: profileRound,
|
|
6638
|
+
entryIndex: index,
|
|
6639
|
+
remoteLeaderCount: leaders.size - Number(leaders.has(selfHash)),
|
|
6640
|
+
selectedRequestPeerCount,
|
|
6641
|
+
carriedAckCount: acknowledgements.size,
|
|
6642
|
+
});
|
|
6567
6643
|
}
|
|
6568
6644
|
if (!isRoundOwnershipCurrent()) continue;
|
|
6569
6645
|
recoverSelectedPeers(recoveryCandidates);
|
|
@@ -6644,17 +6720,30 @@ export class SharedLog<
|
|
|
6644
6720
|
signal: roundSignal,
|
|
6645
6721
|
});
|
|
6646
6722
|
}
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6723
|
+
const confirmationTimeout = getAttemptTimeout();
|
|
6724
|
+
const endConfirmation = profile?.phase("confirmation", {
|
|
6725
|
+
round: profileRound,
|
|
6726
|
+
requestedEntries: hashes.length,
|
|
6727
|
+
timeoutMs: confirmationTimeout,
|
|
6728
|
+
peer,
|
|
6729
|
+
});
|
|
6730
|
+
try {
|
|
6731
|
+
await this._v2Send.confirmLatestForPeer(
|
|
6732
|
+
{
|
|
6733
|
+
peerHash: peer,
|
|
6734
|
+
peerSession: captured.peerSession,
|
|
6735
|
+
receiverTransportSession: captured.capabilitySession,
|
|
6736
|
+
},
|
|
6737
|
+
{
|
|
6738
|
+
timeout: confirmationTimeout,
|
|
6739
|
+
signal: roundSignal,
|
|
6740
|
+
},
|
|
6741
|
+
);
|
|
6742
|
+
endConfirmation?.("fulfilled");
|
|
6743
|
+
} catch (error) {
|
|
6744
|
+
endConfirmation?.("rejected");
|
|
6745
|
+
throw error;
|
|
6746
|
+
}
|
|
6658
6747
|
} catch {
|
|
6659
6748
|
// The exact receiver generation did not prove that it applied
|
|
6660
6749
|
// our latest role state. Replan instead of transferring to a
|
|
@@ -6691,13 +6780,32 @@ export class SharedLog<
|
|
|
6691
6780
|
}
|
|
6692
6781
|
},
|
|
6693
6782
|
onChunkSent: async (chunk) => {
|
|
6694
|
-
|
|
6783
|
+
const startedAt = profile?.now();
|
|
6784
|
+
const pending = this.waitForPersistedTransferAdmission(
|
|
6695
6785
|
peer,
|
|
6696
6786
|
chunk,
|
|
6697
6787
|
captured,
|
|
6698
6788
|
roundSignal,
|
|
6699
6789
|
isPeerRoundCurrent,
|
|
6700
6790
|
);
|
|
6791
|
+
const endAdmission = profile?.phase(
|
|
6792
|
+
"transfer-admission",
|
|
6793
|
+
{
|
|
6794
|
+
round: profileRound,
|
|
6795
|
+
peer,
|
|
6796
|
+
requestedEntries: chunk.length,
|
|
6797
|
+
startedAt,
|
|
6798
|
+
},
|
|
6799
|
+
);
|
|
6800
|
+
if (endAdmission)
|
|
6801
|
+
void pending.then(
|
|
6802
|
+
(admitted) =>
|
|
6803
|
+
endAdmission(
|
|
6804
|
+
admitted ? "fulfilled" : "not-admitted",
|
|
6805
|
+
),
|
|
6806
|
+
() => endAdmission("rejected"),
|
|
6807
|
+
);
|
|
6808
|
+
return pending;
|
|
6701
6809
|
},
|
|
6702
6810
|
operationQueue,
|
|
6703
6811
|
priority: delivery.priority,
|
|
@@ -6742,19 +6850,37 @@ export class SharedLog<
|
|
|
6742
6850
|
break;
|
|
6743
6851
|
}
|
|
6744
6852
|
let responses;
|
|
6853
|
+
const attempt = offset / PERSISTED_RECEIPT_CHUNK_SIZE + 1;
|
|
6854
|
+
let requestStartedAt: number | undefined;
|
|
6855
|
+
let endRequest:
|
|
6856
|
+
| ((outcome: string, acceptedEntries?: number) => void)
|
|
6857
|
+
| undefined;
|
|
6745
6858
|
try {
|
|
6746
|
-
|
|
6859
|
+
const endEgress = profile?.phase("receipt-egress", {
|
|
6860
|
+
round: profileRound,
|
|
6861
|
+
attempt,
|
|
6862
|
+
requestedEntries: requestedHashes.length,
|
|
6747
6863
|
peer,
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6864
|
+
});
|
|
6865
|
+
try {
|
|
6866
|
+
await this.waitForPersistedReceiptEgressAdmission(
|
|
6867
|
+
peer,
|
|
6868
|
+
captured.capabilitySession,
|
|
6869
|
+
requestedHashes.length,
|
|
6870
|
+
roundSignal,
|
|
6871
|
+
);
|
|
6872
|
+
endEgress?.("fulfilled");
|
|
6873
|
+
} catch (error) {
|
|
6874
|
+
endEgress?.("rejected");
|
|
6875
|
+
throw error;
|
|
6876
|
+
}
|
|
6752
6877
|
if (!isPeerRoundCurrent()) break;
|
|
6753
6878
|
const attemptTimeout = getAttemptTimeout();
|
|
6754
6879
|
responses =
|
|
6755
6880
|
(await operationQueue.add(async () => {
|
|
6756
6881
|
if (!isPeerRoundCurrent()) return [];
|
|
6757
|
-
|
|
6882
|
+
requestStartedAt = profile?.now();
|
|
6883
|
+
const pending = this.rpc.request(
|
|
6758
6884
|
new RequestPersistedEntriesV1({
|
|
6759
6885
|
expectedReceiverSession: captured.capabilitySession,
|
|
6760
6886
|
hashes: requestedHashes,
|
|
@@ -6770,14 +6896,25 @@ export class SharedLog<
|
|
|
6770
6896
|
signal: roundSignal,
|
|
6771
6897
|
},
|
|
6772
6898
|
);
|
|
6899
|
+
endRequest = profile?.phase("receipt-request", {
|
|
6900
|
+
round: profileRound,
|
|
6901
|
+
attempt,
|
|
6902
|
+
requestedEntries: requestedHashes.length,
|
|
6903
|
+
timeoutMs: attemptTimeout,
|
|
6904
|
+
peer,
|
|
6905
|
+
startedAt: requestStartedAt,
|
|
6906
|
+
});
|
|
6907
|
+
return pending;
|
|
6773
6908
|
})) ?? [];
|
|
6774
6909
|
} catch {
|
|
6910
|
+
endRequest?.("rejected", 0);
|
|
6775
6911
|
if (roundSignal.aborted) break;
|
|
6776
6912
|
// A peer can disconnect or miss this retry while the overall
|
|
6777
6913
|
// quorum deadline remains active. Replan on the next round.
|
|
6778
6914
|
break;
|
|
6779
6915
|
}
|
|
6780
6916
|
if (!isRoundOwnershipCurrent()) {
|
|
6917
|
+
endRequest?.("stale", 0);
|
|
6781
6918
|
roundController.abort();
|
|
6782
6919
|
break;
|
|
6783
6920
|
}
|
|
@@ -6788,6 +6925,7 @@ export class SharedLog<
|
|
|
6788
6925
|
current.peerSession !== captured.peerSession
|
|
6789
6926
|
) {
|
|
6790
6927
|
purgePeerDeliveryState(peer);
|
|
6928
|
+
endRequest?.("stale", 0);
|
|
6791
6929
|
break;
|
|
6792
6930
|
}
|
|
6793
6931
|
const requested = new Set(requestedHashes);
|
|
@@ -6824,6 +6962,26 @@ export class SharedLog<
|
|
|
6824
6962
|
}
|
|
6825
6963
|
}
|
|
6826
6964
|
}
|
|
6965
|
+
endRequest?.("fulfilled", confirmed.size);
|
|
6966
|
+
for (
|
|
6967
|
+
let entryIndex = 0;
|
|
6968
|
+
entryIndex < (profiledHashes?.length ?? 0);
|
|
6969
|
+
entryIndex++
|
|
6970
|
+
) {
|
|
6971
|
+
const hash = profiledHashes![entryIndex]!;
|
|
6972
|
+
if (confirmed.has(hash))
|
|
6973
|
+
profile?.emit(
|
|
6974
|
+
"progress",
|
|
6975
|
+
{
|
|
6976
|
+
round: profileRound,
|
|
6977
|
+
attempt,
|
|
6978
|
+
entryIndex,
|
|
6979
|
+
carriedAckCount:
|
|
6980
|
+
carriedAcknowledgements.get(hash)!.size,
|
|
6981
|
+
},
|
|
6982
|
+
peer,
|
|
6983
|
+
);
|
|
6984
|
+
}
|
|
6827
6985
|
}
|
|
6828
6986
|
})()
|
|
6829
6987
|
.then(() => undefined)
|
|
@@ -6891,10 +7049,12 @@ export class SharedLog<
|
|
|
6891
7049
|
while (requests.size > 0) {
|
|
6892
7050
|
await Promise.race(requests);
|
|
6893
7051
|
if (await roundComplete()) {
|
|
7052
|
+
profileOutcome = "quorum-validated";
|
|
6894
7053
|
return;
|
|
6895
7054
|
}
|
|
6896
7055
|
}
|
|
6897
7056
|
if (await roundComplete()) {
|
|
7057
|
+
profileOutcome = "quorum-validated";
|
|
6898
7058
|
return;
|
|
6899
7059
|
}
|
|
6900
7060
|
} finally {
|
|
@@ -6922,6 +7082,16 @@ export class SharedLog<
|
|
|
6922
7082
|
);
|
|
6923
7083
|
}
|
|
6924
7084
|
} catch (error) {
|
|
7085
|
+
profileReason =
|
|
7086
|
+
signal.reason instanceof TimeoutError
|
|
7087
|
+
? "timeout"
|
|
7088
|
+
: signal.aborted
|
|
7089
|
+
? "signal"
|
|
7090
|
+
: error instanceof NoPeersError
|
|
7091
|
+
? "no-peers"
|
|
7092
|
+
: "error";
|
|
7093
|
+
profileOutcome =
|
|
7094
|
+
signal.aborted && profileReason !== "timeout" ? "aborted" : "failed";
|
|
6925
7095
|
if (error instanceof PersistedDeliveryError) {
|
|
6926
7096
|
throw error;
|
|
6927
7097
|
}
|
|
@@ -6933,6 +7103,11 @@ export class SharedLog<
|
|
|
6933
7103
|
state.controller.abort();
|
|
6934
7104
|
}
|
|
6935
7105
|
if (ownedDeadline) deadline.dispose();
|
|
7106
|
+
profile?.finish({
|
|
7107
|
+
outcome: profileOutcome,
|
|
7108
|
+
reason: profileReason,
|
|
7109
|
+
round: profileRounds,
|
|
7110
|
+
});
|
|
6936
7111
|
}
|
|
6937
7112
|
}
|
|
6938
7113
|
|
|
@@ -27046,8 +27221,9 @@ export class SharedLog<
|
|
|
27046
27221
|
);
|
|
27047
27222
|
}
|
|
27048
27223
|
|
|
27224
|
+
let fullReplicaPlan;
|
|
27049
27225
|
if (!options?.candidates) {
|
|
27050
|
-
|
|
27226
|
+
fullReplicaPlan = await this.findFullReplicaLeaderPlan(
|
|
27051
27227
|
cursors.length,
|
|
27052
27228
|
context.roleAge,
|
|
27053
27229
|
peerFilter,
|
|
@@ -27055,8 +27231,8 @@ export class SharedLog<
|
|
|
27055
27231
|
this.throwIfReplicationOwnershipLifecycleInactive(
|
|
27056
27232
|
ownershipLifecycleController,
|
|
27057
27233
|
);
|
|
27058
|
-
if (
|
|
27059
|
-
return
|
|
27234
|
+
if (fullReplicaPlan?.complete) {
|
|
27235
|
+
return fullReplicaPlan.leaders;
|
|
27060
27236
|
}
|
|
27061
27237
|
}
|
|
27062
27238
|
|
|
@@ -27073,6 +27249,16 @@ export class SharedLog<
|
|
|
27073
27249
|
this.throwIfReplicationOwnershipLifecycleInactive(
|
|
27074
27250
|
ownershipLifecycleController,
|
|
27075
27251
|
);
|
|
27252
|
+
if (fullReplicaPlan) {
|
|
27253
|
+
// Retain the existing mature global fallback (including strict owners).
|
|
27254
|
+
// Young owners still need coordinate sampling before the plan is complete.
|
|
27255
|
+
for (const [hash, leader] of leaders) {
|
|
27256
|
+
if (!fullReplicaPlan.leaders.has(hash)) {
|
|
27257
|
+
fullReplicaPlan.leaders.set(hash, leader);
|
|
27258
|
+
}
|
|
27259
|
+
}
|
|
27260
|
+
return fullReplicaPlan.leaders;
|
|
27261
|
+
}
|
|
27076
27262
|
return leaders;
|
|
27077
27263
|
}
|
|
27078
27264
|
|
|
@@ -27117,16 +27303,20 @@ export class SharedLog<
|
|
|
27117
27303
|
return peerFilter;
|
|
27118
27304
|
}
|
|
27119
27305
|
|
|
27120
|
-
private async
|
|
27306
|
+
private async findFullReplicaLeaderPlan(
|
|
27121
27307
|
replicas: number,
|
|
27122
27308
|
roleAge: number,
|
|
27123
27309
|
peerFilter?: Set<string>,
|
|
27124
|
-
): Promise<
|
|
27310
|
+
): Promise<
|
|
27311
|
+
| { leaders: Map<string, { intersecting: boolean }>; complete: boolean }
|
|
27312
|
+
| undefined
|
|
27313
|
+
> {
|
|
27125
27314
|
const now = Date.now();
|
|
27126
27315
|
const leaders = new Map<string, { intersecting: boolean }>();
|
|
27127
27316
|
// Strict-only peers are not global fallbacks, but may still own an entry's
|
|
27128
27317
|
// coordinates. Remember them so a partial fallback cannot bypass sampling.
|
|
27129
27318
|
const excludedStrictPeers = new Set<string>();
|
|
27319
|
+
const youngPeers = new Set<string>();
|
|
27130
27320
|
const includeStrict =
|
|
27131
27321
|
this._logProperties?.strictFullReplicaFallback !== false;
|
|
27132
27322
|
const iterator = this.replicationIndex.iterate(
|
|
@@ -27150,6 +27340,7 @@ export class SharedLog<
|
|
|
27150
27340
|
continue;
|
|
27151
27341
|
}
|
|
27152
27342
|
if (!isMatured(range, now, roleAge)) {
|
|
27343
|
+
youngPeers.add(range.hash);
|
|
27153
27344
|
continue;
|
|
27154
27345
|
}
|
|
27155
27346
|
leaders.set(range.hash, { intersecting: true });
|
|
@@ -27168,7 +27359,12 @@ export class SharedLog<
|
|
|
27168
27359
|
}
|
|
27169
27360
|
}
|
|
27170
27361
|
|
|
27171
|
-
return leaders.size > 0
|
|
27362
|
+
return leaders.size > 0
|
|
27363
|
+
? {
|
|
27364
|
+
leaders,
|
|
27365
|
+
complete: [...youngPeers].every((hash) => leaders.has(hash)),
|
|
27366
|
+
}
|
|
27367
|
+
: undefined;
|
|
27172
27368
|
}
|
|
27173
27369
|
|
|
27174
27370
|
private async findEntryReplicatedLeaderBatch(
|
package/src/sync/index.ts
CHANGED
|
@@ -94,8 +94,8 @@ export type SyncOptions<R extends "u32" | "u64"> = {
|
|
|
94
94
|
/**
|
|
95
95
|
* Optional profiling callback. It is only invoked when provided, and should
|
|
96
96
|
* avoid blocking because it runs inline during open, provider resolution,
|
|
97
|
-
* and sync paths
|
|
98
|
-
* not replication or durable
|
|
97
|
+
* and sync paths, including bounded persisted-delivery observations.
|
|
98
|
+
* Profiling events are not replication/readiness barriers or durable receipts.
|
|
99
99
|
*/
|
|
100
100
|
profile?: SyncProfileFn;
|
|
101
101
|
};
|