@peerbit/shared-log 16.0.7 → 16.0.9
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 +25 -1
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +246 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/like.d.ts +3 -0
- package/dist/src/like.d.ts.map +1 -1
- package/dist/src/pid.js +4 -4
- package/dist/src/pid.js.map +1 -1
- package/dist/src/ranges.js +1 -1
- package/dist/src/ranges.js.map +1 -1
- package/dist/src/replication-status.d.ts +35 -0
- package/dist/src/replication-status.d.ts.map +1 -0
- package/dist/src/replication-status.js +37 -0
- package/dist/src/replication-status.js.map +1 -0
- package/dist/src/sync/factory.d.ts.map +1 -1
- package/dist/src/sync/factory.js +8 -2
- package/dist/src/sync/factory.js.map +1 -1
- package/dist/src/sync/index.d.ts +12 -0
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.d.ts +1 -0
- package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.js +67 -14
- package/dist/src/sync/rateless-iblt.js.map +1 -1
- package/dist/src/sync/simple.d.ts.map +1 -1
- package/dist/src/sync/simple.js +36 -10
- package/dist/src/sync/simple.js.map +1 -1
- package/package.json +18 -17
- package/src/index.ts +322 -6
- package/src/like.ts +3 -0
- package/src/pid.ts +4 -4
- package/src/ranges.ts +1 -1
- package/src/replication-status.ts +77 -0
- package/src/sync/factory.ts +11 -2
- package/src/sync/index.ts +14 -4
- package/src/sync/rateless-iblt.ts +81 -24
- package/src/sync/simple.ts +45 -14
package/src/index.ts
CHANGED
|
@@ -252,6 +252,10 @@ import type {
|
|
|
252
252
|
} from "./replication-info-mutation.js";
|
|
253
253
|
import { ReplicationInfoV2ReceiveCoordinator } from "./replication-info-v2-receive.js";
|
|
254
254
|
import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
|
|
255
|
+
import {
|
|
256
|
+
type ReplicationStatus,
|
|
257
|
+
classifyReplicationStatus,
|
|
258
|
+
} from "./replication-status.js";
|
|
255
259
|
import {
|
|
256
260
|
AbsoluteReplicas,
|
|
257
261
|
AddedReplicationInfoV2Message,
|
|
@@ -416,6 +420,7 @@ export {
|
|
|
416
420
|
};
|
|
417
421
|
export { type CPUUsage, CPUUsageIntervalLag };
|
|
418
422
|
export * from "./replication.js";
|
|
423
|
+
export * from "./replication-status.js";
|
|
419
424
|
export type {
|
|
420
425
|
LogLike,
|
|
421
426
|
LogResultsIterator,
|
|
@@ -1022,6 +1027,11 @@ export type ReplicationLimitsOptions =
|
|
|
1022
1027
|
export type DynamicReplicationOptions<R extends "u32" | "u64"> = {
|
|
1023
1028
|
limits?: {
|
|
1024
1029
|
interval?: number;
|
|
1030
|
+
/**
|
|
1031
|
+
* Soft byte objective for the adaptive PID controller. This is not a hard
|
|
1032
|
+
* quota: coverage pressure, discrete entries, and delayed pruning can make
|
|
1033
|
+
* actual local storage exceed this value.
|
|
1034
|
+
*/
|
|
1025
1035
|
storage?: number;
|
|
1026
1036
|
cpu?: number | { max: number; monitor?: CPUUsage };
|
|
1027
1037
|
};
|
|
@@ -1913,6 +1923,14 @@ export type ReplicatorJoinEvent = { publicKey: PublicSignKey };
|
|
|
1913
1923
|
export type ReplicatorLeaveEvent = { publicKey: PublicSignKey };
|
|
1914
1924
|
export type ReplicationChangeEvent = { publicKey: PublicSignKey };
|
|
1915
1925
|
export type ReplicatorMatureEvent = { publicKey: PublicSignKey };
|
|
1926
|
+
export type ReplicationStatusEvent = ReplicationStatus;
|
|
1927
|
+
|
|
1928
|
+
class ReplicationStatusSnapshotChangedError extends Error {
|
|
1929
|
+
constructor() {
|
|
1930
|
+
super("Replication status changed while it was being measured");
|
|
1931
|
+
this.name = "ReplicationStatusSnapshotChangedError";
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1916
1934
|
|
|
1917
1935
|
type LeaderSelectionContext = {
|
|
1918
1936
|
roleAge: number;
|
|
@@ -1927,6 +1945,7 @@ export interface SharedLogEvents extends ProgramEvents {
|
|
|
1927
1945
|
"replicator:leave": CustomEvent<ReplicatorLeaveEvent>;
|
|
1928
1946
|
"replication:change": CustomEvent<ReplicationChangeEvent>;
|
|
1929
1947
|
"replicator:mature": CustomEvent<ReplicatorMatureEvent>;
|
|
1948
|
+
"replication:status": CustomEvent<ReplicationStatusEvent>;
|
|
1930
1949
|
}
|
|
1931
1950
|
|
|
1932
1951
|
export type SharedLogRuntimeSnapshot = Readonly<{
|
|
@@ -1953,6 +1972,10 @@ export class SharedLog<
|
|
|
1953
1972
|
// options
|
|
1954
1973
|
private _isReplicating!: boolean;
|
|
1955
1974
|
private _isAdaptiveReplicating!: boolean;
|
|
1975
|
+
private _replicationStatus?: ReplicationStatus;
|
|
1976
|
+
private _replicationStatusReadTail!: Promise<void>;
|
|
1977
|
+
private _replicationStatusRefreshScheduled!: boolean;
|
|
1978
|
+
private _replicationStatusRefreshDirty!: boolean;
|
|
1956
1979
|
|
|
1957
1980
|
private _replicationRangeIndex!: Index<ReplicationRangeIndexable<R>>;
|
|
1958
1981
|
private _entryCoordinatesIndex!: Index<EntryReplicated<R>>;
|
|
@@ -6049,6 +6072,37 @@ export class SharedLog<
|
|
|
6049
6072
|
msg: FullReplicationInfoMutation | AddedReplicationInfoMutation,
|
|
6050
6073
|
) => void;
|
|
6051
6074
|
},
|
|
6075
|
+
) {
|
|
6076
|
+
const wasAdaptiveReplicating = this._isAdaptiveReplicating;
|
|
6077
|
+
const previousStorageObjective = wasAdaptiveReplicating
|
|
6078
|
+
? this.replicationController?.maxMemoryLimit
|
|
6079
|
+
: undefined;
|
|
6080
|
+
try {
|
|
6081
|
+
return await this.applyReplicationRole(rangeOrEntry, options);
|
|
6082
|
+
} finally {
|
|
6083
|
+
const storageObjective = this._isAdaptiveReplicating
|
|
6084
|
+
? this.replicationController?.maxMemoryLimit
|
|
6085
|
+
: undefined;
|
|
6086
|
+
if (
|
|
6087
|
+
this._isAdaptiveReplicating !== wasAdaptiveReplicating ||
|
|
6088
|
+
storageObjective !== previousStorageObjective
|
|
6089
|
+
) {
|
|
6090
|
+
this.scheduleReplicationStatusRefresh();
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
private async applyReplicationRole(
|
|
6096
|
+
rangeOrEntry?: ReplicationOptions<R> | Entry<T> | Entry<T>[],
|
|
6097
|
+
options?: {
|
|
6098
|
+
reset?: boolean;
|
|
6099
|
+
checkDuplicates?: boolean;
|
|
6100
|
+
rebalance?: boolean;
|
|
6101
|
+
mergeSegments?: boolean;
|
|
6102
|
+
announce?: (
|
|
6103
|
+
msg: FullReplicationInfoMutation | AddedReplicationInfoMutation,
|
|
6104
|
+
) => void;
|
|
6105
|
+
},
|
|
6052
6106
|
) {
|
|
6053
6107
|
this.throwIfCheckedPruneRemoveBlocksLocalOperation(
|
|
6054
6108
|
"replication range mutation",
|
|
@@ -6157,15 +6211,31 @@ export class SharedLog<
|
|
|
6157
6211
|
}
|
|
6158
6212
|
|
|
6159
6213
|
async unreplicate(rangeOrEntry?: Entry<T> | { id: Uint8Array }[]) {
|
|
6214
|
+
const wasAdaptiveReplicating = this._isAdaptiveReplicating;
|
|
6215
|
+
const previousStorageObjective = wasAdaptiveReplicating
|
|
6216
|
+
? this.replicationController?.maxMemoryLimit
|
|
6217
|
+
: undefined;
|
|
6160
6218
|
this.throwIfCheckedPruneRemoveBlocksLocalOperation(
|
|
6161
6219
|
"replication range mutation",
|
|
6162
6220
|
);
|
|
6163
6221
|
const replicationOwnershipLifecycleController =
|
|
6164
6222
|
this.captureReplicationOwnershipLifecycle();
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6223
|
+
try {
|
|
6224
|
+
return await this._unreplicate(
|
|
6225
|
+
rangeOrEntry,
|
|
6226
|
+
replicationOwnershipLifecycleController,
|
|
6227
|
+
);
|
|
6228
|
+
} finally {
|
|
6229
|
+
const storageObjective = this._isAdaptiveReplicating
|
|
6230
|
+
? this.replicationController?.maxMemoryLimit
|
|
6231
|
+
: undefined;
|
|
6232
|
+
if (
|
|
6233
|
+
this._isAdaptiveReplicating !== wasAdaptiveReplicating ||
|
|
6234
|
+
storageObjective !== previousStorageObjective
|
|
6235
|
+
) {
|
|
6236
|
+
this.scheduleReplicationStatusRefresh();
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6169
6239
|
}
|
|
6170
6240
|
|
|
6171
6241
|
private async _unreplicate(
|
|
@@ -6535,6 +6605,13 @@ export class SharedLog<
|
|
|
6535
6605
|
}
|
|
6536
6606
|
return;
|
|
6537
6607
|
}
|
|
6608
|
+
if (deleted.length > 0 || (wasReplicator && !ownerHasRanges)) {
|
|
6609
|
+
// Some committed removals intentionally suppress replication:change,
|
|
6610
|
+
// and liveness cleanup can retire membership without a leave event.
|
|
6611
|
+
// Refresh from the committed post-state instead of relying on either
|
|
6612
|
+
// optional notification path.
|
|
6613
|
+
this.scheduleReplicationStatusRefresh();
|
|
6614
|
+
}
|
|
6538
6615
|
if (options?.noEvent !== true && deleted.length > 0) {
|
|
6539
6616
|
const publicKey = toLocalPublicSignKey(key);
|
|
6540
6617
|
if (publicKey) {
|
|
@@ -14326,6 +14403,7 @@ export class SharedLog<
|
|
|
14326
14403
|
// _receiveOwnershipRevision and _receiveOwnershipMutationAdmissions
|
|
14327
14404
|
// all start at 0 on the incoming lifecycle.
|
|
14328
14405
|
this.startRepairLifecycle();
|
|
14406
|
+
this.resetReplicationStatusLifecycle();
|
|
14329
14407
|
// Guard: between startRepairLifecycle() and
|
|
14330
14408
|
// resetSubscriptionChangeCallbackTracking() the fresh lifecycle's
|
|
14331
14409
|
// membership controller is undefined (legacy exposed the previous
|
|
@@ -14509,10 +14587,24 @@ export class SharedLog<
|
|
|
14509
14587
|
});
|
|
14510
14588
|
const invalidateLeaderSelectionContext = () =>
|
|
14511
14589
|
this.invalidateLeaderSelectionContextCache();
|
|
14590
|
+
const onReplicationStatusInputChange = () =>
|
|
14591
|
+
this.scheduleReplicationStatusRefresh();
|
|
14512
14592
|
this.events.addEventListener(
|
|
14513
14593
|
"replication:change",
|
|
14514
14594
|
invalidateLeaderSelectionContext,
|
|
14515
14595
|
);
|
|
14596
|
+
this.events.addEventListener(
|
|
14597
|
+
"replication:change",
|
|
14598
|
+
onReplicationStatusInputChange,
|
|
14599
|
+
);
|
|
14600
|
+
this.events.addEventListener(
|
|
14601
|
+
"replicator:join",
|
|
14602
|
+
onReplicationStatusInputChange,
|
|
14603
|
+
);
|
|
14604
|
+
this.events.addEventListener(
|
|
14605
|
+
"replicator:leave",
|
|
14606
|
+
onReplicationStatusInputChange,
|
|
14607
|
+
);
|
|
14516
14608
|
this.events.addEventListener(
|
|
14517
14609
|
"replicator:mature",
|
|
14518
14610
|
invalidateLeaderSelectionContext,
|
|
@@ -14522,6 +14614,18 @@ export class SharedLog<
|
|
|
14522
14614
|
"replication:change",
|
|
14523
14615
|
invalidateLeaderSelectionContext,
|
|
14524
14616
|
);
|
|
14617
|
+
this.events.removeEventListener(
|
|
14618
|
+
"replication:change",
|
|
14619
|
+
onReplicationStatusInputChange,
|
|
14620
|
+
);
|
|
14621
|
+
this.events.removeEventListener(
|
|
14622
|
+
"replicator:join",
|
|
14623
|
+
onReplicationStatusInputChange,
|
|
14624
|
+
);
|
|
14625
|
+
this.events.removeEventListener(
|
|
14626
|
+
"replicator:leave",
|
|
14627
|
+
onReplicationStatusInputChange,
|
|
14628
|
+
);
|
|
14525
14629
|
this.events.removeEventListener(
|
|
14526
14630
|
"replicator:mature",
|
|
14527
14631
|
invalidateLeaderSelectionContext,
|
|
@@ -15036,6 +15140,7 @@ export class SharedLog<
|
|
|
15036
15140
|
}, RECALCULATE_PARTICIPATION_DEBOUNCE_INTERVAL);
|
|
15037
15141
|
|
|
15038
15142
|
this._instanceLifecycle!.markOpenComplete();
|
|
15143
|
+
this.scheduleReplicationStatusRefresh();
|
|
15039
15144
|
}
|
|
15040
15145
|
|
|
15041
15146
|
private toNativeReplicationRange(
|
|
@@ -15764,6 +15869,7 @@ export class SharedLog<
|
|
|
15764
15869
|
const promises: Promise<any>[] = [];
|
|
15765
15870
|
const iterator = this.replicationIndex.iterate();
|
|
15766
15871
|
const checkedIsAlive = new Set<string>();
|
|
15872
|
+
const selfHash = this.node.identity.publicKey.hashcode();
|
|
15767
15873
|
|
|
15768
15874
|
while (!iterator.done()) {
|
|
15769
15875
|
const segments = await iterator.next(1000);
|
|
@@ -15775,9 +15881,13 @@ export class SharedLog<
|
|
|
15775
15881
|
for (const segment of segments) {
|
|
15776
15882
|
if (
|
|
15777
15883
|
checkedIsAlive.has(segment.value.hash) ||
|
|
15778
|
-
|
|
15884
|
+
selfHash === segment.value.hash
|
|
15779
15885
|
) {
|
|
15780
|
-
this.uniqueReplicators.
|
|
15886
|
+
if (!this.uniqueReplicators.has(selfHash)) {
|
|
15887
|
+
this.uniqueReplicators.add(selfHash);
|
|
15888
|
+
this.invalidateLeaderSelectionContextCache();
|
|
15889
|
+
this.scheduleReplicationStatusRefresh();
|
|
15890
|
+
}
|
|
15781
15891
|
continue;
|
|
15782
15892
|
}
|
|
15783
15893
|
|
|
@@ -16076,6 +16186,208 @@ export class SharedLog<
|
|
|
16076
16186
|
/* ((await this.log.entryIndex?.getMemoryUsage()) || 0) */ // + (await this.log.blocks.size())
|
|
16077
16187
|
}
|
|
16078
16188
|
|
|
16189
|
+
private resetReplicationStatusLifecycle(): void {
|
|
16190
|
+
this._replicationStatus = undefined;
|
|
16191
|
+
this._replicationStatusReadTail = Promise.resolve();
|
|
16192
|
+
this._replicationStatusRefreshScheduled = false;
|
|
16193
|
+
this._replicationStatusRefreshDirty = false;
|
|
16194
|
+
}
|
|
16195
|
+
|
|
16196
|
+
private isReplicationStatusLifecycleCurrent(
|
|
16197
|
+
lifecycle: InstanceLifecycle | undefined,
|
|
16198
|
+
): lifecycle is InstanceLifecycle {
|
|
16199
|
+
return (
|
|
16200
|
+
lifecycle != null &&
|
|
16201
|
+
lifecycle === this._instanceLifecycle &&
|
|
16202
|
+
lifecycle.phase() === "active"
|
|
16203
|
+
);
|
|
16204
|
+
}
|
|
16205
|
+
|
|
16206
|
+
private async measureReplicationStatus(
|
|
16207
|
+
lifecycle: InstanceLifecycle,
|
|
16208
|
+
): Promise<ReplicationStatus> {
|
|
16209
|
+
if (!this.isReplicationStatusLifecycleCurrent(lifecycle)) {
|
|
16210
|
+
throw new ClosedError();
|
|
16211
|
+
}
|
|
16212
|
+
|
|
16213
|
+
const capturedRoleGeneration = lifecycle.roleGeneration;
|
|
16214
|
+
const capturedOwnershipRevision = lifecycle._receiveOwnershipRevision;
|
|
16215
|
+
if (lifecycle._receiveOwnershipMutationAdmissions !== 0) {
|
|
16216
|
+
throw new ReplicationStatusSnapshotChangedError();
|
|
16217
|
+
}
|
|
16218
|
+
|
|
16219
|
+
// Capture every synchronous policy/membership input before yielding. The
|
|
16220
|
+
// generation and ownership checks below reject the whole measurement if a
|
|
16221
|
+
// same-open role or range mutation races either asynchronous metric read.
|
|
16222
|
+
const defaultReplicaTarget = this.replicas.min.getValue(this);
|
|
16223
|
+
const isAdaptiveReplicating = this._isAdaptiveReplicating;
|
|
16224
|
+
const storageObjectiveBytes = isAdaptiveReplicating
|
|
16225
|
+
? this.replicationController?.maxMemoryLimit
|
|
16226
|
+
: undefined;
|
|
16227
|
+
const activeReplicators = this.uniqueReplicators.size;
|
|
16228
|
+
const [storageUsedBytes, rangeCoverage] = await Promise.all([
|
|
16229
|
+
this.getMemoryUsage(),
|
|
16230
|
+
this.calculateCoverage(),
|
|
16231
|
+
]);
|
|
16232
|
+
if (!this.isReplicationStatusLifecycleCurrent(lifecycle)) {
|
|
16233
|
+
throw new ClosedError();
|
|
16234
|
+
}
|
|
16235
|
+
const currentStorageObjectiveBytes = this._isAdaptiveReplicating
|
|
16236
|
+
? this.replicationController?.maxMemoryLimit
|
|
16237
|
+
: undefined;
|
|
16238
|
+
if (
|
|
16239
|
+
!lifecycle.isRoleCurrent(capturedRoleGeneration) ||
|
|
16240
|
+
!this.isReceiveOwnershipSnapshotStable(capturedOwnershipRevision) ||
|
|
16241
|
+
this.replicas.min.getValue(this) !== defaultReplicaTarget ||
|
|
16242
|
+
this._isAdaptiveReplicating !== isAdaptiveReplicating ||
|
|
16243
|
+
currentStorageObjectiveBytes !== storageObjectiveBytes ||
|
|
16244
|
+
this.uniqueReplicators.size !== activeReplicators
|
|
16245
|
+
) {
|
|
16246
|
+
throw new ReplicationStatusSnapshotChangedError();
|
|
16247
|
+
}
|
|
16248
|
+
const status = classifyReplicationStatus({
|
|
16249
|
+
storageUsedBytes,
|
|
16250
|
+
storageObjectiveBytes,
|
|
16251
|
+
rangeCoverage,
|
|
16252
|
+
defaultReplicaTarget,
|
|
16253
|
+
activeReplicators,
|
|
16254
|
+
});
|
|
16255
|
+
const previous = this._replicationStatus;
|
|
16256
|
+
this._replicationStatus = status;
|
|
16257
|
+
const transitioned =
|
|
16258
|
+
previous == null ||
|
|
16259
|
+
previous.reasons.length !== status.reasons.length ||
|
|
16260
|
+
previous.reasons.some(
|
|
16261
|
+
(reason, index) => reason !== status.reasons[index],
|
|
16262
|
+
);
|
|
16263
|
+
if (transitioned) {
|
|
16264
|
+
this.events.dispatchEvent(
|
|
16265
|
+
new CustomEvent<ReplicationStatusEvent>("replication:status", {
|
|
16266
|
+
detail: status,
|
|
16267
|
+
}),
|
|
16268
|
+
);
|
|
16269
|
+
}
|
|
16270
|
+
return status;
|
|
16271
|
+
}
|
|
16272
|
+
|
|
16273
|
+
/**
|
|
16274
|
+
* Measure local replication health. The snapshot and event are advisory:
|
|
16275
|
+
* they are not persisted, sent to peers, or consumed by replication logic.
|
|
16276
|
+
* Event emission is deduplicated by the ordered reason set, so metric drift
|
|
16277
|
+
* within the same state does not create event traffic. Explicit measurements
|
|
16278
|
+
* work without listeners; automatic refreshes run only while the status event
|
|
16279
|
+
* has at least one listener.
|
|
16280
|
+
*/
|
|
16281
|
+
getReplicationStatus(): Promise<ReplicationStatus> {
|
|
16282
|
+
const lifecycle = this._instanceLifecycle;
|
|
16283
|
+
if (!this.isReplicationStatusLifecycleCurrent(lifecycle)) {
|
|
16284
|
+
return Promise.reject(new ClosedError());
|
|
16285
|
+
}
|
|
16286
|
+
const previous = this._replicationStatusReadTail ?? Promise.resolve();
|
|
16287
|
+
const operation = previous
|
|
16288
|
+
.catch(() => {})
|
|
16289
|
+
.then(async () => {
|
|
16290
|
+
let staleError: ReplicationStatusSnapshotChangedError | undefined;
|
|
16291
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
16292
|
+
try {
|
|
16293
|
+
return await this.measureReplicationStatus(lifecycle);
|
|
16294
|
+
} catch (error) {
|
|
16295
|
+
if (!(error instanceof ReplicationStatusSnapshotChangedError)) {
|
|
16296
|
+
throw error;
|
|
16297
|
+
}
|
|
16298
|
+
staleError = error;
|
|
16299
|
+
if (attempt === 0) {
|
|
16300
|
+
const mutationTail = this._replicationRangeMutationTail;
|
|
16301
|
+
await mutationTail.catch(() => {});
|
|
16302
|
+
}
|
|
16303
|
+
}
|
|
16304
|
+
}
|
|
16305
|
+
throw staleError!;
|
|
16306
|
+
});
|
|
16307
|
+
this._replicationStatusReadTail = operation.then(
|
|
16308
|
+
() => {},
|
|
16309
|
+
() => {},
|
|
16310
|
+
);
|
|
16311
|
+
return operation;
|
|
16312
|
+
}
|
|
16313
|
+
|
|
16314
|
+
private scheduleReplicationStatusRefresh(): void {
|
|
16315
|
+
if (this.events.listenerCount("replication:status") === 0) {
|
|
16316
|
+
return;
|
|
16317
|
+
}
|
|
16318
|
+
const lifecycle = this._instanceLifecycle;
|
|
16319
|
+
if (!this.isReplicationStatusLifecycleCurrent(lifecycle)) {
|
|
16320
|
+
return;
|
|
16321
|
+
}
|
|
16322
|
+
if (this._replicationStatusRefreshScheduled) {
|
|
16323
|
+
this._replicationStatusRefreshDirty = true;
|
|
16324
|
+
return;
|
|
16325
|
+
}
|
|
16326
|
+
this._replicationStatusRefreshScheduled = true;
|
|
16327
|
+
queueMicrotask(() => {
|
|
16328
|
+
if (lifecycle !== this._instanceLifecycle) {
|
|
16329
|
+
return;
|
|
16330
|
+
}
|
|
16331
|
+
if (this.events.listenerCount("replication:status") === 0) {
|
|
16332
|
+
this._replicationStatusRefreshScheduled = false;
|
|
16333
|
+
this._replicationStatusRefreshDirty = false;
|
|
16334
|
+
return;
|
|
16335
|
+
}
|
|
16336
|
+
// Calls coalesced before the microtask starts are reflected by this scan.
|
|
16337
|
+
// Calls arriving while it is in flight set the bit again and receive one
|
|
16338
|
+
// follow-up scan for the latest state.
|
|
16339
|
+
this._replicationStatusRefreshDirty = false;
|
|
16340
|
+
if (!this.isReplicationStatusLifecycleCurrent(lifecycle)) {
|
|
16341
|
+
this._replicationStatusRefreshScheduled = false;
|
|
16342
|
+
return;
|
|
16343
|
+
}
|
|
16344
|
+
void this.getReplicationStatus()
|
|
16345
|
+
.catch((error) => {
|
|
16346
|
+
if (error instanceof ReplicationStatusSnapshotChangedError) {
|
|
16347
|
+
this._replicationStatusRefreshDirty = true;
|
|
16348
|
+
return;
|
|
16349
|
+
}
|
|
16350
|
+
if (
|
|
16351
|
+
this.isReplicationStatusLifecycleCurrent(lifecycle) &&
|
|
16352
|
+
!(error instanceof ClosedError) &&
|
|
16353
|
+
!isNotStartedError(error as Error)
|
|
16354
|
+
) {
|
|
16355
|
+
logger.error(error);
|
|
16356
|
+
}
|
|
16357
|
+
})
|
|
16358
|
+
.then(() => {
|
|
16359
|
+
if (lifecycle !== this._instanceLifecycle) {
|
|
16360
|
+
return;
|
|
16361
|
+
}
|
|
16362
|
+
this._replicationStatusRefreshScheduled = false;
|
|
16363
|
+
if (!this.isReplicationStatusLifecycleCurrent(lifecycle)) {
|
|
16364
|
+
this._replicationStatusRefreshDirty = false;
|
|
16365
|
+
return;
|
|
16366
|
+
}
|
|
16367
|
+
if (this._replicationStatusRefreshDirty) {
|
|
16368
|
+
this._replicationStatusRefreshDirty = false;
|
|
16369
|
+
this.scheduleReplicationStatusRefresh();
|
|
16370
|
+
}
|
|
16371
|
+
});
|
|
16372
|
+
});
|
|
16373
|
+
}
|
|
16374
|
+
|
|
16375
|
+
private scheduleReplicationStatusRefreshForStorage(
|
|
16376
|
+
storageUsedBytes: number,
|
|
16377
|
+
): void {
|
|
16378
|
+
const objective = this.replicationController?.maxMemoryLimit;
|
|
16379
|
+
if (!this._isAdaptiveReplicating || objective == null) {
|
|
16380
|
+
return;
|
|
16381
|
+
}
|
|
16382
|
+
const exceeded = storageUsedBytes > objective;
|
|
16383
|
+
const wasExceeded = this._replicationStatus?.reasons.includes(
|
|
16384
|
+
"storage-objective-exceeded",
|
|
16385
|
+
);
|
|
16386
|
+
if (wasExceeded == null || wasExceeded !== exceeded) {
|
|
16387
|
+
this.scheduleReplicationStatusRefresh();
|
|
16388
|
+
}
|
|
16389
|
+
}
|
|
16390
|
+
|
|
16079
16391
|
/** Return a detached snapshot of effective shared-log runtime settings. */
|
|
16080
16392
|
getRuntimeSnapshot(): SharedLogRuntimeSnapshot {
|
|
16081
16393
|
const nativeGraph = this.log.entryIndex.properties.nativeGraph;
|
|
@@ -16856,6 +17168,7 @@ export class SharedLog<
|
|
|
16856
17168
|
private async _close(options?: { preserveDropRetryResources?: boolean }) {
|
|
16857
17169
|
this.stopRepairLifecycle();
|
|
16858
17170
|
this._instanceLifecycle?.beginTerminal("internal-close");
|
|
17171
|
+
this.resetReplicationStatusLifecycle();
|
|
16859
17172
|
const preserveDropRetryResources =
|
|
16860
17173
|
options?.preserveDropRetryResources === true;
|
|
16861
17174
|
let firstError: unknown;
|
|
@@ -17049,6 +17362,7 @@ export class SharedLog<
|
|
|
17049
17362
|
this.preventParentAttachments();
|
|
17050
17363
|
this.stopRepairLifecycle();
|
|
17051
17364
|
this._instanceLifecycle?.beginTerminal("close");
|
|
17365
|
+
this.resetReplicationStatusLifecycle();
|
|
17052
17366
|
this._v2Receive?.clearForClose();
|
|
17053
17367
|
const replicationRangeTerminalFence =
|
|
17054
17368
|
this.acquireReplicationRangeMutationTerminalFence();
|
|
@@ -17186,6 +17500,7 @@ export class SharedLog<
|
|
|
17186
17500
|
this.preventParentAttachments();
|
|
17187
17501
|
this.stopRepairLifecycle();
|
|
17188
17502
|
this._instanceLifecycle?.beginTerminal("drop");
|
|
17503
|
+
this.resetReplicationStatusLifecycle();
|
|
17189
17504
|
this._v2Receive?.clearForClose();
|
|
17190
17505
|
const replicationRangeTerminalFence =
|
|
17191
17506
|
this.acquireReplicationRangeMutationTerminalFence();
|
|
@@ -25012,6 +25327,7 @@ export class SharedLog<
|
|
|
25012
25327
|
const peers = this.replicationIndex;
|
|
25013
25328
|
const usedMemory = await this.getMemoryUsage();
|
|
25014
25329
|
if (!isCurrent()) return false;
|
|
25330
|
+
this.scheduleReplicationStatusRefreshForStorage(usedMemory);
|
|
25015
25331
|
let dynamicRange = await this.getDynamicRange();
|
|
25016
25332
|
if (!isCurrent()) return false;
|
|
25017
25333
|
|
package/src/like.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import type { Entry } from "@peerbit/log";
|
|
8
8
|
import type { ShallowEntry } from "@peerbit/log";
|
|
9
9
|
import type { ReplicationOptions, ReplicationRangeIndexable } from "./index.js";
|
|
10
|
+
import type { ReplicationStatus } from "./replication-status.js";
|
|
10
11
|
|
|
11
12
|
export type LogBlocksLike = {
|
|
12
13
|
has: (hash: string) => Promise<boolean> | boolean;
|
|
@@ -80,5 +81,7 @@ export type SharedLogLike<T = any, R extends "u32" | "u64" = any> = {
|
|
|
80
81
|
}) => Promise<number>;
|
|
81
82
|
getMyReplicationSegments: () => Promise<ReplicationRangeIndexable<R>[]>;
|
|
82
83
|
getAllReplicationSegments: () => Promise<ReplicationRangeIndexable<R>[]>;
|
|
84
|
+
/** Optional for proxy/legacy implementations that predate local telemetry. */
|
|
85
|
+
getReplicationStatus?: () => Promise<ReplicationStatus>;
|
|
83
86
|
close: () => Promise<void | boolean>;
|
|
84
87
|
};
|
package/src/pid.ts
CHANGED
|
@@ -46,13 +46,13 @@ export class PIDReplicationController {
|
|
|
46
46
|
let errorMemory = 0;
|
|
47
47
|
|
|
48
48
|
if (this.maxMemoryLimit != null) {
|
|
49
|
-
// Treat the configured storage limit as a
|
|
50
|
-
//
|
|
51
|
-
// prunes
|
|
49
|
+
// Treat the configured storage limit as a soft operating objective, not
|
|
50
|
+
// an exact set point. A small reserve helps discrete entry sizes and delayed
|
|
51
|
+
// checked prunes settle near that objective without repeated overshoot.
|
|
52
52
|
const effectiveMemoryLimit =
|
|
53
53
|
this.maxMemoryLimit > 0 ? this.maxMemoryLimit * 0.95 : 0;
|
|
54
54
|
if (effectiveMemoryLimit <= 0) {
|
|
55
|
-
// A zero storage
|
|
55
|
+
// A zero storage objective can never hold data; drive the factor to zero
|
|
56
56
|
// instead of leaving the memory term neutral.
|
|
57
57
|
errorMemory = -currentFactor;
|
|
58
58
|
} else if (currentFactor > 0 && memoryUsage > 0) {
|
package/src/ranges.ts
CHANGED
|
@@ -1490,7 +1490,7 @@ export const calculateCoverage = async <R extends "u32" | "u64">(properties: {
|
|
|
1490
1490
|
// When points are equal, process a start (delta +1) before an end (delta -1)
|
|
1491
1491
|
endpoints.sort((a, b) => {
|
|
1492
1492
|
if (a.point === b.point) return b.delta - a.delta;
|
|
1493
|
-
return
|
|
1493
|
+
return a.point < b.point ? -1 : 1;
|
|
1494
1494
|
});
|
|
1495
1495
|
|
|
1496
1496
|
// If there are no endpoints at all, nothing covers the content range.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const REPLICATION_STATUS_REASONS = [
|
|
2
|
+
"storage-objective-exceeded",
|
|
3
|
+
"range-coverage-underfilled",
|
|
4
|
+
"default-replica-target-unattainable",
|
|
5
|
+
] as const;
|
|
6
|
+
|
|
7
|
+
export type ReplicationStatusReason =
|
|
8
|
+
(typeof REPLICATION_STATUS_REASONS)[number];
|
|
9
|
+
|
|
10
|
+
export type ReplicationStatusInput = Readonly<{
|
|
11
|
+
storageUsedBytes: number;
|
|
12
|
+
storageObjectiveBytes?: number;
|
|
13
|
+
rangeCoverage: number;
|
|
14
|
+
defaultReplicaTarget: number;
|
|
15
|
+
activeReplicators: number;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Local, advisory replication health. This snapshot is never persisted or
|
|
20
|
+
* exchanged with another peer, and must not be used as placement or pruning
|
|
21
|
+
* authority.
|
|
22
|
+
*/
|
|
23
|
+
export type ReplicationStatus = Readonly<{
|
|
24
|
+
healthy: boolean;
|
|
25
|
+
reasons: readonly ReplicationStatusReason[];
|
|
26
|
+
storage: Readonly<{
|
|
27
|
+
usedBytes: number;
|
|
28
|
+
objectiveBytes?: number;
|
|
29
|
+
/** `limits.storage` is a PID objective, not an enforced quota. */
|
|
30
|
+
semantics: "soft-objective";
|
|
31
|
+
}>;
|
|
32
|
+
coverage: Readonly<{
|
|
33
|
+
actual: number;
|
|
34
|
+
target: number;
|
|
35
|
+
}>;
|
|
36
|
+
replicas: Readonly<{
|
|
37
|
+
active: number;
|
|
38
|
+
target: number;
|
|
39
|
+
}>;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
/** Classify a detached measurement without reading or mutating runtime state. */
|
|
43
|
+
export const classifyReplicationStatus = (
|
|
44
|
+
input: ReplicationStatusInput,
|
|
45
|
+
): ReplicationStatus => {
|
|
46
|
+
const reasons: ReplicationStatusReason[] = [];
|
|
47
|
+
if (
|
|
48
|
+
input.storageObjectiveBytes != null &&
|
|
49
|
+
input.storageUsedBytes > input.storageObjectiveBytes
|
|
50
|
+
) {
|
|
51
|
+
reasons.push("storage-objective-exceeded");
|
|
52
|
+
}
|
|
53
|
+
if (input.rangeCoverage < input.defaultReplicaTarget) {
|
|
54
|
+
reasons.push("range-coverage-underfilled");
|
|
55
|
+
}
|
|
56
|
+
if (input.activeReplicators < input.defaultReplicaTarget) {
|
|
57
|
+
reasons.push("default-replica-target-unattainable");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
healthy: reasons.length === 0,
|
|
62
|
+
reasons: Object.freeze(reasons),
|
|
63
|
+
storage: Object.freeze({
|
|
64
|
+
usedBytes: input.storageUsedBytes,
|
|
65
|
+
objectiveBytes: input.storageObjectiveBytes,
|
|
66
|
+
semantics: "soft-objective" as const,
|
|
67
|
+
}),
|
|
68
|
+
coverage: Object.freeze({
|
|
69
|
+
actual: input.rangeCoverage,
|
|
70
|
+
target: input.defaultReplicaTarget,
|
|
71
|
+
}),
|
|
72
|
+
replicas: Object.freeze({
|
|
73
|
+
active: input.activeReplicators,
|
|
74
|
+
target: input.defaultReplicaTarget,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
};
|
package/src/sync/factory.ts
CHANGED
|
@@ -87,11 +87,20 @@ export function createSyncronizer<R extends "u32" | "u64">(
|
|
|
87
87
|
end1: bigint | number;
|
|
88
88
|
start2: bigint | number;
|
|
89
89
|
end2: bigint | number;
|
|
90
|
+
limit?: number;
|
|
90
91
|
}) => {
|
|
91
92
|
const nativeState = props.getNativeState();
|
|
93
|
+
const limit = range.limit;
|
|
94
|
+
if (limit == null) {
|
|
95
|
+
return (
|
|
96
|
+
nativeState?.getEntryHashNumbersInRangeU64?.(range) ??
|
|
97
|
+
nativeState?.getEntryHashNumbersInRange(range)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const limitedRange = { ...range, limit };
|
|
92
101
|
return (
|
|
93
|
-
nativeState?.
|
|
94
|
-
nativeState?.
|
|
102
|
+
nativeState?.getEntryHashNumbersInRangeU64Limited?.(limitedRange) ??
|
|
103
|
+
nativeState?.getEntryHashNumbersInRangeLimited?.(limitedRange)
|
|
95
104
|
);
|
|
96
105
|
};
|
|
97
106
|
|
package/src/sync/index.ts
CHANGED
|
@@ -34,6 +34,14 @@ export type SyncOptions<R extends "u32" | "u64"> = {
|
|
|
34
34
|
*/
|
|
35
35
|
maxSimpleEntries?: number;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Maximum number of local entries materialized into the range encoder for
|
|
39
|
+
* one incoming rateless sync. Ranges above this limit fall back to bounded
|
|
40
|
+
* simple sync. Defaults to 16,384. This is a per-process entry cap, not an
|
|
41
|
+
* aggregate byte budget across concurrent syncs.
|
|
42
|
+
*/
|
|
43
|
+
maxRatelessReceiveRangeEntries?: number;
|
|
44
|
+
|
|
37
45
|
/**
|
|
38
46
|
* Maximum number of hash strings in one simple sync message.
|
|
39
47
|
*/
|
|
@@ -141,16 +149,18 @@ export type HashSymbolResolver = (
|
|
|
141
149
|
|
|
142
150
|
export type HashSymbolHashListResolver = (
|
|
143
151
|
symbols: HashSymbolInput,
|
|
144
|
-
) =>
|
|
145
|
-
| Iterable<string>
|
|
146
|
-
| undefined
|
|
147
|
-
| Promise<Iterable<string> | undefined>;
|
|
152
|
+
) => Iterable<string> | undefined | Promise<Iterable<string> | undefined>;
|
|
148
153
|
|
|
149
154
|
export type HashSymbolRangeResolver = (range: {
|
|
150
155
|
start1: bigint | number;
|
|
151
156
|
end1: bigint | number;
|
|
152
157
|
start2: bigint | number;
|
|
153
158
|
end2: bigint | number;
|
|
159
|
+
/**
|
|
160
|
+
* Maximum values to return, including any overflow sentinel. Omitted only by
|
|
161
|
+
* legacy custom synchronizers; built-in receive paths always provide it.
|
|
162
|
+
*/
|
|
163
|
+
limit?: number;
|
|
154
164
|
}) =>
|
|
155
165
|
| Iterable<bigint | number>
|
|
156
166
|
| undefined
|