Skip to content

fix(replication): drain priority sync queue and recover from routing-event lag#165

Open
mickvandijke wants to merge 9 commits into
mainfrom
fix/neighbor-sync-drain-window
Open

fix(replication): drain priority sync queue and recover from routing-event lag#165
mickvandijke wants to merge 9 commits into
mainfrom
fix/neighbor-sync-drain-window

Conversation

@mickvandijke

@mickvandijke mickvandijke commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR hardens the replication repair path under churn and load. It expands the original neighbor-sync fix into a full set of paid-list repair, verification retry, queue-capacity, and audit-challenge fixes so legitimate repair work is not silently dropped or converted into false audit failures.

The branch contains six commits:

  • 12e8c42 - fix(replication): harden paid-list verification repair
  • 42d3242 - fix(replication): tolerate paid-list edge churn
  • e2967a6 - fix(replication): drain priority sync queue and recover from routing-event lag
  • ad5f9ce - fix(replication): preserve verification retry capacity
  • 47f67ae - fix(replication): eliminate false audit challenge timeouts
  • fc8d724 - fix(replication): preserve dequeued retry reservations

Problem

Several replication paths could lose valid work during the exact conditions where repair pressure is highest:

  1. Paid-list repair could become terminal too early. Duplicate replica and paid hints were deduplicated before knowing whether the replica path was actually admitted, verified local paid keys with no currently responding holders could be removed instead of retried, and exhausted verified fetches could lose the verification context needed for a later retry.
  2. Verification batches were too coarse. A large request could exceed useful responder work limits, failed responses could blur which keys were actually in the failed batch, and repeated keys could redo local storage or paid-list lookups unnecessarily.
  3. Paid-list edge peers were too strict under churn. Boundary peers in the paid close group can legitimately disagree during local routing-table churn. Counting negative edge votes into the same denominator as stable core votes could reject otherwise valid paid-list majorities.
  4. Neighbor sync could stall after topology bursts. sync_trigger is a coalescing Notify, so many topology-change wakeups can collapse into one. The loop previously ran one round and then parked on the 10-20 minute periodic tick, leaving queued priority peers waiting for later ticks.
  5. Routing event lag could hide entrants. Dropped KClosestPeersChanged broadcast events meant close-group entrants were never inserted into the priority neighbor-sync queue.
  6. Retry capacity could be stolen. A verified key promoted out of pending_verify into fetch no longer counted against pending capacity, so unrelated fresh hints could consume the sender/global slots required to requeue that verified key after fetch-source exhaustion.
  7. Audit challenge bursts could look like honest-peer timeouts. Multiple local audit issuers could concurrently send AuditChallenges to the same target and exceed the responder admission cap. Those local bursts were then observed as remote non-responses.

Changes

Paid-list verification and repair

  • Allows a duplicate paid hint to run when the duplicate replica hint was rejected, while still letting an admitted replica hint take precedence.
  • Adds next_verify_at to pending verification entries and uses ready_pending_keys / defer_pending so inconclusive or no-holder states wait for a retry delay instead of spinning or being dropped.
  • Caps verification request batches at MAX_VERIFICATION_KEYS_PER_REQUEST and splits outgoing per-peer requests with paid-list indices rebased per batch.
  • Caches responder-side storage and paid-list lookups per key inside a verification request.
  • Treats oversized or malformed paid-list index batches defensively, and scopes unresolved evidence to the keys that were actually in the failed request batch.
  • Keeps locally paid replica-repair keys alive when no holder responds yet; they are deferred and retried instead of becoming terminal data-loss cases.
  • Includes the replica hint sender as a fetch source for replica repair when appropriate, so a valid hint can repair even if quorum evidence does not surface another holder in that round.
  • Lets verified paid-only hints repair a missing local replica when this node is inside the storage-admission group.
  • Updates bootstrap drain accounting when stale pending-verification entries are evicted, so bootstrap state does not remain stuck on work that has aged out.

