@peerbit/shared-log 16.0.28 → 16.0.30

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/src/index.ts CHANGED
@@ -564,7 +564,7 @@ const emitAdvisorySyncProfileDuration = (
564
564
  try {
565
565
  emitSyncProfileDuration(profile, startedAt, event);
566
566
  } catch {
567
- // Diagnostics must not change open or provider-resolution correctness.
567
+ // Advisory diagnostics must not change replication or lifecycle behavior.
568
568
  }
569
569
  };
570
570
 
@@ -4033,7 +4033,13 @@ export class SharedLog<
4033
4033
  this._repairMetrics["join-warmup"].simpleFallbackPasses += 1;
4034
4034
  },
4035
4035
  sendEntriesSimple: (target, entries, options) =>
4036
- this.sendRepairEntriesWithTransport(target, entries, "simple", options),
4036
+ this.sendRepairEntriesWithTransport(
4037
+ target,
4038
+ entries,
4039
+ "simple",
4040
+ options,
4041
+ "join-warmup",
4042
+ ),
4037
4043
  logError: (error) => logger.error(error),
4038
4044
  });
4039
4045
  }
@@ -6524,6 +6530,24 @@ export class SharedLog<
6524
6530
  );
6525
6531
  };
6526
6532
  try {
6533
+ if (!isPeerRoundCurrent()) return;
6534
+ if (
6535
+ !this._v2Send.hasCurrentStateForPeer({
6536
+ peerHash: peer,
6537
+ peerSession: captured.peerSession,
6538
+ receiverTransportSession: captured.capabilitySession,
6539
+ })
6540
+ ) {
6541
+ // Preflight can precede replacement or loss of the sender
6542
+ // stream. Delivery must recover its own exact current
6543
+ // binding before waiting for application confirmation.
6544
+ this._v2Receive.reAdvertiseLocalCapabilityForRemoteFull({
6545
+ peerHash: peer,
6546
+ peerSession: captured.peerSession,
6547
+ receiveEpoch: captured.receiveEpoch,
6548
+ signal: roundSignal,
6549
+ });
6550
+ }
6527
6551
  await this._v2Send.confirmLatestForPeer(
6528
6552
  {
6529
6553
  peerHash: peer,
@@ -10621,6 +10645,7 @@ export class SharedLog<
10621
10645
  isStillCurrent?: () => boolean;
10622
10646
  signal?: AbortSignal;
10623
10647
  },
10648
+ mode?: RepairDispatchMode,
10624
10649
  ) {
10625
10650
  const isStillCurrent = options?.isStillCurrent ?? (() => true);
10626
10651
  if (!isStillCurrent()) {
@@ -10628,53 +10653,87 @@ export class SharedLog<
10628
10653
  }
10629
10654
  const unknownEntries = new Map<string, RepairDispatchEntry<R>>();
10630
10655
  const knownHashes: string[] = [];
10631
- for (const [hash, entry] of entries) {
10632
- if (
10633
- (options?.bypassRecentKnownPeers ||
10634
- !this.isEntryRecentlyKnownByPeer(
10635
- hash,
10636
- target,
10637
- RECENT_KNOWN_REPAIR_SUPPRESSION_MS,
10638
- )) &&
10639
- (options?.bypassKnownPeers || !this.isEntryKnownByPeer(hash, target))
10640
- ) {
10641
- unknownEntries.set(hash, entry);
10642
- } else {
10643
- knownHashes.push(hash);
10656
+ const profile = this._logProperties?.sync?.profile;
10657
+ const startedAt = syncProfileStart(profile);
10658
+ const inputEntries = profile ? entries.size : 0;
10659
+ let selectedEntries = 0;
10660
+ let lastObservedCurrent = true;
10661
+ let outcome = "stale";
10662
+ try {
10663
+ for (const [hash, entry] of entries) {
10664
+ if (
10665
+ (options?.bypassRecentKnownPeers ||
10666
+ !this.isEntryRecentlyKnownByPeer(
10667
+ hash,
10668
+ target,
10669
+ RECENT_KNOWN_REPAIR_SUPPRESSION_MS,
10670
+ )) &&
10671
+ (options?.bypassKnownPeers || !this.isEntryKnownByPeer(hash, target))
10672
+ ) {
10673
+ unknownEntries.set(hash, entry);
10674
+ } else {
10675
+ knownHashes.push(hash);
10676
+ }
10644
10677
  }
10645
- }
10646
- if (!isStillCurrent()) {
10647
- return;
10648
- }
10649
- this.clearRepairFrontierHashes(target, knownHashes);
10650
- if (unknownEntries.size === 0) {
10651
- return;
10652
- }
10653
- if (transport === "simple") {
10654
- // Fallback repair should not depend on the target completing the
10655
- // RequestMaybeSync -> ResponseMaybeSync round trip.
10656
- await this.pushRepairEntries(
10657
- target,
10658
- unknownEntries,
10659
- isStillCurrent,
10660
- options?.signal,
10661
- );
10662
- return;
10663
- }
10664
-
10665
- const syncEntries = this._logProperties?.sync?.priority
10666
- ? (this._coordinates.materializeRepairDispatchEntries(
10678
+ // A custom synchronizer may mutate the Map once it receives it.
10679
+ if (profile) selectedEntries = unknownEntries.size;
10680
+ if (!isStillCurrent()) return;
10681
+ this.clearRepairFrontierHashes(target, knownHashes);
10682
+ if (unknownEntries.size === 0) {
10683
+ outcome = "known-suppressed";
10684
+ return;
10685
+ }
10686
+ if (transport === "simple") {
10687
+ // Observe only checks the lower path already makes, without adding
10688
+ // lifecycle decisions or wrapping the disabled-profiling path.
10689
+ const dispatchIsStillCurrent = profile
10690
+ ? () => (lastObservedCurrent = isStillCurrent())
10691
+ : isStillCurrent;
10692
+ // Fallback repair does not wait for the maybe-sync round trip.
10693
+ await this.pushRepairEntries(
10694
+ target,
10667
10695
  unknownEntries,
10668
- ) as unknown as Map<string, SyncEntryCoordinates<R>>)
10669
- : (unknownEntries as Map<string, SyncEntryCoordinates<R>>);
10670
- if (!isStillCurrent()) {
10671
- return;
10696
+ dispatchIsStillCurrent,
10697
+ options?.signal,
10698
+ );
10699
+ } else {
10700
+ const syncEntries = this._logProperties?.sync?.priority
10701
+ ? (this._coordinates.materializeRepairDispatchEntries(
10702
+ unknownEntries,
10703
+ ) as unknown as Map<string, SyncEntryCoordinates<R>>)
10704
+ : (unknownEntries as Map<string, SyncEntryCoordinates<R>>);
10705
+ if (!isStillCurrent()) return;
10706
+ await this.syncronizer.onMaybeMissingEntries({
10707
+ entries: syncEntries,
10708
+ targets: [target],
10709
+ signal: options?.signal,
10710
+ });
10711
+ }
10712
+ outcome = !lastObservedCurrent
10713
+ ? "stale"
10714
+ : options?.signal?.aborted
10715
+ ? "cancelled"
10716
+ : "dispatched";
10717
+ } catch (error) {
10718
+ outcome = "error";
10719
+ throw error;
10720
+ } finally {
10721
+ if (profile) {
10722
+ emitAdvisorySyncProfileDuration(profile, startedAt, {
10723
+ name: "sharedLog.repair.dispatch",
10724
+ component: "shared-log",
10725
+ entries: inputEntries,
10726
+ count: selectedEntries,
10727
+ targets: 1,
10728
+ details: {
10729
+ mode,
10730
+ transport,
10731
+ outcome,
10732
+ knownSuppressedEntries: knownHashes.length,
10733
+ },
10734
+ });
10735
+ }
10672
10736
  }
10673
- await this.syncronizer.onMaybeMissingEntries({
10674
- entries: syncEntries,
10675
- targets: [target],
10676
- signal: options?.signal,
10677
- });
10678
10737
  }
10679
10738
 
10680
10739
  private async sendMaybeMissingEntriesNow(
@@ -10758,6 +10817,7 @@ export class SharedLog<
10758
10817
  this.isRepairLifecycleActive(repairLifecycleController),
10759
10818
  signal: repairLifecycleController.signal,
10760
10819
  },
10820
+ options.mode,
10761
10821
  ),
10762
10822
  ).catch((error: any) => logger.error(error));
10763
10823
  }
@@ -11148,6 +11208,7 @@ export class SharedLog<
11148
11208
  this.isRepairLifecycleActive(repairLifecycleController),
11149
11209
  signal: repairLifecycleController.signal,
11150
11210
  },
11211
+ options.mode,
11151
11212
  ),
11152
11213
  ).catch((error: any) => logger.error(error));
11153
11214
  };
@@ -11291,6 +11352,18 @@ export class SharedLog<
11291
11352
  repairLifecycleController: AbortController = this._instanceLifecycle
11292
11353
  ?.ownershipLifecycleController as AbortController,
11293
11354
  ) {
11355
+ const profile = this._logProperties?.sync?.profile;
11356
+ const startedAt = syncProfileStart(profile);
11357
+ const profileCounts = profile
11358
+ ? {
11359
+ passes: 0,
11360
+ inputEntries: 0,
11361
+ nativePasses: 0,
11362
+ repairCandidates: 0,
11363
+ repairBatches: 0,
11364
+ outcome: "stale",
11365
+ }
11366
+ : undefined;
11294
11367
  try {
11295
11368
  while (this.isRepairLifecycleActive(repairLifecycleController)) {
11296
11369
  if (!this.isRepairLifecycleActive(repairLifecycleController)) {
@@ -11329,8 +11402,10 @@ export class SharedLog<
11329
11402
  pruneStaleJoinWarmupPeers();
11330
11403
 
11331
11404
  if (pendingModes.size === 0) {
11405
+ if (profileCounts) profileCounts.outcome = "completed";
11332
11406
  return;
11333
11407
  }
11408
+ if (profileCounts) profileCounts.passes += 1;
11334
11409
 
11335
11410
  const optimisticGidPeersByMode = new Map<
11336
11411
  RepairDispatchMode,
@@ -11429,6 +11504,10 @@ export class SharedLog<
11429
11504
  }
11430
11505
  return;
11431
11506
  }
11507
+ if (profileCounts) {
11508
+ profileCounts.repairCandidates += entries.size;
11509
+ profileCounts.repairBatches += 1;
11510
+ }
11432
11511
  this.dispatchMaybeMissingEntries(
11433
11512
  target,
11434
11513
  entries,
@@ -11495,6 +11574,10 @@ export class SharedLog<
11495
11574
  residentEntriesByHash &&
11496
11575
  !this.hasCustomFindLeaders()
11497
11576
  ) {
11577
+ if (profileCounts) {
11578
+ profileCounts.nativePasses += 1;
11579
+ profileCounts.inputEntries += residentEntriesByHash.size;
11580
+ }
11498
11581
  const repairDispatchPlan = pruneStaleJoinWarmupPeers()
11499
11582
  ? await this.planResidentRepairDispatchBatch(
11500
11583
  {
@@ -11533,6 +11616,7 @@ export class SharedLog<
11533
11616
  const entries = await iterator.next(
11534
11617
  REPAIR_SWEEP_ENTRY_BATCH_SIZE,
11535
11618
  );
11619
+ if (profileCounts) profileCounts.inputEntries += entries.length;
11536
11620
  if (!this.isRepairLifecycleActive(repairLifecycleController)) {
11537
11621
  return;
11538
11622
  }
@@ -11657,6 +11741,7 @@ export class SharedLog<
11657
11741
  }
11658
11742
  }
11659
11743
  } catch (error: any) {
11744
+ if (profileCounts) profileCounts.outcome = "error";
11660
11745
  if (
11661
11746
  this.isRepairLifecycleActive(repairLifecycleController) &&
11662
11747
  !isNotStartedError(error)
@@ -11678,6 +11763,21 @@ export class SharedLog<
11678
11763
  void this.runRepairSweep(repairLifecycleController);
11679
11764
  }
11680
11765
  }
11766
+ if (profileCounts) {
11767
+ emitAdvisorySyncProfileDuration(profile, startedAt, {
11768
+ name: "sharedLog.placement.pass",
11769
+ component: "shared-log",
11770
+ entries: profileCounts.inputEntries,
11771
+ count: profileCounts.repairCandidates,
11772
+ details: {
11773
+ phase: "repair-sweep",
11774
+ outcome: profileCounts.outcome,
11775
+ passes: profileCounts.passes,
11776
+ nativePasses: profileCounts.nativePasses,
11777
+ repairBatches: profileCounts.repairBatches,
11778
+ },
11779
+ });
11780
+ }
11681
11781
  }
11682
11782
  }
