@peerbit/shared-log 13.1.16 → 13.1.17

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.
Files changed (39) hide show
  1. package/dist/benchmark/native-graph.d.ts +2 -0
  2. package/dist/benchmark/native-graph.d.ts.map +1 -0
  3. package/dist/benchmark/native-graph.js +249 -0
  4. package/dist/benchmark/native-graph.js.map +1 -0
  5. package/dist/benchmark/sync-batch-sweep.js +72 -24
  6. package/dist/benchmark/sync-batch-sweep.js.map +1 -1
  7. package/dist/benchmark/sync-phase-profile.d.ts +2 -0
  8. package/dist/benchmark/sync-phase-profile.d.ts.map +1 -0
  9. package/dist/benchmark/sync-phase-profile.js +303 -0
  10. package/dist/benchmark/sync-phase-profile.js.map +1 -0
  11. package/dist/src/checked-prune.d.ts +55 -0
  12. package/dist/src/checked-prune.d.ts.map +1 -0
  13. package/dist/src/checked-prune.js +244 -0
  14. package/dist/src/checked-prune.js.map +1 -0
  15. package/dist/src/index.d.ts +10 -9
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +265 -164
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/sync/index.d.ts +9 -1
  20. package/dist/src/sync/index.d.ts.map +1 -1
  21. package/dist/src/sync/profile.d.ts +3 -0
  22. package/dist/src/sync/profile.d.ts.map +1 -0
  23. package/dist/src/sync/profile.js +2 -0
  24. package/dist/src/sync/profile.js.map +1 -0
  25. package/dist/src/sync/rateless-iblt.d.ts +25 -11
  26. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  27. package/dist/src/sync/rateless-iblt.js +597 -106
  28. package/dist/src/sync/rateless-iblt.js.map +1 -1
  29. package/dist/src/sync/simple.d.ts +5 -0
  30. package/dist/src/sync/simple.d.ts.map +1 -1
  31. package/dist/src/sync/simple.js +224 -74
  32. package/dist/src/sync/simple.js.map +1 -1
  33. package/package.json +17 -13
  34. package/src/checked-prune.ts +331 -0
  35. package/src/index.ts +429 -282
  36. package/src/sync/index.ts +11 -1
  37. package/src/sync/profile.ts +9 -0
  38. package/src/sync/rateless-iblt.ts +768 -128
  39. package/src/sync/simple.ts +256 -94
package/dist/src/index.js CHANGED
@@ -44,12 +44,13 @@ import { ClosedError, Program } from "@peerbit/program";
44
44
  import { FanoutChannel, waitForSubscribers, } from "@peerbit/pubsub";
45
45
  import { SubscriptionEvent, UnsubcriptionEvent, } from "@peerbit/pubsub-interface";
46
46
  import { RPC } from "@peerbit/rpc";
47
- import { ACK_CONTROL_PRIORITY, AcknowledgeDelivery, AnyWhere, createRequestTransportContext, DataMessage, MessageHeader, NotStartedError, SilentDelivery, } from "@peerbit/stream-interface";
47
+ import { ACK_CONTROL_PRIORITY, AcknowledgeDelivery, AnyWhere, BACKGROUND_MESSAGE_PRIORITY, CONVERGENCE_MESSAGE_PRIORITY, createRequestTransportContext, DataMessage, MessageHeader, NotStartedError, SilentDelivery, } from "@peerbit/stream-interface";
48
48
  import { AbortError, TimeoutError, debounceAccumulator, debounceFixedInterval, waitFor, } from "@peerbit/time";
49
49
  import pDefer, {} from "p-defer";
50
50
  import PQueue from "p-queue";
51
51
  import { concat, fromString } from "uint8arrays";
52
52
  import { BlocksMessage } from "./blocks.js";
53
+ import { CheckedPruneCoordinator, } from "./checked-prune.js";
53
54
  import { CPUUsageIntervalLag } from "./cpu.js";
54
55
  import { debouncedAccumulatorMap, } from "./debounce.js";
55
56
  import { NoPeersError } from "./errors.js";
@@ -66,7 +67,7 @@ import {} from "./replication-domain.js";
66
67
  import { AbsoluteReplicas, AddedReplicationSegmentMessage, AllReplicatingSegmentsMessage, MinReplicas, ReplicationPingMessage, ReplicationError, RequestReplicationInfoMessage, ResponseRoleMessage, StoppedReplicating, decodeReplicas, encodeReplicas, maxReplicas, } from "./replication.js";
67
68
  import { Observer, Replicator } from "./role.js";
68
69
  import { RatelessIBLTSynchronizer } from "./sync/rateless-iblt.js";
69
- import { ConfirmEntriesMessage, SimpleSyncronizer } from "./sync/simple.js";
70
+ import { ConfirmEntriesMessage, SYNC_MESSAGE_PRIORITY, SimpleSyncronizer, } from "./sync/simple.js";
70
71
  import { groupByGid } from "./utils.js";
