@peerbit/shared-log 13.0.8 → 13.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/shared-log",
3
- "version": "13.0.8",
3
+ "version": "13.0.10",
4
4
  "description": "Shared log",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -61,33 +61,35 @@
61
61
  "pidusage": "^4.0.1",
62
62
  "pino": "^9.4.0",
63
63
  "uint8arrays": "^5.1.0",
64
- "@peerbit/log": "6.0.7",
65
- "@peerbit/logger": "2.0.0",
66
- "@peerbit/program": "6.0.5",
67
64
  "@peerbit/riblt": "1.2.0",
68
- "@peerbit/rpc": "6.0.7",
65
+ "@peerbit/log": "6.0.9",
66
+ "@peerbit/program": "6.0.6",
67
+ "@peerbit/rpc": "6.0.9",
68
+ "@peerbit/logger": "2.0.0",
69
69
  "@peerbit/any-store": "2.2.5",
70
- "@peerbit/cache": "3.0.0",
71
- "@peerbit/blocks-interface": "2.0.1",
72
- "@peerbit/blocks": "4.0.3",
70
+ "@peerbit/blocks": "4.0.4",
71
+ "@peerbit/blocks-interface": "2.0.2",
73
72
  "@peerbit/crypto": "3.0.0",
74
- "@peerbit/indexer-sqlite3": "3.0.0",
73
+ "@peerbit/cache": "3.0.0",
74
+ "@peerbit/pubsub": "5.0.4",
75
75
  "@peerbit/indexer-interface": "3.0.0",
76
- "@peerbit/pubsub-interface": "5.0.2",
77
- "@peerbit/stream-interface": "6.0.1",
76
+ "@peerbit/pubsub-interface": "5.0.3",
77
+ "@peerbit/stream-interface": "6.0.2",
78
78
  "@peerbit/time": "3.0.0",
79
- "@peerbit/pubsub": "5.0.3"
79
+ "@peerbit/indexer-sqlite3": "3.0.1"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@types/libsodium-wrappers": "^0.7.14",
83
83
  "@types/pidusage": "^2.0.5",
84
84
  "uuid": "^10.0.0",
85
- "@peerbit/test-utils": "3.0.7"
85
+ "@peerbit/test-utils": "3.0.9"
86
86
  },
87
87
  "scripts": {
88
88
  "clean": "aegir clean",
89
89
  "build": "aegir build --no-bundle",
90
+ "benchmark:adaptive-ingest": "node --loader ts-node/esm ./benchmark/adaptive-ingest.ts",
90
91
  "test": "aegir test --target node",
91
- "lint": "aegir lint"
92
+ "lint": "aegir lint",
93
+ "test:cov": "aegir test -t node --cov"
92
94
  }
93
95
  }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type AppendOptions,
24
24
  type Change,
25
25
  Entry,
26
+ EntryType,
26
27
  Log,
27
28
  type LogEvents,
28
29
  type LogProperties,
@@ -48,6 +49,7 @@ import {
48
49
  } from "@peerbit/pubsub-interface";
49
50
  import { RPC, type RequestContext } from "@peerbit/rpc";
50
51
  import {
52
+ ACK_CONTROL_PRIORITY,
51
53
  AcknowledgeDelivery,
52
54
  AnyWhere,
53
55
  createRequestTransportContext,
@@ -399,6 +401,7 @@ export type SharedLogOptions<
399
401
  ? I
400
402
  : "u32",
401
403
  > = {
404
+ appendDurability?: LogProperties<T>["appendDurability"];
402
405
  replicate?: ReplicationOptions<R>;
403
406
  replicas?: ReplicationLimitsOptions;
404
407
  respondToIHaveTimeout?: number;
@@ -441,6 +444,8 @@ const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE = 0.01;
441
444
  const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_CPU_LIMIT = 0.005;
442
445
  const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_MEMORY_LIMIT = 0.001;
443
446
  const RECALCULATE_PARTICIPATION_RELATIVE_DENOMINATOR_FLOOR = 1e-3;
447
+ const ADAPTIVE_REBALANCE_IDLE_INTERVAL_MULTIPLIER = 5;
448
+ const ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS = 10_000;
444
449
 
445
450
  const DEFAULT_DISTRIBUTION_DEBOUNCE_TIME = 500;
446
451
  const RECENT_REPAIR_DISPATCH_TTL_MS = 5_000;
@@ -705,6 +710,8 @@ export class SharedLog<
705
710
  replicas!: ReplicationLimits;
706
711
 
707
712
  private cpuUsage?: CPUUsage;
713
+ private _lastLocalAppendAt!: number;
714
+ private adaptiveRebalanceIdleMs!: number;
708
715
 
709
716
  timeUntilRoleMaturity!: number;
710
717
  waitForReplicatorTimeout!: number;
@@ -1457,6 +1464,71 @@ export class SharedLog<
1457
1464
  );
1458
1465
  }