Paid-list edge churn handling

  • Adds PAID_LIST_FLEX_EDGE_COUNT = 4 for the furthest paid-list close-group positions.
  • Tracks per-key paid-list edge targets in VerificationTargets.
  • For full-width paid groups, negative or missing edge votes do not enlarge the paid-list denominator, while positive edge votes still count and expand the denominator.
  • Keeps undersized paid groups on the normal strict majority rule.
  • Preserves inconclusive outcomes when unresolved core or edge votes could still make the paid-list majority possible.
  • Bumps ant-node from 0.14.2 to 0.14.3.

Neighbor-sync burst and lag recovery

  • Drains NeighborSyncState priority peers back-to-back and only parks when priority_order is empty.
  • Adds NeighborSyncState::has_priority_peers as the loop's durable pending-work signal.
  • Handles RecvError::Lagged from the DHT event broadcast by snapshotting the current close-neighbor set, queueing those peers for priority sync, and triggering the loop.
  • Keeps RecvError::Closed behavior unchanged.

Verification retry capacity

  • Carries VerificationEntry retry metadata from pending_verify into FetchCandidate and then InFlightEntry.
  • Reserves pending-verification capacity while a retryable verified key is in fetch_queue or in_flight_fetch.
  • Counts those reservations against both global pending capacity and per-sender fairness capacity.
  • Releases the reservation on successful completion, terminal discard, duplicate replacement, or restoration to pending_verify.
  • Requeues exhausted verified fetches after verification_request_timeout instead of losing the repair work.

Dequeued retry reservations

  • Introduces by-value dequeued-candidate APIs so a candidate carrying retry metadata must be consumed explicitly.
  • Uses start_dequeued_fetch to transfer a dequeued retry reservation into in_flight_fetch.
  • Uses discard_fetch_candidate to release a reservation when a dequeued candidate is intentionally dropped.
  • Adds requeue_candidate_for_verification for dequeued candidates that have no usable sources; those are restored to pending verification with a retry delay instead of being dropped.
  • Centralizes pending insertion bookkeeping so pending_verify and per-sender counters stay in lockstep.

Audit challenge timeout hardening

  • Adds AuditChallengeCoordinator, a shared per-target outbound limiter for local audit issuers.
  • Limits this auditor to two concurrent AuditChallenges per target peer, matching the deployed responder per-source digest admission cap.
  • Makes excess local challenges wait before their response deadline starts, instead of sending bursts that the target drops and then recording false timeouts.
  • Shares the coordinator across responsible-chunk audits, prune-confirmation audits, and possession checks.
  • Adds node-local audit metrics and structured labels for responsible/prune/possession no-response classes.
  • Splits response-deadline timeouts from unreachable/send failures for local observability while preserving wire-compatible audit evidence semantics.
  • Records responder-side audit admission drops and digest dispatch latency.
  • Raises the per-source digest responder allowance separately from subtree/byte audit paths, leaving multi-MiB subtree/byte limits unchanged.

Commit Details

12e8c42 - harden paid-list verification repair

This commit makes the paid-list verification pipeline retry-safe. It fixes cross-set hint admission so a paid hint can still authorize metadata convergence when the replica side was rejected by local routing churn. It adds verification retry timing, request batch limits, response scoping, lookup caching, and non-terminal retry behavior for verified keys that currently have no responding holder. It also carries verification context into fetch so exhausted verified repairs can return to pending verification.

42d3242 - tolerate paid-list edge churn

This commit makes paid-list majority evaluation edge-aware. The furthest four paid-list peers in a full 20-peer group are queried, but their negative or missing responses are treated as churny boundary disagreement. Positive edge responses still count. This lets stable core paid-list majorities authorize repair without letting negative boundary votes create false failures.

e2967a6 - drain priority sync queue and recover from routing-event lag

This commit fixes the original neighbor-sync stall. Priority peers are now drained at round-trip speed instead of one batch per Notify wakeup, and dropped DHT broadcast events trigger a close-neighbor resnapshot so missed entrants still get priority sync.

ad5f9ce - preserve verification retry capacity

