@peerbit/shared-log 12.2.0-3333888 → 12.2.0-3885fc9
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.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +193 -50
- package/dist/src/index.js.map +1 -1
- package/dist/src/ranges.d.ts +1 -0
- package/dist/src/ranges.d.ts.map +1 -1
- package/dist/src/ranges.js +48 -18
- package/dist/src/ranges.js.map +1 -1
- package/package.json +18 -18
- package/src/index.ts +244 -89
- package/src/ranges.ts +97 -65
package/src/index.ts
CHANGED
|
@@ -366,6 +366,8 @@ export type SharedLogOptions<
|
|
|
366
366
|
syncronizer?: SynchronizerConstructor<R>;
|
|
367
367
|
timeUntilRoleMaturity?: number;
|
|
368
368
|
waitForReplicatorTimeout?: number;
|
|
369
|
+
waitForReplicatorRequestIntervalMs?: number;
|
|
370
|
+
waitForReplicatorRequestMaxAttempts?: number;
|
|
369
371
|
waitForPruneDelay?: number;
|
|
370
372
|
distributionDebounceTime?: number;
|
|
371
373
|
compatibility?: number;
|
|
@@ -376,6 +378,8 @@ export type SharedLogOptions<
|
|
|
376
378
|
export const DEFAULT_MIN_REPLICAS = 2;
|
|
377
379
|
export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
|
|
378
380
|
export const WAIT_FOR_ROLE_MATURITY = 5000;
|
|
381
|
+
export const WAIT_FOR_REPLICATOR_REQUEST_INTERVAL = 1000;
|
|
382
|
+
export const WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS = 3;
|
|
379
383
|
// TODO(prune): Investigate if/when a non-zero prune delay is required for correctness
|
|
380
384
|
// (e.g. responsibility/replication-info message reordering in multi-peer scenarios).
|
|
381
385
|
// Prefer making pruning robust without timing-based heuristics.
|
|
@@ -463,6 +467,7 @@ export class SharedLog<
|
|
|
463
467
|
private recentlyRebalanced!: Cache<string>;
|
|
464
468
|
|
|
465
469
|
uniqueReplicators!: Set<string>;
|
|
470
|
+
private _replicatorsReconciled!: boolean;
|
|
466
471
|
|
|
467
472
|
/* private _totalParticipation!: number; */
|
|
468
473
|
|
|
@@ -563,6 +568,8 @@ export class SharedLog<
|
|
|
563
568
|
|
|
564
569
|
timeUntilRoleMaturity!: number;
|
|
565
570
|
waitForReplicatorTimeout!: number;
|
|
571
|
+
waitForReplicatorRequestIntervalMs!: number;
|
|
572
|
+
waitForReplicatorRequestMaxAttempts?: number;
|
|
566
573
|
waitForPruneDelay!: number;
|
|
567
574
|
distributionDebounceTime!: number;
|
|
568
575
|
|
|
@@ -1164,16 +1171,31 @@ export class SharedLog<
|
|
|
1164
1171
|
|
|
1165
1172
|
let prevCount = deleted.length;
|
|
1166
1173
|
|
|
1167
|
-
|
|
1174
|
+
const existingById = new Map(deleted.map((x) => [x.idString, x]));
|
|
1175
|
+
const hasSameRanges =
|
|
1176
|
+
deleted.length === ranges.length &&
|
|
1177
|
+
ranges.every((range) => {
|
|
1178
|
+
const existing = existingById.get(range.idString);
|
|
1179
|
+
return existing != null && existing.equalRange(range);
|
|
1180
|
+
});
|
|
1168
1181
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
})
|
|
1176
|
-
|
|
1182
|
+
// Avoid churn on repeated full-state announcements that don't change any
|
|
1183
|
+
// replication ranges. This prevents unnecessary `replication:change`
|
|
1184
|
+
// events and rebalancing cascades.
|
|
1185
|
+
if (hasSameRanges) {
|
|
1186
|
+
diffs = [];
|
|
1187
|
+
} else {
|
|
1188
|
+
await this.replicationIndex.del({ query: { hash: from.hashcode() } });
|
|
1189
|
+
|
|
1190
|
+
diffs = [
|
|
1191
|
+
...deleted.map((x) => {
|
|
1192
|
+
return { range: x, type: "removed" as const, timestamp };
|
|
1193
|
+
}),
|
|
1194
|
+
...ranges.map((x) => {
|
|
1195
|
+
return { range: x, type: "added" as const, timestamp };
|
|
1196
|
+
}),
|
|
1197
|
+
];
|
|
1198
|
+
}
|
|
1177
1199
|
|
|
1178
1200
|
isNewReplicator = prevCount === 0 && ranges.length > 0;
|
|
1179
1201
|
} else {
|
|
@@ -1876,6 +1898,7 @@ export class SharedLog<
|
|
|
1876
1898
|
this.recentlyRebalanced = new Cache<string>({ max: 1e4, ttl: 1e5 });
|
|
1877
1899
|
|
|
1878
1900
|
this.uniqueReplicators = new Set();
|
|
1901
|
+
this._replicatorsReconciled = false;
|
|
1879
1902
|
|
|
1880
1903
|
this.openTime = +new Date();
|
|
1881
1904
|
this.oldestOpenTime = this.openTime;
|
|
@@ -1886,12 +1909,31 @@ export class SharedLog<
|
|
|
1886
1909
|
options?.timeUntilRoleMaturity ?? WAIT_FOR_ROLE_MATURITY;
|
|
1887
1910
|
this.waitForReplicatorTimeout =
|
|
1888
1911
|
options?.waitForReplicatorTimeout ?? WAIT_FOR_REPLICATOR_TIMEOUT;
|
|
1912
|
+
this.waitForReplicatorRequestIntervalMs =
|
|
1913
|
+
options?.waitForReplicatorRequestIntervalMs ??
|
|
1914
|
+
WAIT_FOR_REPLICATOR_REQUEST_INTERVAL;
|
|
1915
|
+
this.waitForReplicatorRequestMaxAttempts =
|
|
1916
|
+
options?.waitForReplicatorRequestMaxAttempts;
|
|
1889
1917
|
this.waitForPruneDelay = options?.waitForPruneDelay ?? WAIT_FOR_PRUNE_DELAY;
|
|
1890
1918
|
|
|
1891
1919
|
if (this.waitForReplicatorTimeout < this.timeUntilRoleMaturity) {
|
|
1892
1920
|
this.waitForReplicatorTimeout = this.timeUntilRoleMaturity; // does not makes sense to expect a replicator to mature faster than it is reachable
|
|
1893
1921
|
}
|
|
1894
1922
|
|
|
1923
|
+
if (this.waitForReplicatorRequestIntervalMs <= 0) {
|
|
1924
|
+
throw new Error(
|
|
1925
|
+
"waitForReplicatorRequestIntervalMs must be a positive number",
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
if (
|
|
1929
|
+
this.waitForReplicatorRequestMaxAttempts != null &&
|
|
1930
|
+
this.waitForReplicatorRequestMaxAttempts <= 0
|
|
1931
|
+
) {
|
|
1932
|
+
throw new Error(
|
|
1933
|
+
"waitForReplicatorRequestMaxAttempts must be a positive number",
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1895
1937
|
this._closeController = new AbortController();
|
|
1896
1938
|
this._isTrustedReplicator = options?.canReplicate;
|
|
1897
1939
|
this.keep = options?.keep;
|
|
@@ -2178,7 +2220,16 @@ export class SharedLog<
|
|
|
2178
2220
|
await super.afterOpen();
|
|
2179
2221
|
|
|
2180
2222
|
// We do this here, because these calls requires this.closed == false
|
|
2181
|
-
this.pruneOfflineReplicators()
|
|
2223
|
+
void this.pruneOfflineReplicators()
|
|
2224
|
+
.then(() => {
|
|
2225
|
+
this._replicatorsReconciled = true;
|
|
2226
|
+
})
|
|
2227
|
+
.catch((error) => {
|
|
2228
|
+
if (isNotStartedError(error as Error)) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
logger.error(error);
|
|
2232
|
+
});
|
|
2182
2233
|
|
|
2183
2234
|
await this.rebalanceParticipation();
|
|
2184
2235
|
|
|
@@ -2866,22 +2917,20 @@ export class SharedLog<
|
|
|
2866
2917
|
context.from!.hashcode(),
|
|
2867
2918
|
);
|
|
2868
2919
|
} else if (msg instanceof RequestReplicationInfoMessage) {
|
|
2869
|
-
// TODO this message type is never used, should we remove it?
|
|
2870
|
-
|
|
2871
2920
|
if (context.from.equals(this.node.identity.publicKey)) {
|
|
2872
2921
|
return;
|
|
2873
2922
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
x.toReplicationRange(),
|
|
2878
|
-
),
|
|
2879
|
-
}),
|
|
2880
|
-
{
|
|
2881
|
-
mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
|
|
2882
|
-
},
|
|
2923
|
+
|
|
2924
|
+
const segments = (await this.getMyReplicationSegments()).map((x) =>
|
|
2925
|
+
x.toReplicationRange(),
|
|
2883
2926
|
);
|
|
2884
2927
|
|
|
2928
|
+
this.rpc
|
|
2929
|
+
.send(new AllReplicatingSegmentsMessage({ segments }), {
|
|
2930
|
+
mode: new SeekDelivery({ to: [context.from], redundancy: 1 }),
|
|
2931
|
+
})
|
|
2932
|
+
.catch((e) => logger.error(e.toString()));
|
|
2933
|
+
|
|
2885
2934
|
// for backwards compatibility (v8) remove this when we are sure that all nodes are v9+
|
|
2886
2935
|
if (this.v8Behaviour) {
|
|
2887
2936
|
const role = this.getRole();
|
|
@@ -2903,73 +2952,60 @@ export class SharedLog<
|
|
|
2903
2952
|
}
|
|
2904
2953
|
}
|
|
2905
2954
|
} else if (
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
let replicationInfoMessage = msg as
|
|
2914
|
-
| AllReplicatingSegmentsMessage
|
|
2915
|
-
| AddedReplicationSegmentMessage;
|
|
2916
|
-
|
|
2917
|
-
// we have this statement because peers might have changed/announced their role,
|
|
2918
|
-
// but we don't know them as "subscribers" yet. i.e. they are not online
|
|
2919
|
-
|
|
2920
|
-
this.waitFor(context.from, {
|
|
2921
|
-
signal: this._closeController.signal,
|
|
2922
|
-
timeout: this.waitForReplicatorTimeout,
|
|
2923
|
-
})
|
|
2924
|
-
.then(async () => {
|
|
2925
|
-
// do use an operation log here, because we want to make sure that we don't miss any updates
|
|
2926
|
-
// and do them in the right order
|
|
2927
|
-
const prev = this.latestReplicationInfoMessage.get(
|
|
2928
|
-
context.from!.hashcode(),
|
|
2929
|
-
);
|
|
2955
|
+
msg instanceof AllReplicatingSegmentsMessage ||
|
|
2956
|
+
msg instanceof AddedReplicationSegmentMessage
|
|
2957
|
+
) {
|
|
2958
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2930
2961
|
|
|
2931
|
-
|
|
2962
|
+
const replicationInfoMessage = msg as
|
|
2963
|
+
| AllReplicatingSegmentsMessage
|
|
2964
|
+
| AddedReplicationSegmentMessage;
|
|
2965
|
+
|
|
2966
|
+
// Process replication updates even if the sender isn't yet considered "ready" by
|
|
2967
|
+
// `Program.waitFor()`. Dropping these messages can lead to missing replicator info
|
|
2968
|
+
// (and downstream `waitForReplicator()` timeouts) under timing-sensitive joins.
|
|
2969
|
+
const from = context.from!;
|
|
2970
|
+
const messageTimestamp = context.message.header.timestamp;
|
|
2971
|
+
(async () => {
|
|
2972
|
+
const prev = this.latestReplicationInfoMessage.get(from.hashcode());
|
|
2973
|
+
if (prev && prev > messageTimestamp) {
|
|
2932
2974
|
return;
|
|
2933
2975
|
}
|
|
2934
2976
|
|
|
2935
|
-
this.latestReplicationInfoMessage.set(
|
|
2936
|
-
context.from!.hashcode(),
|
|
2937
|
-
context.message.header.timestamp,
|
|
2938
|
-
);
|
|
2939
|
-
|
|
2940
|
-
let reset = msg instanceof AllReplicatingSegmentsMessage;
|
|
2977
|
+
this.latestReplicationInfoMessage.set(from.hashcode(), messageTimestamp);
|
|
2941
2978
|
|
|
2942
2979
|
if (this.closed) {
|
|
2943
2980
|
return;
|
|
2944
2981
|
}
|
|
2945
2982
|
|
|
2983
|
+
const reset = msg instanceof AllReplicatingSegmentsMessage;
|
|
2946
2984
|
await this.addReplicationRange(
|
|
2947
2985
|
replicationInfoMessage.segments.map((x) =>
|
|
2948
|
-
x.toReplicationRangeIndexable(
|
|
2986
|
+
x.toReplicationRangeIndexable(from),
|
|
2949
2987
|
),
|
|
2950
|
-
|
|
2988
|
+
from,
|
|
2951
2989
|
{
|
|
2952
2990
|
reset,
|
|
2953
2991
|
checkDuplicates: true,
|
|
2954
|
-
timestamp: Number(
|
|
2992
|
+
timestamp: Number(messageTimestamp),
|
|
2955
2993
|
},
|
|
2956
2994
|
);
|
|
2957
|
-
|
|
2958
|
-
/* await this._modifyReplicators(msg.role, context.from!); */
|
|
2959
|
-
})
|
|
2960
|
-
.catch((e) => {
|
|
2995
|
+
})().catch((e) => {
|
|
2961
2996
|
if (isNotStartedError(e)) {
|
|
2962
2997
|
return;
|
|
2963
2998
|
}
|
|
2964
2999
|
logger.error(
|
|
2965
|
-
|
|
2966
|
-
e?.message
|
|
3000
|
+
`Failed to apply replication settings from '${from.hashcode()}': ${
|
|
3001
|
+
e?.message ?? e
|
|
3002
|
+
}`,
|
|
2967
3003
|
);
|
|
2968
3004
|
});
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3005
|
+
} else if (msg instanceof StoppedReplicating) {
|
|
3006
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
2973
3009
|
|
|
2974
3010
|
const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(
|
|
2975
3011
|
msg.segmentIds,
|
|
@@ -3303,6 +3339,7 @@ export class SharedLog<
|
|
|
3303
3339
|
|
|
3304
3340
|
let settled = false;
|
|
3305
3341
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
3342
|
+
let requestTimer: ReturnType<typeof setTimeout> | undefined;
|
|
3306
3343
|
|
|
3307
3344
|
const clear = () => {
|
|
3308
3345
|
this.events.removeEventListener("replicator:mature", check);
|
|
@@ -3312,6 +3349,10 @@ export class SharedLog<
|
|
|
3312
3349
|
clearTimeout(timer);
|
|
3313
3350
|
timer = undefined;
|
|
3314
3351
|
}
|
|
3352
|
+
if (requestTimer != null) {
|
|
3353
|
+
clearTimeout(requestTimer);
|
|
3354
|
+
requestTimer = undefined;
|
|
3355
|
+
}
|
|
3315
3356
|
};
|
|
3316
3357
|
|
|
3317
3358
|
const resolve = () => {
|
|
@@ -3343,6 +3384,42 @@ export class SharedLog<
|
|
|
3343
3384
|
);
|
|
3344
3385
|
}, timeoutMs);
|
|
3345
3386
|
|
|
3387
|
+
let requestAttempts = 0;
|
|
3388
|
+
const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
|
|
3389
|
+
const maxRequestAttempts =
|
|
3390
|
+
this.waitForReplicatorRequestMaxAttempts ??
|
|
3391
|
+
Math.max(
|
|
3392
|
+
WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
|
|
3393
|
+
Math.ceil(timeoutMs / requestIntervalMs),
|
|
3394
|
+
);
|
|
3395
|
+
|
|
3396
|
+
const requestReplicationInfo = () => {
|
|
3397
|
+
if (settled || this.closed) {
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
if (requestAttempts >= maxRequestAttempts) {
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
requestAttempts++;
|
|
3406
|
+
|
|
3407
|
+
this.rpc
|
|
3408
|
+
.send(new RequestReplicationInfoMessage(), {
|
|
3409
|
+
mode: new SeekDelivery({ redundancy: 1, to: [key] }),
|
|
3410
|
+
})
|
|
3411
|
+
.catch((e) => {
|
|
3412
|
+
// Best-effort: missing peers / unopened RPC should not fail the wait logic.
|
|
3413
|
+
if (isNotStartedError(e as Error)) {
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
|
|
3418
|
+
if (requestAttempts < maxRequestAttempts) {
|
|
3419
|
+
requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
3422
|
+
|
|
3346
3423
|
const check = async () => {
|
|
3347
3424
|
const iterator = this.replicationIndex?.iterate(
|
|
3348
3425
|
{ query: new StringMatch({ key: "hash", value: key.hashcode() }) },
|
|
@@ -3367,6 +3444,7 @@ export class SharedLog<
|
|
|
3367
3444
|
}
|
|
3368
3445
|
};
|
|
3369
3446
|
|
|
3447
|
+
requestReplicationInfo();
|
|
3370
3448
|
check();
|
|
3371
3449
|
this.events.addEventListener("replicator:mature", check);
|
|
3372
3450
|
this.events.addEventListener("replication:change", check);
|
|
@@ -3616,27 +3694,54 @@ export class SharedLog<
|
|
|
3616
3694
|
return 0;
|
|
3617
3695
|
}
|
|
3618
3696
|
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
?.length ?? 1);
|
|
3624
|
-
const diffToOldest =
|
|
3625
|
-
subscribers > 1 ? now - this.oldestOpenTime - 1 : Number.MAX_SAFE_INTEGER;
|
|
3626
|
-
|
|
3627
|
-
const result = Math.min(
|
|
3628
|
-
this.timeUntilRoleMaturity,
|
|
3629
|
-
Math.max(diffToOldest, this.timeUntilRoleMaturity),
|
|
3630
|
-
Math.max(
|
|
3631
|
-
Math.round(
|
|
3632
|
-
(this.timeUntilRoleMaturity * Math.log(subscribers + 1)) / 3,
|
|
3633
|
-
),
|
|
3634
|
-
this.timeUntilRoleMaturity,
|
|
3635
|
-
),
|
|
3636
|
-
); // / 3 so that if 2 replicators and timeUntilRoleMaturity = 1e4 the result will be 1
|
|
3697
|
+
// Explicitly disable maturity gating (used by many tests).
|
|
3698
|
+
if (this.timeUntilRoleMaturity <= 0) {
|
|
3699
|
+
return 0;
|
|
3700
|
+
}
|
|
3637
3701
|
|
|
3638
|
-
|
|
3639
|
-
|
|
3702
|
+
// If we're alone (or pubsub isn't ready), a fixed maturity time is sufficient.
|
|
3703
|
+
// When there are multiple replicators we want a stable threshold that doesn't
|
|
3704
|
+
// depend on "now" (otherwise it can drift and turn into a flake).
|
|
3705
|
+
let subscribers = 1;
|
|
3706
|
+
if (!this.rpc.closed) {
|
|
3707
|
+
try {
|
|
3708
|
+
subscribers =
|
|
3709
|
+
(await this.node.services.pubsub.getSubscribers(this.rpc.topic))
|
|
3710
|
+
?.length ?? 1;
|
|
3711
|
+
} catch {
|
|
3712
|
+
// Best-effort only; fall back to 1.
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
if (subscribers <= 1) {
|
|
3717
|
+
return this.timeUntilRoleMaturity;
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
// Use replication range timestamps to compute a stable "age gap" between the
|
|
3721
|
+
// newest and oldest known roles. This keeps the oldest role mature while
|
|
3722
|
+
// preventing newer roles from being treated as mature purely because time
|
|
3723
|
+
// passes between test steps / network events.
|
|
3724
|
+
let newestOpenTime = this.openTime;
|
|
3725
|
+
try {
|
|
3726
|
+
const newestIterator = await this.replicationIndex.iterate(
|
|
3727
|
+
{
|
|
3728
|
+
sort: [new Sort({ key: "timestamp", direction: "desc" })],
|
|
3729
|
+
},
|
|
3730
|
+
{ shape: { timestamp: true }, reference: true },
|
|
3731
|
+
);
|
|
3732
|
+
const newestTimestampFromDB = (await newestIterator.next(1))[0]?.value
|
|
3733
|
+
.timestamp;
|
|
3734
|
+
await newestIterator.close();
|
|
3735
|
+
if (newestTimestampFromDB != null) {
|
|
3736
|
+
newestOpenTime = Number(newestTimestampFromDB);
|
|
3737
|
+
}
|
|
3738
|
+
} catch {
|
|
3739
|
+
// Best-effort only; fall back to local open time.
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
const ageGapToOldest = newestOpenTime - this.oldestOpenTime;
|
|
3743
|
+
const roleAge = Math.max(this.timeUntilRoleMaturity, ageGapToOldest);
|
|
3744
|
+
return roleAge < 0 ? 0 : roleAge;
|
|
3640
3745
|
}
|
|
3641
3746
|
|
|
3642
3747
|
async findLeaders(
|
|
@@ -3715,13 +3820,37 @@ export class SharedLog<
|
|
|
3715
3820
|
},
|
|
3716
3821
|
): Promise<Map<string, { intersecting: boolean }>> {
|
|
3717
3822
|
const roleAge = options?.roleAge ?? (await this.getDefaultMinRoleAge()); // TODO -500 as is added so that i f someone else is just as new as us, then we treat them as mature as us. without -500 we might be slower syncing if two nodes starts almost at the same time
|
|
3823
|
+
const selfHash = this.node.identity.publicKey.hashcode();
|
|
3824
|
+
|
|
3825
|
+
// Use `uniqueReplicators` (replicator cache) once we've reconciled it against the
|
|
3826
|
+
// persisted replication index. Until then, fall back to live pubsub subscribers
|
|
3827
|
+
// and avoid relying on `uniqueReplicators` being complete.
|
|
3828
|
+
let peerFilter: Set<string> | undefined = undefined;
|
|
3829
|
+
if (this._replicatorsReconciled && this.uniqueReplicators.size > 0) {
|
|
3830
|
+
peerFilter = this.uniqueReplicators.has(selfHash)
|
|
3831
|
+
? this.uniqueReplicators
|
|
3832
|
+
: new Set([...this.uniqueReplicators, selfHash]);
|
|
3833
|
+
} else {
|
|
3834
|
+
try {
|
|
3835
|
+
const subscribers =
|
|
3836
|
+
(await this.node.services.pubsub.getSubscribers(this.topic)) ??
|
|
3837
|
+
undefined;
|
|
3838
|
+
if (subscribers && subscribers.length > 0) {
|
|
3839
|
+
peerFilter = new Set(subscribers.map((key) => key.hashcode()));
|
|
3840
|
+
peerFilter.add(selfHash);
|
|
3841
|
+
}
|
|
3842
|
+
} catch {
|
|
3843
|
+
// Best-effort only; if pubsub isn't ready, do a full scan.
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3718
3846
|
return getSamples<R>(
|
|
3719
3847
|
cursors,
|
|
3720
3848
|
this.replicationIndex,
|
|
3721
3849
|
roleAge,
|
|
3722
3850
|
this.indexableDomain.numbers,
|
|
3723
3851
|
{
|
|
3724
|
-
|
|
3852
|
+
peerFilter,
|
|
3853
|
+
uniqueReplicators: peerFilter,
|
|
3725
3854
|
},
|
|
3726
3855
|
);
|
|
3727
3856
|
}
|
|
@@ -3815,6 +3944,15 @@ export class SharedLog<
|
|
|
3815
3944
|
.catch((e) => logger.error(e.toString()));
|
|
3816
3945
|
}
|
|
3817
3946
|
}
|
|
3947
|
+
|
|
3948
|
+
// Request the remote peer's replication info. This makes joins resilient to
|
|
3949
|
+
// timing-sensitive delivery/order issues where we may miss their initial
|
|
3950
|
+
// replication announcement.
|
|
3951
|
+
this.rpc
|
|
3952
|
+
.send(new RequestReplicationInfoMessage(), {
|
|
3953
|
+
mode: new SeekDelivery({ redundancy: 1, to: [publicKey] }),
|
|
3954
|
+
})
|
|
3955
|
+
.catch((e) => logger.error(e.toString()));
|
|
3818
3956
|
} else {
|
|
3819
3957
|
await this.removeReplicator(publicKey);
|
|
3820
3958
|
}
|
|
@@ -3974,11 +4112,28 @@ export class SharedLog<
|
|
|
3974
4112
|
|
|
3975
4113
|
let cursor: NumberFromType<R>[] | undefined = undefined;
|
|
3976
4114
|
|
|
3977
|
-
|
|
4115
|
+
// Checked prune requests can legitimately take longer than a fixed 10s:
|
|
4116
|
+
// - The remote may not have the entry yet and will wait up to `_respondToIHaveTimeout`
|
|
4117
|
+
// - Leadership/replicator information may take up to `waitForReplicatorTimeout` to settle
|
|
4118
|
+
// If we time out too early we can end up with permanently prunable heads that never
|
|
4119
|
+
// get retried (a common CI flake in "prune before join" tests).
|
|
4120
|
+
const checkedPruneTimeoutMs =
|
|
4121
|
+
options?.timeout ??
|
|
4122
|
+
Math.max(
|
|
4123
|
+
10_000,
|
|
4124
|
+
Number(this._respondToIHaveTimeout ?? 0) +
|
|
4125
|
+
this.waitForReplicatorTimeout +
|
|
4126
|
+
PRUNE_DEBOUNCE_INTERVAL * 2,
|
|
4127
|
+
);
|
|
4128
|
+
|
|
4129
|
+
const timeout = setTimeout(() => {
|
|
3978
4130
|
reject(
|
|
3979
|
-
new Error(
|
|
4131
|
+
new Error(
|
|
4132
|
+
`Timeout for checked pruning after ${checkedPruneTimeoutMs}ms (closed=${this.closed})`,
|
|
4133
|
+
),
|
|
3980
4134
|
);
|
|
3981
|
-
},
|
|
4135
|
+
}, checkedPruneTimeoutMs);
|
|
4136
|
+
timeout.unref?.();
|
|
3982
4137
|
|
|
3983
4138
|
this._pendingDeletes.set(entry.hash, {
|
|
3984
4139
|
promise: deferredPromise,
|