@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/dist/src/index.js CHANGED
@@ -38,13 +38,13 @@ import { cidifyString } from "@peerbit/blocks-interface";
38
38
  import { Cache } from "@peerbit/cache";
39
39
  import { AccessError, PublicSignKey, getPublicKeyFromPeerId, sha256Base64Sync, sha256Sync, } from "@peerbit/crypto";
40
40
  import { And, ByteMatchQuery, NotStartedError as IndexNotStartedError, Or, Sort, StringMatch, toId, } from "@peerbit/indexer-interface";
41
- import { Entry, Log, Meta, ShallowEntry, } from "@peerbit/log";
41
+ import { Entry, EntryType, Log, Meta, ShallowEntry, } from "@peerbit/log";
42
42
  import { logger as loggerFn } from "@peerbit/logger";
43
43
  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 { AcknowledgeDelivery, AnyWhere, createRequestTransportContext, DataMessage, MessageHeader, NotStartedError, SilentDelivery, } from "@peerbit/stream-interface";
47
+ import { ACK_CONTROL_PRIORITY, AcknowledgeDelivery, AnyWhere, 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";
@@ -215,6 +215,8 @@ const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE = 0.01;
215
215
  const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_CPU_LIMIT = 0.005;
216
216
  const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_MEMORY_LIMIT = 0.001;
217
217
  const RECALCULATE_PARTICIPATION_RELATIVE_DENOMINATOR_FLOOR = 1e-3;
218
+ const ADAPTIVE_REBALANCE_IDLE_INTERVAL_MULTIPLIER = 5;
219
+ const ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS = 10_000;
218
220
  const DEFAULT_DISTRIBUTION_DEBOUNCE_TIME = 500;
219
221
  const RECENT_REPAIR_DISPATCH_TTL_MS = 5_000;
220
222
  const REPAIR_SWEEP_ENTRY_BATCH_SIZE = 1_000;
@@ -348,6 +350,8 @@ let SharedLog = (() => {
348
350
  syncronizer;
349
351
  replicas;
350
352
  cpuUsage;
353
+ _lastLocalAppendAt;
354
+ adaptiveRebalanceIdleMs;
351
355
  timeUntilRoleMaturity;
352
356
  waitForReplicatorTimeout;
353
357
  waitForReplicatorRequestIntervalMs;
@@ -966,6 +970,51 @@ let SharedLog = (() => {
966
970
  ) */
967
971
  interval);
968
972
  }
973
+ markLocalAppendActivity(timestamp = Date.now()) {
974
+ this._lastLocalAppendAt = Math.max(this._lastLocalAppendAt ?? 0, timestamp);
975
+ }
976
+ shouldDelayAdaptiveRebalance(now = Date.now()) {
977
+ return (this._isAdaptiveReplicating &&
978
+ this._lastLocalAppendAt > 0 &&
979
+ now - this._lastLocalAppendAt < this.adaptiveRebalanceIdleMs);
980
+ }
981
+ shouldDeferHeadCoordinatePersistence(options) {
982
+ return (!this._isReplicating &&
983
+ options?.replicate === false &&
984
+ options?.target === "none");
985
+ }
986
+ async deleteCoordinatesForHashes(hashes) {
987
+ const values = [...new Set([...hashes].filter(Boolean))];
988
+ if (values.length === 0) {
989
+ return;
990
+ }
991
+ await this.entryCoordinatesIndex.del({
992
+ query: values.length === 1
993
+ ? { hash: values[0] }
994
+ : new Or(values.map((hash) => new StringMatch({ key: "hash", value: hash }))),
995
+ });
996
+ }
997
+ async ensureCurrentHeadCoordinatesIndexed() {
998
+ const heads = await this.log.getHeads(true).all();
999
+ const headsByHash = new Map(heads.map((head) => [head.hash, head]));
1000
+ const indexedHeads = await this.entryCoordinatesIndex
1001
+ .iterate({}, { shape: { hash: true } })
1002
+ .all();
1003
+ const indexedHashes = new Set(indexedHeads.map((entry) => entry.value.hash));
1004
+ const staleHashes = indexedHeads
1005
+ .map((entry) => entry.value.hash)
1006
+ .filter((hash) => !headsByHash.has(hash));
1007
+ if (staleHashes.length > 0) {
1008
+ await this.deleteCoordinatesForHashes(staleHashes);
1009
+ }
1010
+ for (const head of heads) {
1011
+ if (indexedHashes.has(head.hash)) {
1012
+ continue;
1013
+ }
1014
+ const minReplicas = decodeReplicas(head).getValue(this);
1015
+ await this.findLeaders(await this.createCoordinates(head, minReplicas), head, { persist: {} });
1016
+ }
1017
+ }
969
1018
  async _replicate(options, { reset, checkDuplicates, announce, mergeSegments, rebalance, } = {}) {
970
1019
  let offsetWasProvided = false;
971
1020
  if (isUnreplicationOptions(options)) {
@@ -1564,6 +1613,7 @@ let SharedLog = (() => {
1564
1613
  return diffs;
1565
1614
  }
1566
1615
  async startAnnounceReplicating(range, options = {}) {
1616
+ await this.ensureCurrentHeadCoordinatesIndexed();
1567
1617
  const change = await this.addReplicationRange(range, this.node.identity.publicKey, options);
1568
1618
  if (!change) {
1569
1619
  warn("Not allowed to replicate by canReplicate");
@@ -1889,6 +1939,9 @@ let SharedLog = (() => {
1889
1939
  this._checkedPruneRetries.set(hash, state);
1890
1940
  }
1891
1941
  async append(data, options) {
1942
+ if (this._isAdaptiveReplicating) {
1943
+ this.markLocalAppendActivity();
1944
+ }
1892
1945
  const appendOptions = { ...options };
1893
1946
  const minReplicas = this.getClampedReplicas(options?.replicas
1894
1947
  ? typeof options.replicas === "number"
@@ -1921,9 +1974,18 @@ let SharedLog = (() => {
1921
1974
  };
1922
1975
  }
1923
1976
  const result = await this.log.append(data, appendOptions);
1977
+ const deferHeadCoordinatePersistence = result.entry.meta.type !== EntryType.CUT &&
1978
+ this.shouldDeferHeadCoordinatePersistence(options);
1924
1979
  if (options?.replicate) {
1925
1980
  await this.replicate(result.entry, { checkDuplicates: true });
1926
1981
  }
1982
+ if (deferHeadCoordinatePersistence) {
1983
+ await this.deleteCoordinatesForHashes([
1984
+ ...result.entry.meta.next,
1985
+ ...result.removed.map((entry) => entry.hash),
1986
+ ]);
1987
+ return result;
1988
+ }
1927
1989
  const coordinates = await this.createCoordinates(result.entry, minReplicasValue);
1928
1990
  const selfHash = this.node.identity.publicKey.hashcode();
1929
1991
  let isLeader = false;
@@ -1950,13 +2012,15 @@ let SharedLog = (() => {
1950
2012
  await this._appendDeliverToReplicators(result.entry, minReplicasValue, leaders, selfHash, isLeader, deliveryArg);
1951
2013
  }
1952
2014
  }
1953
- if (!isLeader) {
2015
+ if (!isLeader && !this.shouldDelayAdaptiveRebalance()) {
1954
2016
  this.pruneDebouncedFnAddIfNotKeeping({
1955
2017
  key: result.entry.hash,
1956
2018
  value: { entry: result.entry, leaders },
1957
2019
  });
1958
2020
  }
1959
- this.rebalanceParticipationDebounced?.call();
2021
+ if (!this._isAdaptiveReplicating) {
2022
+ this.rebalanceParticipationDebounced?.call();
2023
+ }
1960
2024
  return result;
1961
2025
  }
1962
2026
  async open(options) {
@@ -2002,6 +2066,13 @@ let SharedLog = (() => {
2002
2066
  this._replicatorLivenessCursor = 0;
2003
2067
  this._replicatorLivenessFailures = new Map();
2004
2068
  this._replicatorLastActivityAt = new Map();
2069
+ this._lastLocalAppendAt = 0;
2070
+ const adaptiveReplicateOptions = options?.replicate && isAdaptiveReplicatorOption(options.replicate)
2071
+ ? options.replicate
2072
+ : undefined;
2073
+ this.adaptiveRebalanceIdleMs = Math.max(ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS, (adaptiveReplicateOptions?.limits?.interval ??
2074
+ RECALCULATE_PARTICIPATION_DEBOUNCE_INTERVAL) *
2075
+ ADAPTIVE_REBALANCE_IDLE_INTERVAL_MULTIPLIER);
2005
2076
  this.openTime = +new Date();
2006
2077
  this.oldestOpenTime = this.openTime;
2007
2078
  this.distributionDebounceTime =
@@ -2595,7 +2666,8 @@ let SharedLog = (() => {
2595
2666
  // triggering large segment snapshots just to prove liveness.
2596
2667
  await this.rpc.send(new ReplicationPingMessage(), {
2597
2668
  mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] }),
2598
- priority: 1,
2669
+ priority: ACK_CONTROL_PRIORITY,
2670
+ responsePriority: ACK_CONTROL_PRIORITY,
2599
2671
  });
2600
2672
  this.markReplicatorActivity(peerHash);
2601
2673
  this._replicatorLivenessFailures.delete(peerHash);
@@ -2606,6 +2678,14 @@ let SharedLog = (() => {
2606
2678
  return;
2607
2679
  }
2608
2680
  }
2681
+ // Relay-backed prod paths can keep a peer subscribed/reachable even if an
2682
+ // ACKed liveness ping gets delayed or dropped under load. Treat observed
2683
+ // topic presence as a positive liveness signal before evicting the peer.
2684
+ if (await this.confirmReplicatorSubscriberPresence(peerHash)) {
2685
+ this.markReplicatorActivity(peerHash);
2686
+ this._replicatorLivenessFailures.delete(peerHash);
2687
+ return;
2688
+ }
2609
2689
  const failures = (this._replicatorLivenessFailures.get(peerHash) ?? 0) + 1;
2610
2690
  this._replicatorLivenessFailures.set(peerHash, failures);
2611
2691
  this.scheduleReplicationInfoRequests(publicKey);
@@ -2618,6 +2698,21 @@ let SharedLog = (() => {
2618
2698
  }
2619
2699
  await this.evictReplicatorFromLiveness(peerHash, publicKey);
2620
2700
  }
2701
+ async confirmReplicatorSubscriberPresence(peerHash) {
2702
+ try {
2703
+ await waitForSubscribers(this.node, peerHash, this.rpc.topic, {
2704
+ signal: this._closeController.signal,
2705
+ timeout: Math.max(1_000, Math.min(5_000, Math.floor(this.waitForReplicatorTimeout / 4))),
2706
+ });
2707
+ return true;
2708
+ }
2709
+ catch (error) {
2710
+ if (isNotStartedError(error)) {
2711
+ return false;
2712
+ }
2713
+ return false;
2714
+ }
2715
+ }
2621
2716
  async getMemoryUsage() {
2622
2717
  return this.log.blocks.size();
2623
2718
  /* ((await this.log.entryIndex?.getMemoryUsage()) || 0) */ // + (await this.log.blocks.size())
@@ -3416,13 +3511,16 @@ let SharedLog = (() => {
3416
3511
  }
3417
3512
  async join(entries, options) {
3418
3513
  let entriesToReplicate = [];
3514
+ const localHashes = options?.replicate && this.log.length > 0
3515
+ ? await this.log.entryIndex.hasMany(entries.map((element) => typeof element === "string" ? element : element.hash))
3516
+ : new Set();
3419
3517
  if (options?.replicate && this.log.length > 0) {
3420
3518
  // 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"
3421
3519
  // check which entrise we already have but not are replicating, and replicate them
3422
3520
  // we can not just do the 'join' call because it will ignore the already joined entries
3423
3521
  for (const element of entries) {
3424
3522
  if (typeof element === "string") {
3425
- if (await this.log.has(element)) {
3523
+ if (localHashes.has(element)) {
3426
3524
  const entry = await this.log.get(element);
3427
3525
  if (entry) {
3428
3526
  entriesToReplicate.push(entry);
@@ -3430,12 +3528,12 @@ let SharedLog = (() => {
3430
3528
  }
3431
3529
  }
3432
3530
  else if (element instanceof Entry) {
3433
- if (await this.log.has(element.hash)) {
3531
+ if (localHashes.has(element.hash)) {
3434
3532
  entriesToReplicate.push(element);
3435
3533
  }
3436
3534
  }
3437
3535
  else {
3438
- if (await this.log.has(element.hash)) {
3536
+ if (localHashes.has(element.hash)) {
3439
3537
  const entry = await this.log.get(element.hash);
3440
3538
  if (entry) {
3441
3539
  entriesToReplicate.push(entry);
@@ -4569,6 +4667,13 @@ let SharedLog = (() => {
4569
4667
  async rebalanceParticipation() {
4570
4668
  // update more participation rate to converge to the average expected rate or bounded by
4571
4669
  // resources such as memory and or cpu
4670
+ const isClosedStoreRace = (error) => {
4671
+ const message = typeof error?.message === "string" ? error.message : String(error);
4672
+ return (this.closed ||
4673
+ message.includes("Iterator is not open") ||
4674
+ message.includes("cannot read after close()") ||
4675
+ message.includes("Database is not open"));
4676
+ };
4572
4677
  const fn = async () => {
4573
4678
  if (this.closed) {
4574
4679
  return false;
@@ -4578,6 +4683,10 @@ let SharedLog = (() => {
4578
4683
  return false;
4579
4684
  }
4580
4685
  if (this._isAdaptiveReplicating) {
4686
+ if (this.shouldDelayAdaptiveRebalance()) {
4687
+ this.rebalanceParticipationDebounced?.call();
4688
+ return false;
4689
+ }
4581
4690
  const peers = this.replicationIndex;
4582
4691
  const usedMemory = await this.getMemoryUsage();
4583
4692
  let dynamicRange = await this.getDynamicRange();
@@ -4635,7 +4744,12 @@ let SharedLog = (() => {
4635
4744
  }
4636
4745
  return false;
4637
4746
  };
4638
- const resp = await fn();
4747
+ const resp = await fn().catch((error) => {
4748
+ if (isNotStartedError(error) || isClosedStoreRace(error)) {
4749
+ return false;
4750
+ }
4751
+ throw error;
4752
+ });
4639
4753
  return resp;
4640
4754
  }
4641
4755
  getDynamicRangeOffset() {