This commit makes retryable verified fetches reserve the pending capacity they need to come back. A sender at capacity can no longer have a promoted verified key blocked from requeueing by newer hints from the same sender. The reservation follows the key through fetch queue and in-flight fetch, then releases on completion or requeue.

47f67ae - eliminate false audit challenge timeouts

This commit prevents local audit bursts from creating false remote timeout verdicts. It adds the shared audit challenge coordinator, wires it through responsible audits, prune audits, and possession checks, and adds local metrics/classification so timeout-like outcomes can be distinguished from unreachable transport failures in logs and counters.

fc8d724 - preserve dequeued retry reservations

This commit closes the remaining reservation hole after dequeue. Fetch workers now consume candidates by value so retry metadata cannot be orphaned. No-source retry candidates are restored to pending verification with a delay, and pending insertion accounting is centralized to keep per-sender counters correct.

Coverage Added Or Updated

  • Unit coverage for paid-hint admission after duplicate replica rejection.
  • Verification request batching, paid-list index rebasing, and failed-batch scoping coverage.
  • Paid-list edge-vote tests for negative edge shrinkage, positive edge denominator expansion, self-inclusive groups, and unresolved edge/core possibilities.
  • Neighbor-sync priority queue drain/termination regression coverage.
  • Retry-capacity reservation coverage across promotion, in-flight completion, exhaustion, discard, dequeue, no-source requeue, and per-sender cap behavior.
  • Audit coordinator coverage for per-target serialization and cross-peer parallelism.
  • E2E coverage for paid-list majority repair below storage quorum: scenario_26_paid_list_majority_repairs_missing_replica_below_storage_quorum.
  • Existing prune, fetch-retry, and replication e2e tests updated for the shared audit coordinator and dequeued-candidate APIs.

Expected Effect

  • Partition heals and mass joins drain neighbor-sync priority work in seconds-scale back-to-back rounds rather than waiting for multiple 10-20 minute periodic ticks.
  • Paid-list-authorized repair remains live through transient no-holder/no-source rounds, queue pressure, and fetch-source exhaustion.
  • Paid-list majority decisions tolerate close-group edge disagreement without weakening core majority requirements.
  • Local audit concurrency no longer turns honest responder admission limits into false timeout verdicts.

SemVer

Patch.

Copilot AI review requested due to automatic review settings July 1, 2026 16:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves replication convergence under churn by (a) ensuring neighbor sync drains queued priority peers without waiting for periodic ticks and (b) recovering from lagged DHT routing-table broadcast events. It also expands verification/fetch pipeline behavior (batching caps, retry/defer semantics, paid-list edge-voter quorum handling) and adds an e2e scenario covering paid-list-authorized repair below storage quorum.

Changes:

  • Neighbor-sync loop now drains the priority queue back-to-back and resyncs close peers on RecvError::Lagged.
  • Verification/fetch pipeline enhancements: bounded verification batches, fetch→verification retry metadata, and deferred re-verification scheduling.
  • Paid-list quorum evaluation updated with “edge voter” handling; adds a deterministic paid-list repair e2e scenario and bumps crate version.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/poc_d1_bounded_queues.rs Updates test VerificationEntry construction for new timing fields.