11683
11783
 
@@ -21256,10 +21356,11 @@ export class SharedLog<
21256
21356
  msg.heads.map((head) => head.hash),
21257
21357
  );
21258
21358
  if (syncProfile) {
21259
- emitSyncProfileDuration(syncProfile, rawExistingStartedAt, {
21359
+ emitAdvisorySyncProfileDuration(syncProfile, rawExistingStartedAt, {
21260
21360
  name: "sharedLog.rawReceive.existingHeads",
21261
21361
  component: "shared-log",
21262
21362
  entries: msg.heads.length,
21363
+ count: rawExistingHashes.size,
21263
21364
  messages: 1,
21264
21365
  });
21265
21366
  }
@@ -21693,10 +21794,11 @@ export class SharedLog<
21693
21794
  ? undefined
21694
21795
  : await this.log.hasMany(headHashes);
21695
21796
  if (syncProfile) {
21696
- emitSyncProfileDuration(syncProfile, existingStartedAt, {
21797
+ emitAdvisorySyncProfileDuration(syncProfile, existingStartedAt, {
21697
21798
  name: "sharedLog.receive.existingHeads",
21698
21799
  component: "shared-log",
21699
21800
  entries: heads.length,
21801
+ count: existingHashes?.size,
21700
21802
  messages: 1,
21701
21803
  details: { rawMaterializedKnownMissing },
21702
21804
  });
@@ -24943,6 +25045,27 @@ export class SharedLog<
24943
25045
  recoveryTimer = undefined;
24944
25046
  if (!continueWait()) return;
24945
25047
  const recoveryTarget = confirmationRecoveryTarget;
25048
+ if (confirmationController && recoveryTarget) {
25049
+ if (
25050
+ this._peerSessions.current(peerHash) !==
25051
+ recoveryTarget.peerSession ||
25052
+ this._peerSessions.receiveEpoch(peerHash) !==
25053
+ recoveryTarget.receiveEpoch ||
25054
+ this._peerSyncCapabilitySessions.get(peerHash) !==
25055
+ recoveryTarget.capabilitySession ||
25056
+ !this.uniqueReplicators.has(peerHash)
25057
+ ) {
25058
+ // Events can be coalesced while application confirmation waits.
25059
+ // Release the obsolete target on the recovery tick as well,
25060
+ // including loss of replicator eligibility within one session.
25061
+ // A temporary receive reservation keeps this same target alive;
25062
+ // the final readiness inspection still checks every admission gate.
25063
+ confirmationRecoveryTarget = undefined;
25064
+ confirmationController.abort(
25065
+ new AbortError("Persisted-receipt readiness target changed"),
25066
+ );
25067
+ }
25068
+ }
24946
25069
  this.nudgePersistedReceiptPeerReadiness(
24947
25070
  key,
24948
25071
  operationSignal,
@@ -29439,6 +29562,17 @@ export class SharedLog<
29439
29562
  isOwnershipLifecycleCurrent() &&
29440
29563
  [...warmupPeers].every(isCurrentJoinWarmupTarget);
29441
29564
 
29565
+ const profile = this._logProperties?.sync?.profile;
29566
+ const profileStartedAt = syncProfileStart(profile);
29567
+ const profileCounts = profile
29568
+ ? {
29569
+ examinedEntries: 0,
29570
+ repairCandidates: 0,
29571
+ repairBatches: 0,
29572
+ pruneScan: false,
29573
+ outcome: "stale",
29574
+ }
29575
+ : undefined;
29442
29576
  try {
29443
29577
  const uncheckedDeliver: Map<
29444
29578
  string,
@@ -29462,6 +29596,10 @@ export class SharedLog<
29462
29596
  : isWarmupTarget
29463
29597
  ? "join-warmup"
29464
29598
  : "join-authoritative";
29599
+ if (profileCounts) {
29600
+ profileCounts.repairCandidates += entries.size;
29601
+ profileCounts.repairBatches += 1;
29602
+ }
29465
29603
  this.dispatchMaybeMissingEntries(
29466
29604
  target,
29467
29605
  entries,
@@ -29516,6 +29654,7 @@ export class SharedLog<
29516
29654
  forceFresh: forceFreshDelivery || useJoinWarmupFastPath,
29517
29655
  },
29518
29656
  )) {
29657
+ if (profileCounts) profileCounts.examinedEntries += 1;
29519
29658
  if (
29520
29659
  !isOwnershipLifecycleCurrent() ||
29521
29660
  (useJoinWarmupFastPath && !areJoinWarmupGenerationsCurrent())
@@ -29799,6 +29938,7 @@ export class SharedLog<
29799
29938
  ));
29800
29939
 
29801
29940
  if (shouldRunLocalPruneScan) {
29941
+ if (profileCounts) profileCounts.pruneScan = true;
29802
29942
  throwIfOwnershipLifecycleInactive();
29803
29943
  // Adaptive range changes and fixed zero-width updates can make already-indexed
29804
29944
  // local heads prunable even when the incremental rebalance scan misses them
@@ -29818,6 +29958,7 @@ export class SharedLog<
29818
29958
  }
29819
29959
  }
29820
29960
 
29961
+ if (profileCounts) profileCounts.outcome = "completed";
29821
29962
  return changed;
29822
29963
  } catch (error: any) {
29823
29964
  if (!isOwnershipLifecycleCurrent()) {
@@ -29827,8 +29968,27 @@ export class SharedLog<
29827
29968
  return false; // we are not started yet, so no changes
29828
29969
  }
29829
29970
 
29971
+ if (profileCounts) profileCounts.outcome = "error";
29830
29972
  logger.error(error.toString());
29831
29973
  throw error;
29974
+ } finally {
29975
+ if (profileCounts) {
29976
+ emitAdvisorySyncProfileDuration(profile, profileStartedAt, {
29977
+ name: "sharedLog.placement.pass",
29978
+ component: "shared-log",
29979
+ entries: profileCounts.examinedEntries,
29980
+ count: profileCounts.repairCandidates,
29981
+ details: {
29982
+ phase: "range-change",
29983
+ outcome: profileCounts.outcome,
29984
+ changes: changes.length,
29985
+ repairBatches: profileCounts.repairBatches,
29986
+ pruneScan: profileCounts.pruneScan,
29987
+ forceFreshDelivery,
29988
+ joinWarmupFastPath: useJoinWarmupFastPath,
29989
+ },
29990
+ });
29991
+ }
29832
29992
  }
29833
29993
  }
29834
29994
 
@@ -29887,6 +30047,15 @@ export class SharedLog<
29887
30047
  ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
29888
30048
  rebalanceParticipationDebounced = this.rebalanceParticipationDebounced,
29889
30049
  ) {
30050
+ const profile = this._isAdaptiveReplicating
30051
+ ? this._logProperties?.sync?.profile
30052
+ : undefined;
30053
+ const profileStartedAt = syncProfileStart(profile);
30054
+ const profileDetails:
30055
+ | Record<string, string | number | boolean | undefined>
30056
+ | undefined = profile
30057
+ ? { outcome: "stale", idleRemainingMs: 0 }
30058
+ : undefined;
29890
30059
  // Stage 3: the lifecycle owns all three identity terms. `lifecycle` may
29891
30060
  // go stale later; its deps late-bind to the host, so the debouncer term
29892
30061
  // still reads the current host field, and the role term can disagree
@@ -29926,6 +30095,14 @@ export class SharedLog<
29926
30095
 
29927
30096
  if (this._isAdaptiveReplicating) {
29928
30097
  if (this.shouldDelayAdaptiveRebalance()) {
30098
+ if (profileDetails) {
30099
+ profileDetails.outcome = "idle-deferred";
30100
+ profileDetails.idleRemainingMs = Math.max(
30101
+ 0,
30102
+ this.adaptiveRebalanceIdleMs -
30103
+ (Date.now() - this._lastLocalAppendAt),
30104
+ );
30105
+ }
29929
30106
  if (isCurrent()) {
29930
30107
  void rebalanceParticipationDebounced?.call();
29931
30108
  }
@@ -29935,11 +30112,17 @@ export class SharedLog<
29935
30112
  const peers = this.replicationIndex;
29936
30113
  const usedMemory = await this.getMemoryUsage();
29937
30114
  if (!isCurrent()) return false;
30115
+ if (profileDetails) {
30116
+ profileDetails.storageUsedBytes = usedMemory;
30117
+ profileDetails.storageObjectiveBytes =
30118
+ this.replicationController.maxMemoryLimit;
30119
+ }
29938
30120
  this.scheduleReplicationStatusRefreshForStorage(usedMemory);
29939
30121
  let dynamicRange = await this.getDynamicRange();
29940
30122
  if (!isCurrent()) return false;
29941
30123
 
29942
30124
  if (!dynamicRange) {
30125
+ if (profileDetails) profileDetails.outcome = "not-permitted";
29943
30126
  return; // not allowed to replicate
29944
30127
  }
29945
30128
 
@@ -29966,13 +30149,24 @@ export class SharedLog<
29966
30149
  const totalParticipation = await this.calculateTotalParticipation();
29967
30150
  if (!isCurrent()) return false;
29968
30151
 
30152
+ const cpuUsage = this.cpuUsage?.value();
30153
+ const stepStartedAt = syncProfileStart(profile);
29969
30154
  const newFactor = this.replicationController.step({
29970
30155
  memoryUsage: usedMemory,
29971
30156
  currentFactor: dynamicRange.widthNormalized,
29972
30157
  totalFactor: totalParticipation, // TODO use this._totalParticipation when flakiness is fixed
29973
30158
  peerCount: peersSize,
29974
- cpuUsage: this.cpuUsage?.value(),
30159
+ cpuUsage,
29975
30160
  });
30161
+ if (profileDetails) {
30162
+ profileDetails.preStepMs = stepStartedAt - profileStartedAt;
30163
+ profileDetails.stepMs = syncProfileStart(profile) - stepStartedAt;
30164
+ profileDetails.currentFactor = dynamicRange.widthNormalized;
30165
+ profileDetails.proposedFactor = newFactor;
30166
+ profileDetails.totalFactor = totalParticipation;
30167
+ profileDetails.controllerPeerCount = peersSize;
30168
+ profileDetails.cpuUsage = cpuUsage;
30169
+ }
29976
30170
 
29977
30171
  const absoluteDifference = Math.abs(
29978
30172
  dynamicRange.widthNormalized - newFactor,
@@ -30009,9 +30203,11 @@ export class SharedLog<
30009
30203
  (await this._isTrustedReplicator(this.node.identity.publicKey));
30010
30204
  if (!isCurrent()) return false;
30011
30205
  if (!canReplicate) {
30206
+ if (profileDetails) profileDetails.outcome = "not-permitted";
30012
30207
  return false;
30013
30208
  }
30014
30209
 
30210
+ const applyStartedAt = syncProfileStart(profile);
30015
30211
  await this.startAnnounceReplicating(
30016
30212
  [dynamicRange],
30017
30213
  {
@@ -30022,6 +30218,10 @@ export class SharedLog<
30022
30218
  ownershipLifecycleController,
30023
30219
  );
30024
30220
  if (!isCurrent()) return false;
30221
+ if (profileDetails) {
30222
+ profileDetails.outcome = "apply-settled";
30223
+ profileDetails.applyMs = syncProfileStart(profile) - applyStartedAt;
30224
+ }
30025
30225
 
30026
30226
  /* await this._updateRole(newRole, onRoleChange); */
30027
30227
  if (isCurrent()) {
@@ -30030,6 +30230,7 @@ export class SharedLog<
30030
30230
 
30031
30231
  return true;
30032
30232
  } else {
30233
+ if (profileDetails) profileDetails.outcome = "unchanged";
30033
30234
  if (isCurrent()) {
30034
30235
  void rebalanceParticipationDebounced?.call();
30035
30236
  }
@@ -30039,14 +30240,24 @@ export class SharedLog<
30039
30240
  return false;
30040
30241
  };
30041
30242
 
30042
- const resp = await fn().catch((error: any) => {
30043
- if (isNotStartedError(error) || isClosedStoreRace(error)) {
30044
- return false;
30243
+ try {
30244
+ return await fn().catch((error: any) => {
30245
+ if (isNotStartedError(error) || isClosedStoreRace(error)) {
30246
+ if (profileDetails) profileDetails.outcome = "stale";
30247
+ return false;
30248
+ }
30249
+ if (profileDetails) profileDetails.outcome = "error";
30250
+ throw error;
30251
+ });
30252
+ } finally {
30253
+ if (profileDetails) {
30254
+ emitAdvisorySyncProfileDuration(profile, profileStartedAt, {
30255
+ name: "sharedLog.adaptive.rebalance",
30256
+ component: "shared-log",
30257
+ details: profileDetails,
30258
+ });
30045
30259
  }
30046
- throw error;
30047
- });
30048
-
30049
- return resp;
30260
+ }
30050
30261
  }
30051
30262
 
30052
30263
  private getDynamicRangeOffset(): NumberFromType<R> {
@@ -27,6 +27,7 @@ const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
27
27
  const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
28
28
  const DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS = 2_000;
29
29
  const DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS = 5_000;
30
+ const MAX_REMOTE_FULL_REARM_COOLDOWN_MS = 30_000;
30
31
  const MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION = 2;
31
32
  const MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL = 64;
32
33
  const MAX_TIMER_MS = 2_147_483_647;
@@ -825,18 +826,28 @@ export class ReplicationInfoV2ReceiveCoordinator {
825
826
  ) {
826
827
  return true;
827
828
  }
829
+ // Repeated hints must leave time for a slow Full and its Applied response
830
+ // to settle. Reuse this exact generation's bounded attempt counter so a
831
+ // watchdog cannot continually replace the binding it is trying to confirm.
832
+ const nextAttemptAt =
833
+ now +
834
+ Math.min(
835
+ MAX_REMOTE_FULL_REARM_COOLDOWN_MS,
836
+ this.remoteFullRearmCooldownMs *
837
+ 2 ** Math.min(rearm?.attemptsStarted ?? 0, MAX_BACKOFF_EXPONENT),
838
+ );
828
839
  if (!rearm) {
829
840
  rearm = {
830
841
  peerHash: properties.peerHash,
831
842
  receiveEpoch: properties.receiveEpoch,
832
843
  receiverTransportSession,
833
- nextAttemptAt: now + this.remoteFullRearmCooldownMs,
844
+ nextAttemptAt,
834
845
  attemptsStarted: 0,
835
846
  outstandingAttempts: 0,
836
847
  };
837
848
  this._remoteFullRearmBySession.set(properties.peerSession, rearm);
838
849
  } else {
839
- rearm.nextAttemptAt = now + this.remoteFullRearmCooldownMs;
850
+ rearm.nextAttemptAt = nextAttemptAt;
840
851
  }
841
852
  const attemptState = rearm;
842
853
  const outstandingReservation = {};