1459
1466
 
1467
+ private markLocalAppendActivity(timestamp = Date.now()) {
1468
+ this._lastLocalAppendAt = Math.max(this._lastLocalAppendAt ?? 0, timestamp);
1469
+ }
1470
+
1471
+ private shouldDelayAdaptiveRebalance(now = Date.now()) {
1472
+ return (
1473
+ this._isAdaptiveReplicating &&
1474
+ this._lastLocalAppendAt > 0 &&
1475
+ now - this._lastLocalAppendAt < this.adaptiveRebalanceIdleMs
1476
+ );
1477
+ }
1478
+
1479
+ private shouldDeferHeadCoordinatePersistence(
1480
+ options?: SharedAppendOptions<T>,
1481
+ ) {
1482
+ return (
1483
+ !this._isReplicating &&
1484
+ options?.replicate === false &&
1485
+ options?.target === "none"
1486
+ );
1487
+ }
1488
+
1489
+ private async deleteCoordinatesForHashes(hashes: Iterable<string>) {
1490
+ const values = [...new Set([...hashes].filter(Boolean))];
1491
+ if (values.length === 0) {
1492
+ return;
1493
+ }
1494
+ await this.entryCoordinatesIndex.del({
1495
+ query:
1496
+ values.length === 1
1497
+ ? { hash: values[0] }
1498
+ : new Or(
1499
+ values.map((hash) => new StringMatch({ key: "hash", value: hash })),
1500
+ ),
1501
+ });
1502
+ }
1503
+
1504
+ private async ensureCurrentHeadCoordinatesIndexed() {
1505
+ const heads = await this.log.getHeads(true).all();
1506
+ const headsByHash = new Map(heads.map((head) => [head.hash, head]));
1507
+ const indexedHeads = await this.entryCoordinatesIndex
1508
+ .iterate({}, { shape: { hash: true } })
1509
+ .all();
1510
+ const indexedHashes = new Set(indexedHeads.map((entry) => entry.value.hash));
1511
+ const staleHashes = indexedHeads
1512
+ .map((entry) => entry.value.hash)
1513
+ .filter((hash) => !headsByHash.has(hash));
1514
+
1515
+ if (staleHashes.length > 0) {
1516
+ await this.deleteCoordinatesForHashes(staleHashes);
1517
+ }
1518
+
1519
+ for (const head of heads) {
1520
+ if (indexedHashes.has(head.hash)) {
1521
+ continue;
1522
+ }
1523
+ const minReplicas = decodeReplicas(head).getValue(this);
1524
+ await this.findLeaders(
1525
+ await this.createCoordinates(head, minReplicas),
1526
+ head,
1527
+ { persist: {} },
1528
+ );
1529
+ }
1530
+ }
1531
+
1460
1532
  private async _replicate(
1461
1533
  options?: ReplicationOptions<R>,
1462
1534
  {
@@ -2264,6 +2336,8 @@ export class SharedLog<
2264
2336
  ) => void;
2265
2337
  } = {},