tests/poc_bootstrap_stall.rs Updates test VerificationEntry construction for new timing fields.
tests/e2e/replication.rs Adds deterministic e2e scenario for paid-list-majority repair under low storage quorum.
src/replication/types.rs Adds next_verify_at, fetch retry metadata, and NeighborSyncState::has_priority_peers + test.
src/replication/scheduling.rs Adds deferred pending scheduling, fetch retry→verification requeue, and returns evicted keys.
src/replication/quorum.rs Adds edge-aware paid-list vote summary and splits verification requests into capped batches.
src/replication/mod.rs Implements lag recovery, neighbor-sync drain-before-park, verification request caps, and retry/defer integration.
src/replication/config.rs Introduces PAID_LIST_FLEX_EDGE_COUNT and MAX_VERIFICATION_KEYS_PER_REQUEST.
src/replication/bootstrap.rs Updates test VerificationEntry construction for new timing fields.
src/replication/admission.rs Adjusts cross-set dedup/admission so paid hints can survive replica rejection under churn.
Cargo.toml Bumps version to 0.14.3.
Cargo.lock Updates locked crate version to 0.14.3.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/replication/mod.rs
Comment on lines 2543 to 2547
results.push(protocol::KeyVerificationResult {
key: *key,
present,
present: cached.present.unwrap_or(false),
paid,
});
Comment thread src/replication/mod.rs
}
continue;
}
Err(RecvError::Closed) => continue,
Comment thread src/replication/quorum.rs
Comment on lines +495 to +497
let handles =
spawn_verification_batch_tasks(targets, p2p_node, config.verification_request_timeout);
collect_verification_batch_results(handles, targets, &mut evidence).await;
Comment thread src/replication/config.rs
Comment on lines +43 to +47
/// Number of furthest paid-list close-group peers treated as churny edge
/// voters.
///
/// Once the paid-list close group reaches [`PAID_LIST_CLOSE_GROUP_SIZE`], edge
/// peers are queried, but a negative edge paid-list response does not count
mickvandijke and others added 6 commits July 2, 2026 12:10
…event lag

Neighbor-sync paid-list hint propagation could stall for tens of minutes on
the furthest close-group members during correlated topology change (partition
heal, mass join). Two root causes:

- The neighbor-sync loop parked on the periodic 10-20 min tick after every
  single round, and `sync_trigger` is a coalescing `Notify` that collapses a
  burst of entrant wakeups into one. A churn burst that queued many priority
  peers therefore drained only one batch (<=4) promptly; the rest waited for
  subsequent ticks. Park only when `priority_order` is empty and otherwise run
  rounds back-to-back, draining the durable queue at round-trip speed. The
  drain terminates because `select_next_sync_peer` pops each priority peer
  unconditionally and `new_cycle` only refills under `is_cycle_complete()`.

- The DHT event handler discarded broadcast `RecvError::Lagged`, silently
  dropping `KClosestPeersChanged` events under load -- exactly when churn is
  heaviest -- so missed entrants were never queued. On lag, resynchronize from
  ground truth: snapshot the current close-peer set, queue every member for
  priority sync, and fire the trigger.

Add `NeighborSyncState::has_priority_peers` and a regression test covering the
loop's drain/termination contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SemVer: patch

Consume dequeued fetch candidates through reservation-aware queue APIs and requeue no-source retry candidates for verification.

Centralize pending_verify insertion bookkeeping so per-sender counters stay in lockstep.
@mickvandijke mickvandijke force-pushed the fix/neighbor-sync-drain-window branch from 1a6fe63 to fc8d724 Compare July 2, 2026 12:59
Copilot AI review requested due to automatic review settings July 2, 2026 12:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 3 comments.

Comment on lines +1284 to +1288
warn!(
"Prune audit challenge for {} against {peer} could not acquire coordinator slot",
hex::encode(key)
);
return PruneAuditChallengeResult::MalformedResponse;
Comment thread src/replication/mod.rs
Comment on lines +2837 to 2841
if cached.present.is_none() && paid.is_none() {
continue;
}

results.push(protocol::KeyVerificationResult {
Comment thread src/replication/mod.rs
Comment on lines 18 to 21
pub mod audit;
pub mod audit_coordinator;
pub(crate) mod audit_metrics;
pub mod bootstrap;
Copilot AI review requested due to automatic review settings July 2, 2026 15:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Comment thread src/replication/mod.rs
Comment on lines +132 to +134
fn fresh_offer_key_lock_index(key: &XorName) -> usize {
usize::from(key[0]) % FRESH_OFFER_KEY_LOCK_SHARDS
}
Comment on lines +1283 to +1289
let Some(_slot) = audit_challenge_coordinator.acquire(*peer).await else {
warn!(
"Prune audit challenge for {} against {peer} could not acquire coordinator slot",
hex::encode(key)
);
return PruneAuditChallengeResult::MalformedResponse;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants