@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/benchmark/adaptive-ingest.d.ts +2 -0
- package/dist/benchmark/adaptive-ingest.d.ts.map +1 -0
- package/dist/benchmark/adaptive-ingest.js +275 -0
- package/dist/benchmark/adaptive-ingest.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +123 -9
- package/dist/src/index.js.map +1 -1
- package/package.json +16 -14
- package/src/index.ts +187 -29
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.
|
|
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:
|
|
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 (
|
|
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 (
|
|
3531
|
+
if (localHashes.has(element.hash)) {
|
|
3434
3532
|
entriesToReplicate.push(element);
|
|
3435
3533
|
}
|
|
3436
3534
|
}
|
|
3437
3535
|
else {
|
|
3438
|
-
if (
|
|
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() {
|