2266
2338
  ) {
2339
+ await this.ensureCurrentHeadCoordinatesIndexed();
2340
+
2267
2341
  const change = await this.addReplicationRange(
2268
2342
  range,
2269
2343
  this.node.identity.publicKey,
@@ -2659,6 +2733,10 @@ export class SharedLog<
2659
2733
  entry: Entry<T>;
2660
2734
  removed: ShallowOrFullEntry<T>[];
2661
2735
  }> {
2736
+ if (this._isAdaptiveReplicating) {
2737
+ this.markLocalAppendActivity();
2738
+ }
2739
+
2662
2740
  const appendOptions: AppendOptions<T> = { ...options };
2663
2741
  const minReplicas = this.getClampedReplicas(
2664
2742
  options?.replicas
@@ -2693,13 +2771,23 @@ export class SharedLog<
2693
2771
  return options.onChange!(change);
2694
2772
  };
2695
2773
  }
2696
-
2697
2774
  const result = await this.log.append(data, appendOptions);
2775
+ const deferHeadCoordinatePersistence =
2776
+ result.entry.meta.type !== EntryType.CUT &&
2777
+ this.shouldDeferHeadCoordinatePersistence(options);
2698
2778
 
2699
2779
  if (options?.replicate) {
2700
2780
  await this.replicate(result.entry, { checkDuplicates: true });
2701
2781
  }
2702
2782
 
2783
+ if (deferHeadCoordinatePersistence) {
2784
+ await this.deleteCoordinatesForHashes([
2785
+ ...result.entry.meta.next,
2786
+ ...result.removed.map((entry) => entry.hash),
2787
+ ]);
2788
+ return result;
2789
+ }
2790
+
2703
2791
  const coordinates = await this.createCoordinates(
2704
2792
  result.entry,
2705
2793
  minReplicasValue,
@@ -2744,13 +2832,15 @@ export class SharedLog<
2744
2832
  }
2745
2833
  }
2746
2834
 
2747
- if (!isLeader) {
2835
+ if (!isLeader && !this.shouldDelayAdaptiveRebalance()) {
2748
2836
  this.pruneDebouncedFnAddIfNotKeeping({
2749
2837
  key: result.entry.hash,
2750
2838
  value: { entry: result.entry, leaders },
2751
2839
  });
2752
2840
  }
2753
- this.rebalanceParticipationDebounced?.call();
2841
+ if (!this._isAdaptiveReplicating) {
2842
+ this.rebalanceParticipationDebounced?.call();
2843
+ }
2754
2844
 
2755
2845
  return result;
2756
2846
  }
@@ -2805,6 +2895,17 @@ export class SharedLog<
2805
2895
  this._replicatorLivenessCursor = 0;
2806
2896
  this._replicatorLivenessFailures = new Map();
2807
2897
  this._replicatorLastActivityAt = new Map();
2898
+ this._lastLocalAppendAt = 0;
2899
+ const adaptiveReplicateOptions =
2900
+ options?.replicate && isAdaptiveReplicatorOption(options.replicate)
2901
+ ? options.replicate
2902
+ : undefined;
2903
+ this.adaptiveRebalanceIdleMs = Math.max(
2904
+ ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS,
2905
+ (adaptiveReplicateOptions?.limits?.interval ??
2906
+ RECALCULATE_PARTICIPATION_DEBOUNCE_INTERVAL) *
2907
+ ADAPTIVE_REBALANCE_IDLE_INTERVAL_MULTIPLIER,
2908
+ );
2808
2909
 
2809
2910
  this.openTime = +new Date();
2810
2911
  this.oldestOpenTime = this.openTime;
@@ -2933,10 +3034,10 @@ export class SharedLog<
2933
3034
  ],
2934
3035
  })) > 0;
2935
3036
 
2936
- this._gidPeersHistory = new Map();
2937
- this._requestIPruneSent = new Map();
2938
- this._requestIPruneResponseReplicatorSet = new Map();
2939
- this._checkedPruneRetries = new Map();
3037
+ this._gidPeersHistory = new Map();
3038
+ this._requestIPruneSent = new Map();
3039
+ this._requestIPruneResponseReplicatorSet = new Map();
3040
+ this._checkedPruneRetries = new Map();
2940
3041
 
2941
3042
  this.replicationChangeDebounceFn = debounceAggregationChanges<
2942
3043
  ReplicationRangeIndexable<R>
@@ -3549,7 +3650,8 @@ export class SharedLog<
3549
3650
  // triggering large segment snapshots just to prove liveness.