71
72
  const toLocalPublicSignKey = (key) => {
72
73
  if (typeof key === "string") {
@@ -238,6 +239,7 @@ export const WAIT_FOR_PRUNE_DELAY = 0;
238
239
  const PRUNE_DEBOUNCE_INTERVAL = 500;
239
240
  const CHECKED_PRUNE_RESEND_INTERVAL_MIN_MS = 250;
240
241
  const CHECKED_PRUNE_RESEND_INTERVAL_MAX_MS = 5_000;
242
+ const CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS = 120_000;
241
243
  const CHECKED_PRUNE_RETRY_MAX_ATTEMPTS = 3;
242
244
  const CHECKED_PRUNE_RETRY_MAX_DELAY_MS = 30_000;
243
245
  // DONT SET THIS ANY LOWER, because it will make the pid controller unstable as the system responses are not fast enough to updates from the pid controller
@@ -426,7 +428,10 @@ let SharedLog = (() => {
426
428
  _logProperties;
427
429
  _closeController;
428
430
  _respondToIHaveTimeout;
429
- _pendingDeletes;
431
+ _checkedPrune;
432
+ get _pendingDeletes() {
433
+ return this._checkedPrune.pendingDeletes;
434
+ }
430
435
  _pendingIHave;
431
436
  // public key hash to range id to range
432
437
  pendingMaturity; // map of peerId to timeout
@@ -453,9 +458,15 @@ let SharedLog = (() => {
453
458
  // A fn for debouncing the calls for pruning
454
459
  pruneDebouncedFn;
455
460
  responseToPruneDebouncedFn;
456
- _requestIPruneSent; // tracks entry hash to peer hash for requesting I prune messages
457
- _requestIPruneResponseReplicatorSet; // tracks entry hash to peer hash
458
- _checkedPruneRetries;
461
+ get _requestIPruneSent() {
462
+ return this._checkedPrune.requestIPruneSent;
463
+ }
464
+ get _requestIPruneResponseReplicatorSet() {
465
+ return this._checkedPrune.responseReplicatorSet;
466
+ }
467
+ get _checkedPruneRetries() {
468
+ return this._checkedPrune.retries;
469
+ }
459
470
  replicationChangeDebounceFn;
460
471
  _repairRetryTimers;
461
472
  _recentRepairDispatch;
@@ -606,7 +617,7 @@ let SharedLog = (() => {
606
617
  header: new MessageHeader({
607
618
  session: 0,
608
619
  mode: new AnyWhere(),
609
- priority: 0,
620
+ priority: BACKGROUND_MESSAGE_PRIORITY,
610
621
  }),
611
622
  });
612
623
  contextMessage.header.timestamp = envelope.timestamp;
@@ -634,7 +645,7 @@ let SharedLog = (() => {
634
645
  header: new MessageHeader({
635
646
  session: 0,
636
647
  mode: new AnyWhere(),
637
- priority: 0,
648
+ priority: BACKGROUND_MESSAGE_PRIORITY,
638
649
  }),
639
650
  });
640
651
  contextMessage.header.timestamp = detail.timestamp;
@@ -1347,7 +1358,7 @@ let SharedLog = (() => {
1347
1358
  await this.rpc.send(new StoppedReplicating({
1348
1359
  segmentIds: rangesToUnreplicate.map((x) => x.id),
1349
1360
  }), {
1350
- priority: 1,
1361
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1351
1362
  });
1352
1363
  }
1353
1364
  return rangesToReplicate;
@@ -1459,7 +1470,7 @@ let SharedLog = (() => {
1459
1470
  const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(segmentIds, this.node.identity.publicKey);
1460
1471
  await this.removeReplicationRanges(rangesToRemove, this.node.identity.publicKey);
1461
1472
  await this.rpc.send(new StoppedReplicating({ segmentIds }), {
1462
- priority: 1,
1473
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1463
1474
  });
1464
1475
  }
1465
1476
  async removeReplicator(key, options) {
@@ -1477,7 +1488,7 @@ let SharedLog = (() => {
1477
1488
  if (isMe) {
1478
1489
  // announce that we are no longer replicating
1479
1490
  await this.rpc.send(new AllReplicatingSegmentsMessage({ segments: [] }), {
1480
- priority: 1,
1491
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1481
1492
  });
1482
1493
  }
1483
1494
  if (options?.noEvent !== true) {
@@ -1840,7 +1851,7 @@ let SharedLog = (() => {
1840
1851
  }
1841
1852
  else {
1842
1853
  await this.rpc.send(message, {
1843
- priority: 1,
1854
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1844
1855
  });
1845
1856
  }
1846
1857
  }
@@ -2027,7 +2038,7 @@ let SharedLog = (() => {
2027
2038
  for (let i = 0; i < uniqueHashes.length; i += REPAIR_CONFIRMATION_HASH_BATCH_SIZE) {
2028
2039
  const chunk = uniqueHashes.slice(i, i + REPAIR_CONFIRMATION_HASH_BATCH_SIZE);
2029
2040
  await this.rpc.send(new ConfirmEntriesMessage({ hashes: chunk }), {
2030
- priority: 1,
2041
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2031
2042
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2032
2043
  });
2033
2044
  }
@@ -2036,7 +2047,7 @@ let SharedLog = (() => {
2036
2047
  for await (const message of createExchangeHeadsMessages(this.log, [...entries.keys()])) {
2037
2048
  message.reserved[0] |= EXCHANGE_HEADS_REPAIR_HINT;
2038
2049
  await this.rpc.send(message, {
2039
- priority: 1,
2050
+ priority: SYNC_MESSAGE_PRIORITY,
2040
2051
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2041
2052
  });
2042
2053
  }
@@ -2551,25 +2562,22 @@ let SharedLog = (() => {
2551
2562
  if (this.keep && (await this.keep(args.value.entry))) {
2552
2563
  return false;
2553
2564
  }
2565
+ this._checkedPrune.trackCandidate(args.key, args.value.entry, args.value.leaders);
2554
2566
  void this.pruneDebouncedFn.add(args);
2555
2567
  return true;
2556
2568
  }
2557
- async cancelCheckedPruneForLocalLeader(hash) {
2569
+ async cancelCheckedPruneForLocalLeader(hash, options) {
2558
2570
  this.pruneDebouncedFn.delete(hash);
2559
- this.clearCheckedPruneRetry(hash);
2560
- this.removePruneRequestSent(hash);
2561
- this._requestIPruneResponseReplicatorSet.delete(hash);
2562
- await this._pendingDeletes
2563
- .get(hash)
2564
- ?.reject(new Error("Failed to delete, is leader again"));
2571
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
2572
+ this._checkedPrune.markCancelled(hash, {
2573
+ preserveRetry: options?.preserveRetry,
2574
+ });
2575
+ await pendingDelete?.reject(new Error("Failed to delete, is leader again"));
2565
2576
  }
2566
2577
  hasActiveCheckedPruneWork(hash) {
2567
- return (this._pendingDeletes.has(hash) ||
2568
- this._requestIPruneSent.has(hash) ||
2569
- this._requestIPruneResponseReplicatorSet.has(hash) ||
2570
- this._checkedPruneRetries.has(hash));
2578
+ return this._checkedPrune.hasActiveWork(hash);
2571
2579
  }
2572
- async resolveCheckedPruneLeaders(args) {
2580
+ async revalidateCheckedPruneOwnership(args) {
2573
2581
  const selfHash = this.node.identity.publicKey.hashcode();
2574
2582
  if (args.leaders.has(selfHash)) {
2575
2583
  if (args.selfReplicating === false) {
@@ -2615,7 +2623,7 @@ let SharedLog = (() => {
2615
2623
  await this.cancelCheckedPruneForLocalLeader(entry.hash);
2616
2624
  continue;
2617
2625
  }
2618
- if (this._pendingDeletes.has(entry.hash)) {
2626
+ if (this._checkedPrune.hasPendingDelete(entry.hash)) {
2619
2627
  continue;
2620
2628
  }
2621
2629
  if (leaders.size === 0) {
@@ -2628,7 +2636,7 @@ let SharedLog = (() => {
2628
2636
  this.responseToPruneDebouncedFn.delete(entry.hash);
2629
2637
  }
2630
2638
  }
2631
- async pruneIndexedEntriesNoLongerLed() {
2639
+ async pruneIndexedEntriesNoLongerLed(options) {
2632
2640
  const selfHash = this.node.identity.publicKey.hashcode();
2633
2641
  const iterator = this.entryCoordinatesIndex.iterate({});
2634
2642
  let enqueuedPrune = false;
@@ -2640,12 +2648,12 @@ let SharedLog = (() => {
2640
2648
  if (this.closed) {
2641
2649
  continue;
2642
2650
  }
2643
- const leaders = await this.findLeaders(entryReplicated.coordinates, entryReplicated, { roleAge: 0 });
2651
+ const leaders = await this.findLeaders(entryReplicated.coordinates, entryReplicated, options?.useDefaultRoleAge ? undefined : { roleAge: 0 });
2644
2652
  if (leaders.has(selfHash)) {
2645
2653
  await this.cancelCheckedPruneForLocalLeader(entryReplicated.hash);
2646
2654
  continue;
2647
2655
  }
2648
- if (this._pendingDeletes.has(entryReplicated.hash)) {
2656
+ if (this._checkedPrune.hasPendingDelete(entryReplicated.hash)) {
2649
2657
  continue;
2650
2658
  }
2651
2659
  if (leaders.size === 0) {
@@ -2667,20 +2675,63 @@ let SharedLog = (() => {
2667
2675
  await this.pruneDebouncedFn.flush();
2668
2676
  }
2669
2677
  }
2670
- clearCheckedPruneRetry(hash) {
2671
- const state = this._checkedPruneRetries.get(hash);
2672
- if (state?.timer) {
2673
- clearTimeout(state.timer);
2678
+ async pruneCurrentHeadsNoLongerLed(options) {
2679
+ const selfHash = this.node.identity.publicKey.hashcode();
2680
+ const heads = await this.log.getHeads(true).all();
2681
+ let enqueuedPrune = false;
2682
+ for (const head of heads) {
2683
+ if (this.closed) {
2684
+ break;
2685
+ }
2686
+ const leaders = await this.findLeadersFromEntry(head, maxReplicas(this, [head]), options?.useDefaultRoleAge ? undefined : { roleAge: 0 });
2687
+ if (leaders.has(selfHash)) {
2688
+ await this.cancelCheckedPruneForLocalLeader(head.hash);
2689
+ continue;
2690
+ }
2691
+ if (this._checkedPrune.hasPendingDelete(head.hash)) {
2692
+ continue;
2693
+ }
2694
+ if (leaders.size === 0) {
2695
+ continue;
2696
+ }
2697
+ enqueuedPrune =
2698
+ (await this.pruneDebouncedFnAddIfNotKeeping({
2699
+ key: head.hash,
2700
+ value: { entry: head, leaders },
2701
+ })) || enqueuedPrune;
2702
+ this.responseToPruneDebouncedFn.delete(head.hash);
2703
+ }
2704
+ if (enqueuedPrune && !this.closed) {
2705
+ await this.pruneDebouncedFn.flush();
2706
+ }
2707
+ }
2708
+ checkedPruneLeadersToMap(leaders) {
2709
+ if (leaders instanceof Map) {
2710
+ return new Map(leaders);
2711
+ }
2712
+ const leadersMap = new Map();
2713
+ for (const leader of leaders) {
2714
+ leadersMap.set(leader, { intersecting: true });
2674
2715
  }
2675
- this._checkedPruneRetries.delete(hash);
2716
+ return leadersMap;
2717
+ }
2718
+ clearCheckedPruneRetry(hash) {
2719
+ this._checkedPrune.clearRetry(hash);
2676
2720
  }
2677
2721
  scheduleCheckedPruneRetry(args) {
2678
2722
  if (this.closed)
2679
2723
  return;
2680
- if (this._pendingDeletes.has(args.entry.hash))
2724
+ if (this._checkedPrune.hasPendingDelete(args.entry.hash))
2681
2725
  return;
2682
2726
  const hash = args.entry.hash;
2683
- const state = this._checkedPruneRetries.get(hash) ?? { attempts: 0 };
2727
+ const state = this._checkedPrune.getRetry(hash) ??
2728
+ {
2729
+ attempts: 0,
2730
+ entry: args.entry,
2731
+ leaders: args.leaders,
2732
+ };
2733
+ state.entry = args.entry;
2734
+ state.leaders = args.leaders;
2684
2735
  if (state.timer)
2685
2736
  return;
2686
2737
  if (state.attempts >= CHECKED_PRUNE_RETRY_MAX_ATTEMPTS) {
@@ -2693,17 +2744,19 @@ let SharedLog = (() => {
2693
2744
  const delayMs = Math.min(CHECKED_PRUNE_RETRY_MAX_DELAY_MS, 1_000 * 2 ** (attempt - 1) + jitterMs);
2694
2745
  state.attempts = attempt;
2695
2746
  state.timer = setTimeout(async () => {
2696
- const st = this._checkedPruneRetries.get(hash);
2747
+ const st = this._checkedPrune.getRetry(hash);
2697
2748
  if (st)
2698
2749
  st.timer = undefined;
2699
2750
  if (this.closed)
2700
2751
  return;
2701
- if (this._pendingDeletes.has(hash))
2752
+ if (this._checkedPrune.hasPendingDelete(hash))
2702
2753
  return;
2754
+ const retryEntry = st?.entry ?? args.entry;
2755
+ const retryLeaders = st?.leaders ?? args.leaders;
2703
2756
  let leadersMap;
2704
2757
  try {
2705
- const replicas = decodeReplicas(args.entry).getValue(this);
2706
- leadersMap = await this.findLeadersFromEntry(args.entry, replicas, {
2758
+ const replicas = decodeReplicas(retryEntry).getValue(this);
2759
+ leadersMap = await this.findLeadersFromEntry(retryEntry, replicas, {
2707
2760
  roleAge: 0,
2708
2761
  });
2709
2762
  }
@@ -2711,21 +2764,13 @@ let SharedLog = (() => {
2711
2764
  // Best-effort only.
2712
2765
  }
2713
2766
  if (!leadersMap || leadersMap.size === 0) {
2714
- if (args.leaders instanceof Map) {
2715
- leadersMap = args.leaders;
2716
- }
2717
- else {
2718
- leadersMap = new Map();
2719
- for (const k of args.leaders) {
2720
- leadersMap.set(k, { intersecting: true });
2721
- }
2722
- }
2767
+ leadersMap = this.checkedPruneLeadersToMap(retryLeaders);
2723
2768
  }
2724
2769
  try {
2725
2770
  const leadersForRetry = leadersMap ?? new Map();
2726
2771
  await this.pruneDebouncedFnAddIfNotKeeping({
2727
2772
  key: hash,
2728
- value: { entry: args.entry, leaders: leadersForRetry },
2773
+ value: { entry: retryEntry, leaders: leadersForRetry },
2729
2774
  });
2730
2775
  }
2731
2776
  catch {
@@ -2733,7 +2778,56 @@ let SharedLog = (() => {
2733
2778
  }
2734
2779
  }, delayMs);
2735
2780
  state.timer.unref?.();
2736
- this._checkedPruneRetries.set(hash, state);
2781
+ this._checkedPrune.setRetry(hash, state);
2782
+ }
2783
+ async recoverCheckedPruneFromLateResponses(hashes, publicKeyHash) {
2784
+ if (this.closed)
2785
+ return;
2786
+ const selfHash = this.node.identity.publicKey.hashcode();
2787
+ const toPrune = new Map();
2788
+ const responseStillApplies = [];
2789
+ for (const hash of hashes) {
2790
+ if (this.closed) {
2791
+ break;
2792
+ }
2793
+ if (this._checkedPrune.hasPendingDelete(hash)) {
2794
+ continue;
2795
+ }
2796
+ const retry = this._checkedPrune.clearRetryTimer(hash);
2797
+ if (!retry) {
2798
+ continue;
2799
+ }
2800
+ const entry = retry.entry;
2801
+ let leaders = this.checkedPruneLeadersToMap(retry.leaders);
2802
+ try {
2803
+ const currentLeaders = await this.findLeadersFromEntry(entry, decodeReplicas(entry).getValue(this), { roleAge: 0 });
2804
+ if (currentLeaders.size > 0) {
2805
+ leaders = currentLeaders;
2806
+ }
2807
+ }
2808
+ catch {
2809
+ // Best-effort only; the stored retry leaders came from a previous
2810
+ // checked-prune decision for this exact entry.
2811
+ }
2812
+ if (leaders.has(selfHash)) {
2813
+ await this.cancelCheckedPruneForLocalLeader(hash);
2814
+ continue;
2815
+ }
2816
+ if (leaders.size === 0) {
2817
+ continue;
2818
+ }
2819
+ toPrune.set(hash, { entry, leaders });
2820
+ if (leaders.has(publicKeyHash)) {
2821
+ responseStillApplies.push(hash);
2822
+ }
2823
+ }
2824
+ if (toPrune.size === 0) {
2825
+ return;
2826
+ }
2827
+ void Promise.allSettled(this.prune(toPrune));
2828
+ for (const hash of responseStillApplies) {
2829
+ void this._checkedPrune.getPendingDelete(hash)?.resolve(publicKeyHash);
2830
+ }
2737
2831
  }
2738
2832
  async append(data, options) {
2739
2833
  if (this._isAdaptiveReplicating) {
@@ -2841,7 +2935,7 @@ let SharedLog = (() => {
2841
2935
  : createReplicationDomainHash(options?.compatibility && options?.compatibility < 10 ? "u32" : "u64")(this);
2842
2936
  this.indexableDomain = createIndexableDomainFromResolution(this.domain.resolution);
2843
2937
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
2844
- this._pendingDeletes = new Map();
2938
+ this._checkedPrune = new CheckedPruneCoordinator();
2845
2939
  this._pendingIHave = new Map();
2846
2940
  this.latestReplicationInfoMessage = new Map();
2847
2941
  this._replicationInfoBlockedPeers = new Set();
@@ -2999,22 +3093,28 @@ let SharedLog = (() => {
2999
3093
  ],
3000
3094
  })) > 0;
3001
3095
  this._gidPeersHistory = new Map();
3002
- this._requestIPruneSent = new Map();
3003
- this._requestIPruneResponseReplicatorSet = new Map();
3004
- this._checkedPruneRetries = new Map();
3005
3096
  this.replicationChangeDebounceFn = debounceAggregationChanges((change) => this.onReplicationChange(change).then(() => this.rebalanceParticipationDebounced?.call()), this.distributionDebounceTime);
3006
3097
  this.pruneDebouncedFn = debouncedAccumulatorMap(async (map) => {
3007
3098
  const current = new Map();
3008
3099
  const selfReplicating = await this.isReplicating();
3009
3100
  for (const [hash, value] of map) {
3010
- const checkedPruneLeaders = await this.resolveCheckedPruneLeaders({
3101
+ const checkedPruneLeaders = await this.revalidateCheckedPruneOwnership({
3011
3102
  hash,
3012
3103
  entry: value.entry,
3013
3104
  leaders: value.leaders,
3014
3105
  selfReplicating,
3015
3106
  });
3016
3107
  if (checkedPruneLeaders.localLeader) {
3017
- await this.cancelCheckedPruneForLocalLeader(hash);
3108
+ const preserveRetry = this._checkedPrune.hasRetry(hash);
3109
+ await this.cancelCheckedPruneForLocalLeader(hash, {
3110
+ preserveRetry,
3111
+ });
3112
+ if (preserveRetry) {
3113
+ this.scheduleCheckedPruneRetry({
3114
+ entry: value.entry,
3115
+ leaders: checkedPruneLeaders.leaders,
3116
+ });
3117
+ }
3018
3118
  continue;
3019
3119
  }
3020
3120
  current.set(hash, {
@@ -3048,7 +3148,7 @@ let SharedLog = (() => {
3048
3148
  to: allRequestingPeers,
3049
3149
  redundancy: 1,
3050
3150
  }),
3051
- priority: 1,
3151
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3052
3152
  });
3053
3153
  }, () => {
3054
3154
  let accumulator = new Map();
@@ -3337,18 +3437,7 @@ let SharedLog = (() => {
3337
3437
  this.cancelReplicationInfoRequests(peerHash);
3338
3438
  this._replicatorLivenessFailures.delete(peerHash);
3339
3439
  this._replicatorLastActivityAt.delete(peerHash);
3340
- for (const [hash, peers] of this._requestIPruneSent) {
3341
- peers.delete(peerHash);
3342
- if (peers.size === 0) {
3343
- this._requestIPruneSent.delete(hash);
3344
- }
3345
- }
3346
- for (const [hash, peers] of this._requestIPruneResponseReplicatorSet) {
3347
- peers.delete(peerHash);
3348
- if (peers.size === 0) {
3349
- this._requestIPruneResponseReplicatorSet.delete(hash);
3350
- }
3351
- }
3440
+ this._checkedPrune.cleanupPeer(peerHash);
3352
3441
  }
3353
3442
  markReplicatorActivity(peerHash, now = Date.now()) {
3354
3443
  this._replicatorLastActivityAt.set(peerHash, now);
@@ -3394,14 +3483,14 @@ let SharedLog = (() => {
3394
3483
  const maxPeers = options?.maxPeers ?? 8;
3395
3484
  const self = this.node.identity.publicKey.hashcode();
3396
3485
  const seed = hashToSeed32(hash);
3397
- const hinted = this._requestIPruneResponseReplicatorSet.get(hash);
3486
+ const hinted = this._checkedPrune.getConfirmedReplicators(hash);
3398
3487
  if (hinted && hinted.size > 0) {
3399
3488
  const peers = [...hinted].filter((p) => p !== self);
3400
3489
  return peers.length > 0
3401
3490
  ? pickDeterministicSubset(peers, seed, maxPeers)
3402
3491
  : undefined;
3403
3492
  }
3404
- const contacted = this._requestIPruneSent.get(hash);
3493
+ const contacted = this._checkedPrune.getContactedReplicators(hash);
3405
3494
  if (contacted && contacted.size > 0) {
3406
3495
  const peers = [...contacted].filter((p) => p !== self);
3407
3496
  return peers.length > 0
@@ -3758,25 +3847,14 @@ let SharedLog = (() => {
3758
3847
  this._appendBackfillTimer = undefined;
3759
3848
  }
3760
3849
  this._appendBackfillPendingByTarget.clear();
3761
- for (const [_k, v] of this._pendingDeletes) {
3762
- v.clear();
3763
- v.promise.resolve(); // TODO or reject?
3764
- }
3765
3850
  for (const [_k, v] of this._pendingIHave) {
3766
3851
  v.clear();
3767
3852
  }
3768
- for (const [_k, v] of this._checkedPruneRetries) {
3769
- if (v.timer)
3770
- clearTimeout(v.timer);
3771
- }
3853
+ this._checkedPrune.close();
3772
3854
  await this.remoteBlocks.stop();
3773
- this._pendingDeletes.clear();
3774
3855
  this._pendingIHave.clear();
3775
- this._checkedPruneRetries.clear();
3776
3856
  this.latestReplicationInfoMessage.clear();
3777
3857
  this._gidPeersHistory.clear();
3778
- this._requestIPruneSent.clear();
3779
- this._requestIPruneResponseReplicatorSet.clear();
3780
3858
  // Cancel any pending debounced timers so they can't fire after we've torn down
3781
3859
  // indexes/RPC state.
3782
3860
  this.rebalanceParticipationDebounced?.close();
@@ -3827,7 +3905,7 @@ let SharedLog = (() => {
3827
3905
  try {
3828
3906
  await this.rpc
3829
3907
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
3830
- priority: 1,
3908
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3831
3909
  signal: abort.signal,
3832
3910
  })
3833
3911
  .catch(() => { });
@@ -3871,7 +3949,7 @@ let SharedLog = (() => {
3871
3949
  try {
3872
3950
  await this.rpc
3873
3951
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
3874
- priority: 1,
3952
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3875
3953
  signal: abort.signal,
3876
3954
  })
3877
3955
  .catch(() => { });
@@ -4014,7 +4092,7 @@ let SharedLog = (() => {
4014
4092
  for (const entry of entries) {
4015
4093
  this.pruneDebouncedFn.delete(entry.entry.hash);
4016
4094
  this.removePruneRequestSent(entry.entry.hash);
4017
- this._requestIPruneResponseReplicatorSet.delete(entry.entry.hash);
4095
+ this._checkedPrune.clearConfirmedReplicators(entry.entry.hash);
4018
4096
  if (fromIsLeader) {
4019
4097
  this.addPeersToGidPeerHistory(gid, [
4020
4098
  context.from.hashcode(),
@@ -4110,27 +4188,37 @@ let SharedLog = (() => {
4110
4188
  this.removeEntriesKnownByPeer([hash], from);
4111
4189
  // if we expect the remote to be owner of this entry because we are to prune ourselves, then we need to remove the remote
4112
4190
  // this is due to that the remote has previously indicated to be a replicator to help us prune but now has changed their mind
4113
- const outGoingPrunes = this._requestIPruneResponseReplicatorSet.get(hash);
4114
- if (outGoingPrunes) {
4115
- outGoingPrunes.delete(from);
4116
- }
4191
+ this._checkedPrune.removeConfirmedReplicator(hash, from);
4117
4192
  const indexedEntry = await this.log.entryIndex.getShallow(hash);
4118
4193
  let isLeader = false;
4119
- if (indexedEntry &&
4120
- !this._pendingDeletes.has(hash) &&
4121
- (await this.log.blocks.has(hash))) {
4122
- this.removePeerFromGidPeerHistory(context.from.hashcode(), indexedEntry.value.meta.gid);
4123
- await this._waitForReplicators(await this.createCoordinates(indexedEntry.value, decodeReplicas(indexedEntry.value).getValue(this)), indexedEntry.value, [
4124
- {
4125
- key: this.node.identity.publicKey.hashcode(),
4126
- replicator: true,
4127
- },
4128
- ], {
4129
- onLeader: (key) => {
4130
- isLeader =
4131
- isLeader || key === this.node.identity.publicKey.hashcode();
4132
- },
4133
- });
4194
+ if (indexedEntry && (await this.log.blocks.has(hash))) {
4195
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
4196
+ if (pendingDelete) {
4197
+ const ownership = await this.revalidateCheckedPruneOwnership({
4198
+ hash,
4199
+ entry: indexedEntry.value,
4200
+ leaders: new Map(),
4201
+ });
4202
+ if (ownership.localLeader) {
4203
+ await this.cancelCheckedPruneForLocalLeader(hash);
4204
+ isLeader = true;
4205
+ }
4206
+ }
4207
+ else {
4208
+ this.removePeerFromGidPeerHistory(context.from.hashcode(), indexedEntry.value.meta.gid);
4209
+ await this._waitForReplicators(await this.createCoordinates(indexedEntry.value, decodeReplicas(indexedEntry.value).getValue(this)), indexedEntry.value, [
4210
+ {
4211
+ key: this.node.identity.publicKey.hashcode(),
4212
+ replicator: true,
4213
+ },
4214
+ ], {
4215
+ onLeader: (key) => {
4216
+ isLeader =
4217
+ isLeader ||
4218
+ key === this.node.identity.publicKey.hashcode();
4219
+ },
4220
+ });
4221
+ }
4134
4222
  }
4135
4223
  if (isLeader) {
4136
4224
  hasAndIsLeader.push(hash);
@@ -4193,8 +4281,18 @@ let SharedLog = (() => {
4193
4281
  }
4194
4282
  }
4195
4283
  else if (msg instanceof ResponseIPrune) {
4284
+ const lateResponses = [];
4196
4285
  for (const hash of msg.hashes) {
4197
- this._pendingDeletes.get(hash)?.resolve(context.from.hashcode());
4286
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
4287
+ if (pendingDelete) {
4288
+ void pendingDelete.resolve(context.from.hashcode());
4289
+ }
4290
+ else {
4291
+ lateResponses.push(hash);
4292
+ }
4293
+ }
4294
+ if (lateResponses.length > 0) {
4295
+ void this.recoverCheckedPruneFromLateResponses(lateResponses, context.from.hashcode()).catch((error) => logger.error(error.toString()));
4198
4296
  }
4199
4297
  }
4200
4298
  else if (msg instanceof ConfirmEntriesMessage) {
@@ -4564,7 +4662,7 @@ let SharedLog = (() => {
4564
4662
  }
4565
4663
  if (messageToSend) {
4566
4664
  await this.rpc.send(messageToSend, {
4567
- priority: 1,
4665
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
4568
4666
  });
4569
4667
  }
4570
4668
  }
@@ -5123,8 +5221,8 @@ let SharedLog = (() => {
5123
5221
  };
5124
5222
  this._replicationInfoRequestByPeer.set(peerHash, state);
5125
5223
  const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
5126
- const maxAttempts = Math.min(5, this.waitForReplicatorRequestMaxAttempts ??
5127
- WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS);
5224
+ const maxAttempts = this.waitForReplicatorRequestMaxAttempts ??
5225
+ Math.max(WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS, Math.ceil(this.waitForReplicatorTimeout / intervalMs));
5128
5226
  const tick = () => {
5129
5227
  if (this.closed || this._closeController.signal.aborted) {
5130
5228
  this.cancelReplicationInfoRequests(peerHash);
@@ -5221,25 +5319,14 @@ let SharedLog = (() => {
5221
5319
  return new AbsoluteReplicas(maxValue);
5222
5320
  }
5223
5321
  removePruneRequestSent(hash, to) {
5224
- if (!to) {
5225
- this._requestIPruneSent.delete(hash);
5226
- }
5227
- else {
5228
- let set = this._requestIPruneSent.get(hash);
5229
- if (set) {
5230
- set.delete(to);
5231
- if (set.size === 0) {
5232
- this._requestIPruneSent.delete(hash);
5233
- }
5234
- }
5235
- }
5322
+ this._checkedPrune.removeRequestSent(hash, to);
5236
5323
  }
5237
5324
  prune(entries, options) {
5238
5325
  if (options?.unchecked) {
5239
5326
  return [...entries.values()].map((x) => {
5240
5327
  this._gidPeersHistory.delete(x.entry.meta.gid);
5241
5328
  this.removePruneRequestSent(x.entry.hash);
5242
- this._requestIPruneResponseReplicatorSet.delete(x.entry.hash);
5329
+ this._checkedPrune.clearConfirmedReplicators(x.entry.hash);
5243
5330
  return this.log.remove(x.entry, {
5244
5331
  recursively: true,
5245
5332
  });
@@ -5267,7 +5354,7 @@ let SharedLog = (() => {
5267
5354
  }
5268
5355
  set.push(entry.hash);
5269
5356
  }
5270
- const pendingPrev = this._pendingDeletes.get(entry.hash);
5357
+ const pendingPrev = this._checkedPrune.getPendingDelete(entry.hash);
5271
5358
  if (pendingPrev) {
5272
5359
  // If a background prune is already in-flight, an explicit prune request should
5273
5360
  // still respect the caller's timeout. Otherwise, tests (and user calls) can
@@ -5296,45 +5383,62 @@ let SharedLog = (() => {
5296
5383
  const minReplicas = decodeReplicas(entry);
5297
5384
  const deferredPromise = pDefer();
5298
5385
  const clear = () => {
5299
- const pending = this._pendingDeletes.get(entry.hash);
5386
+ const pending = this._checkedPrune.getPendingDelete(entry.hash);
5300
5387
  if (pending?.promise === deferredPromise) {
5301
- this._pendingDeletes.delete(entry.hash);
5388
+ this._checkedPrune.deletePendingDelete(entry.hash, pending);
5302
5389
  }
5303
5390
  clearTimeout(timeout);
5304
5391
  };
5305
5392
  const resolve = () => {
5306
- clear();
5393
+ clearTimeout(timeout);
5307
5394
  this.clearCheckedPruneRetry(entry.hash);
5308
5395
  cleanupTimer.push(setTimeout(async () => {
5309
5396
  this._gidPeersHistory.delete(entry.meta.gid);
5310
5397
  this.removePruneRequestSent(entry.hash);
5311
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
5312
- if (await this.isLeader({
5398
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
5399
+ const ownership = await this.revalidateCheckedPruneOwnership({
5400
+ hash: entry.hash,
5313
5401
  entry,
5314
- replicas: minReplicas.getValue(this),
5315
- })) {
5402
+ leaders: this.checkedPruneLeadersToMap(leaders),
5403
+ selfReplicating: true,
5404
+ });
5405
+ if (ownership.localLeader) {
5406
+ clear();
5407
+ if (!explicitTimeout) {
5408
+ this.scheduleCheckedPruneRetry({ entry, leaders });
5409
+ }
5316
5410
  deferredPromise.reject(new Error("Failed to delete, is leader again"));
5317
5411
  return;
5318
5412
  }
5413
+ this._checkedPrune.markRemoving(entry.hash);
5319
5414
  return this.log
5320
5415
  .remove(entry, {
5321
5416
  recursively: true,
5322
5417
  })
5323
5418
  .then(() => {
5419
+ clear();
5420
+ this._checkedPrune.markDone(entry.hash);
5324
5421
  deferredPromise.resolve();
5325
5422
  })
5326
5423
  .catch((e) => {
5424
+ clear();
5425
+ this._checkedPrune.markCancelled(entry.hash, {
5426
+ preserveRetry: false,
5427
+ });
5327
5428
  deferredPromise.reject(e);
5328
5429
  })
5329
5430
  .finally(async () => {
5330
5431
  this._gidPeersHistory.delete(entry.meta.gid);
5331
5432
  this.removePruneRequestSent(entry.hash);
5332
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
5433
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
5333
5434
  // TODO in the case we become leader again here we need to re-add the entry
5334
- if (await this.isLeader({
5435
+ const ownership = await this.revalidateCheckedPruneOwnership({
5436
+ hash: entry.hash,
5335
5437
  entry,
5336
- replicas: minReplicas.getValue(this),
5337
- })) {
5438
+ leaders: this.checkedPruneLeadersToMap(leaders),
5439
+ selfReplicating: true,
5440
+ });
5441
+ if (ownership.localLeader) {
5338
5442
  logger.error("Unexpected: Is leader after delete");
5339
5443
  }
5340
5444
  });
@@ -5345,11 +5449,9 @@ let SharedLog = (() => {
5345
5449
  const isCheckedPruneTimeout = e instanceof Error &&
5346
5450
  typeof e.message === "string" &&
5347
5451
  e.message.startsWith("Timeout for checked pruning");
5348
- if (explicitTimeout || !isCheckedPruneTimeout) {
5349
- this.clearCheckedPruneRetry(entry.hash);
5350
- }
5351
- this.removePruneRequestSent(entry.hash);
5352
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
5452
+ this._checkedPrune.markCancelled(entry.hash, {
5453
+ preserveRetry: !explicitTimeout && isCheckedPruneTimeout,
5454
+ });
5353
5455
  deferredPromise.reject(e);
5354
5456
  };
5355
5457
  let cursor = undefined;
@@ -5359,7 +5461,7 @@ let SharedLog = (() => {
5359
5461
  // If we time out too early we can end up with permanently prunable heads that never
5360
5462
  // get retried (a common CI flake in "prune before join" tests).
5361
5463
  const checkedPruneTimeoutMs = options?.timeout ??
5362
- Math.max(10_000, Number(this._respondToIHaveTimeout ?? 0) +
5464
+ Math.max(CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS, Number(this._respondToIHaveTimeout ?? 0) +
5363
5465
  this.waitForReplicatorTimeout +
5364
5466
  PRUNE_DEBOUNCE_INTERVAL * 2);
5365
5467
  const timeout = setTimeout(() => {
@@ -5372,7 +5474,7 @@ let SharedLog = (() => {
5372
5474
  reject(new Error(`Timeout for checked pruning after ${checkedPruneTimeoutMs}ms (closed=${this.closed})`));
5373
5475
  }, checkedPruneTimeoutMs);
5374
5476
  timeout.unref?.();
5375
- this._pendingDeletes.set(entry.hash, {
5477
+ this._checkedPrune.setPendingDelete(entry.hash, {
5376
5478
  promise: deferredPromise,
5377
5479
  clear,
5378
5480
  reject,
@@ -5392,34 +5494,24 @@ let SharedLog = (() => {
5392
5494
  }))) {
5393
5495
  return;
5394
5496
  }
5395
- let existCounter = this._requestIPruneResponseReplicatorSet.get(entry.hash);
5396
- if (!existCounter) {
5397
- existCounter = new Set();
5398
- this._requestIPruneResponseReplicatorSet.set(entry.hash, existCounter);
5399
- }
5400
- existCounter.add(publicKeyHash);
5497
+ const existCounter = this._checkedPrune.addConfirmedReplicator(entry.hash, publicKeyHash);
5401
5498
  // Seed provider hints so future remote reads can avoid extra round-trips.
5402
5499
  this.remoteBlocks.hintProviders(entry.hash, [publicKeyHash]);
5403
5500
  if (minReplicasValue <= existCounter.size) {
5404
5501
  resolve();
5405
5502
  }
5406
5503
  },
5407
- });
5504
+ }, entry, leaders);
5408
5505
  promises.push(deferredPromise.promise);
5409
5506
  }
5410
5507
  const emitMessages = async (entries, to) => {
5411
5508
  const filteredSet = [];
5412
5509
  for (const entry of entries) {
5413
- let set = this._requestIPruneSent.get(entry);
5414
- if (!set) {
5415
- set = new Set();
5416
- this._requestIPruneSent.set(entry, set);
5417
- }
5418
5510
  /* TODO why can we not have this statement?
5419
5511
  if (set.has(to)) {
5420
5512
  continue;
5421
5513
  } */
5422
- set.add(to);
5514
+ this._checkedPrune.addRequestSent(entry, to);
5423
5515
  filteredSet.push(entry);
5424
5516
  }
5425
5517
  if (filteredSet.length > 0) {
@@ -5430,7 +5522,7 @@ let SharedLog = (() => {
5430
5522
  to: [to], // TODO group by peers?
5431
5523
  redundancy: 1,
5432
5524
  }),
5433
- priority: 1,
5525
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
5434
5526
  });
5435
5527
  }
5436
5528
  };
@@ -5451,7 +5543,7 @@ let SharedLog = (() => {
5451
5543
  return;
5452
5544
  const pendingByPeer = [];
5453
5545
  for (const [peer, hashes] of peerToEntries) {
5454
- const pending = hashes.filter((h) => this._pendingDeletes.has(h));
5546
+ const pending = hashes.filter((h) => this._checkedPrune.hasPendingDelete(h));
5455
5547
  if (pending.length > 0) {
5456
5548
  pendingByPeer.push([peer, pending]);
5457
5549
  }
@@ -5777,12 +5869,21 @@ let SharedLog = (() => {
5777
5869
  for (const target of [...uncheckedDeliver.keys()]) {
5778
5870
  flushUncheckedDeliverTarget(target);
5779
5871
  }
5780
- if (this._isAdaptiveReplicating && hasSelfRangeRemoval) {
5781
- // Adaptive shrink/replacement can make already-indexed local heads
5782
- // prunable even when the incremental rebalance scan missed them under
5783
- // churn or timing pressure. Re-scan after repair dispatches are flushed
5784
- // so checked prune work is enqueued before callers wait for idle.
5785
- await this.pruneIndexedEntriesNoLongerLed();
5872
+ if (this._isAdaptiveReplicating &&
5873
+ (hasSelfRangeRemoval ||
5874
+ changes.some((change) => change.type === "added" ||
5875
+ change.type === "removed" ||
5876
+ change.type === "replaced"))) {
5877
+ // Adaptive range changes can make already-indexed local heads prunable
5878
+ // even when the incremental rebalance scan misses them under churn or
5879
+ // timing pressure. Re-scan after repair dispatches are flushed using the
5880
+ // mature-role view, which matches the bounded pruning contract.
5881
+ await this.pruneIndexedEntriesNoLongerLed({
5882
+ useDefaultRoleAge: true,
5883
+ });
5884
+ await this.pruneCurrentHeadsNoLongerLed({
5885
+ useDefaultRoleAge: true,
5886
+ });
5786
5887
  }
5787
5888
  return changed;
5788
5889
  }