3550
3651
  await this.rpc.send(new ReplicationPingMessage(), {
3551
3652
  mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] }),
3552
- priority: 1,
3653
+ priority: ACK_CONTROL_PRIORITY,
3654
+ responsePriority: ACK_CONTROL_PRIORITY,
3553
3655
  });
3554
3656
  this.markReplicatorActivity(peerHash);
3555
3657
  this._replicatorLivenessFailures.delete(peerHash);
@@ -3560,6 +3662,15 @@ export class SharedLog<
3560
3662
  }
3561
3663
  }
3562
3664
 
3665
+ // Relay-backed prod paths can keep a peer subscribed/reachable even if an
3666
+ // ACKed liveness ping gets delayed or dropped under load. Treat observed
3667
+ // topic presence as a positive liveness signal before evicting the peer.
3668
+ if (await this.confirmReplicatorSubscriberPresence(peerHash)) {
3669
+ this.markReplicatorActivity(peerHash);
3670
+ this._replicatorLivenessFailures.delete(peerHash);
3671
+ return;
3672
+ }
3673
+
3563
3674
  const failures = (this._replicatorLivenessFailures.get(peerHash) ?? 0) + 1;
3564
3675
  this._replicatorLivenessFailures.set(peerHash, failures);
3565
3676
  this.scheduleReplicationInfoRequests(publicKey);
@@ -3575,6 +3686,24 @@ export class SharedLog<
3575
3686
  await this.evictReplicatorFromLiveness(peerHash, publicKey);
3576
3687
  }
3577
3688
 
3689
+ private async confirmReplicatorSubscriberPresence(peerHash: string) {
3690
+ try {
3691
+ await waitForSubscribers(this.node, peerHash, this.rpc.topic, {
3692
+ signal: this._closeController.signal,
3693
+ timeout: Math.max(
3694
+ 1_000,
3695
+ Math.min(5_000, Math.floor(this.waitForReplicatorTimeout / 4)),
3696
+ ),
3697
+ });
3698
+ return true;
3699
+ } catch (error) {
3700
+ if (isNotStartedError(error as Error)) {
3701
+ return false;
3702
+ }
3703
+ return false;
3704
+ }
3705
+ }
3706
+
3578
3707
  async getMemoryUsage() {
3579
3708
  return this.log.blocks.size();
3580
3709
  /* ((await this.log.entryIndex?.getMemoryUsage()) || 0) */ // + (await this.log.blocks.size())
@@ -3801,23 +3930,23 @@ export class SharedLog<
3801
3930
  }
3802
3931
 
3803
3932
  await this.remoteBlocks.stop();
3804
- this._pendingDeletes.clear();
3805
- this._pendingIHave.clear();
3806
- this._checkedPruneRetries.clear();
3807
- this.latestReplicationInfoMessage.clear();
3808
- this._gidPeersHistory.clear();
3809
- this._requestIPruneSent.clear();
3810
- this._requestIPruneResponseReplicatorSet.clear();
3811
- // Cancel any pending debounced timers so they can't fire after we've torn down
3812
- // indexes/RPC state.
3813
- this.rebalanceParticipationDebounced?.close();
3814
- this.replicationChangeDebounceFn?.close?.();
3815
- this.pruneDebouncedFn?.close?.();
3816
- this.responseToPruneDebouncedFn?.close?.();
3817
- this.pruneDebouncedFn = undefined as any;
3818
- this.rebalanceParticipationDebounced = undefined;
3819
- this._replicationRangeIndex.stop();
3820
- this._entryCoordinatesIndex.stop();
3933
+ this._pendingDeletes.clear();
3934
+ this._pendingIHave.clear();
3935
+ this._checkedPruneRetries.clear();
3936
+ this.latestReplicationInfoMessage.clear();
3937
+ this._gidPeersHistory.clear();
3938
+ this._requestIPruneSent.clear();
3939
+ this._requestIPruneResponseReplicatorSet.clear();
3940
+ // Cancel any pending debounced timers so they can't fire after we've torn down
3941
+ // indexes/RPC state.
3942
+ this.rebalanceParticipationDebounced?.close();
3943
+ this.replicationChangeDebounceFn?.close?.();
3944
+ this.pruneDebouncedFn?.close?.();
3945
+ this.responseToPruneDebouncedFn?.close?.();
3946
+ this.pruneDebouncedFn = undefined as any;
3947
+ this.rebalanceParticipationDebounced = undefined;
3948
+ this._replicationRangeIndex.stop();
3949
+ this._entryCoordinatesIndex.stop();
3821
3950
  this._replicationRangeIndex = undefined as any;
3822
3951
  this._entryCoordinatesIndex = undefined as any;
3823
3952
 
@@ -4602,6 +4731,14 @@ export class SharedLog<
4602
4731
  },
4603
4732
  ): Promise<void> {
4604
4733
  let entriesToReplicate: Entry<T>[] = [];
4734
+ const localHashes =
4735
+ options?.replicate && this.log.length > 0
4736
+ ? await this.log.entryIndex.hasMany(
4737
+ entries.map((element) =>
4738
+ typeof element === "string" ? element : element.hash,
4739
+ ),
4740
+ )
4741
+ : new Set<string>();
4605
4742
  if (options?.replicate && this.log.length > 0) {
4606
4743
  // TODO this block should perhaps be called from a callback on the this.log.join method on all the ignored element because already joined, like "onAlreadyJoined"
4607
4744
 
@@ -4609,18 +4746,18 @@ export class SharedLog<
4609
4746
  // we can not just do the 'join' call because it will ignore the already joined entries
4610
4747
  for (const element of entries) {
4611
4748
  if (typeof element === "string") {
4612
- if (await this.log.has(element)) {
4749
+ if (localHashes.has(element)) {
4613
4750
  const entry = await this.log.get(element);
4614
4751
  if (entry) {
4615
4752
  entriesToReplicate.push(entry);
4616
4753
  }
4617
4754
  }
4618
4755
  } else if (element instanceof Entry) {
4619
- if (await this.log.has(element.hash)) {
4756
+ if (localHashes.has(element.hash)) {
4620
4757
  entriesToReplicate.push(element);
4621
4758
  }
4622
4759
  } else {
4623
- if (await this.log.has(element.hash)) {
4760
+ if (localHashes.has(element.hash)) {
4624
4761
  const entry = await this.log.get(element.hash);
4625
4762
  if (entry) {
4626
4763
  entriesToReplicate.push(entry);
@@ -6168,6 +6305,17 @@ export class SharedLog<
6168
6305
  // update more participation rate to converge to the average expected rate or bounded by
6169
6306
  // resources such as memory and or cpu
6170
6307
 
6308
+ const isClosedStoreRace = (error: any) => {
6309
+ const message =
6310
+ typeof error?.message === "string" ? error.message : String(error);
6311
+ return (
6312
+ this.closed ||
6313
+ message.includes("Iterator is not open") ||
6314
+ message.includes("cannot read after close()") ||
6315
+ message.includes("Database is not open")
6316
+ );
6317
+ };
6318
+
6171
6319
  const fn = async () => {
6172
6320
  if (this.closed) {
6173
6321
  return false;
@@ -6179,6 +6327,11 @@ export class SharedLog<
6179
6327
  }
6180
6328
 
6181
6329
  if (this._isAdaptiveReplicating) {
6330
+ if (this.shouldDelayAdaptiveRebalance()) {
6331
+ this.rebalanceParticipationDebounced?.call();
6332
+ return false;
6333
+ }
6334
+
6182
6335
  const peers = this.replicationIndex;
6183
6336
  const usedMemory = await this.getMemoryUsage();
6184
6337
  let dynamicRange = await this.getDynamicRange();
@@ -6252,7 +6405,12 @@ export class SharedLog<
6252
6405
  return false;
6253
6406
  };
6254
6407
 
6255
- const resp = await fn();
6408
+ const resp = await fn().catch((error: any) => {
6409
+ if (isNotStartedError(error) || isClosedStoreRace(error)) {
6410
+ return false;
6411
+ }
6412
+ throw error;
6413
+ });
6256
6414
 
6257
6415
  return resp;
6258
6416
  }