fix(replication): cap first-audit pending admission at the launch budget#183
fix(replication): cap first-audit pending admission at the launch budget#183grumbach wants to merge 21 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR tightens ADR-0004’s monetized first-audit pipeline so the pending admission queue is sized to what the launch budget can realistically drain within the answerability window, preventing an ever-aging backlog under sustained nomination load. It also expands observability (audit outcome/drops counters, scheduler summary fields) and updates payment-verifier nomination behavior and ADR documentation.
Changes:
- Cap first-audit
pendingadmission atFIRST_AUDIT_PENDING_CAPderived from the launch budget + effective answerability window, with LRU displacement counted ascapacity_evicted. - Add cumulative audit outcome/drop accounting and emit it alongside the existing replication traffic summary.
- Update payment verification to nominate only the actually-paid candidate(s), add settlement payee-prefix checking, and add an opt-in A/B e2e workload driver plus ADR amendments.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/mod.rs | Registers the new opt-in A/B workload module. |
| tests/e2e/first_audit_ab.rs | Adds an environment-gated A/B workload driver for production-shaped paid uploads + first-audit behavior comparison. |
| src/replication/protocol.rs | Adds process-global audit pass/fail-by-reason and responder-drop counters + summary log. |
| src/replication/mod.rs | Implements the capped pending scheduler + reservation/promotion flow, adds new scheduler observability, and wires audit outcome/drop recording. |
| src/replication/config.rs | Documents and centralizes first-audit limiter constants (launch interval, burst, inflight, jitter, ingress capacity, etc.). |
| src/payment/verifier.rs | Narrows deterministic first-audit nomination to paid candidates, adds settlement redirect rejection, and updates test hooks accordingly. |
| docs/adr/ADR-0004-commitment-bound-quote-pricing.md | Adds ADR-0004 Amendments 2–4, including the pending admission sizing rationale and semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let Some(recorded) = recorded_rewards_prefix { | ||
| let expected = Self::rewards_address_prefix(&candidate.quote.rewards_address); | ||
| if recorded != expected { | ||
| return Err(Error::Payment(format!( | ||
| "Median-priced quote settlement for peer {:?} was redirected: recorded rewards address prefix {} does not match the quote's {}", | ||
| candidate.encoded_peer_id, | ||
| hex::encode(recorded), | ||
| hex::encode(expected) | ||
| ))); | ||
| } | ||
| } |
…mendment 2) Production v0.14.3 showed the deterministic first-audit path amplifying fleet-wide: launches scaled as uploads x pinned-quotes-per-proof x verifying-storers, with hourly pin rotation defeating dedup and no aggregate launch bound. The overflow tripped responder admission caps and was recorded as peer Timeouts (545k/24h), degrading downloads ~31%. Payments now NOMINATE pins; the clock LAUNCHES them: - verifier: nominate only the settlement-verified paid candidate (single-node median issuer; merkle paid_node_addresses indices), not every quote in the bundle / candidate in the pool - drainer: launch limiter with a token bucket (burst 2, one launch per 5 min), an in-flight cap (2), and 0-30s launch jitter so one chunk's storers do not challenge the paid peer simultaneously - per-peer re-audit window (2h) that survives pin rotation, with a >1.5x committed-count jump override so inflated sidecar-only pins are still re-nominated immediately - budget deferrals are penalty-free and consume nothing; newest-monetized pins get budget first - scheduler summary gains rate_deferred_attempts / window_deduped / tokens; outcome labels stay append-only vs WithAutonomi#173 - ADR-0004 Amendment 2 documents first audits as bounded best-effort sampling and restates the coverage guarantee - tests/e2e/first_audit_ab.rs: env-gated production-shaped A/B workload driver (real Anvil settlement, quote sidecars, median payment, close-group PUTs, periodic rotation) for baseline-vs-fix comparisons
…limiter accounting Review-round fixes for the first-audit scheduler change: - Settlement-redirect rejection: completedPayments stores whatever (rewardsAddress, amount) the payer supplied for a quote hash, so the amount-only check accepted a payment the client redirected to its own wallet while the median issuer was treated (and first-audited) as paid. The verifier now compares the vault's bytes16 payee prefix against the quote's rewards address; validated against the real contract via the Anvil workload (honest pay_for_quotes settlements still verify). - Drop-guarded in-flight slot: a panicking or cancelled first-audit task can no longer leak an in-flight slot and wedge the cap shut. - Correct the launch-rate wording (sustained 12/h plus at most the burst of 2 in the first hour) in config docs and ADR-0004 Amendment 2. - A/B workload driver only runs when FIRST_AUDIT_AB=1 exactly.
…fter jitter Final review-round hardenings against the production-incident context: - The verifier-to-drainer monetized-pin channel is now bounded (FIRST_AUDIT_INGRESS_CAPACITY = 1024); producers try_send and drop on full. Every stage of the first-audit pipeline is now capacity-limited: ingress queue, pending LRU, and launch-rate token bucket. A dropped nomination is penalty-free and the peer stays covered by the lottery. - The A1 answerability screen runs again after the 0-30s launch jitter, so a pin can never be challenged outside its window regardless of how deferral time, jitter, and the skew margin compose. No false-conviction window remains by construction. - ADR-0004 Amendment 2 documents the accepted residuals: the budget-exhaustion starvation economics (bounded by real settled payment costs, lottery backstop) and the PRE-EXISTING vault-level completedPayments overwrite race (already defeats the amount check with a 1-wei payment on all node versions; needs a contract-side fix, tracked as follow-up).
Anti-starvation lane for the launch budget: passes alternate between newest-first (freshest answerability windows) and oldest-first, so a stream of fresh settled decoy nominations cannot keep an aging pin from launching before its eligibility window closes. Suppressing a pin now additionally requires pre-aged pending decoys at every observer, which the oldest lane itself drains and the per-peer re-audit window blocks from refreshing. Also restate the ADR-0004 Amendment 2 starvation residual with accurate economics (decoy settlements hit a whole storer cohort at once, merkle settlements nominate every paid index, principal recycles through sybil reward addresses, and sidecar-only pins have no lottery backstop) and correct over-claimed lottery-coverage comments: a dropped nomination is re-covered by the peer's next settled payment, the lottery covers only gossiped commitments.
… per pass The alternating newest/oldest lane flipped on every drainer pass, but most passes spend no token (the bucket is empty between refills) and a pass is triggered by incoming nominations as well as by the retry tick. An attacker could therefore inject a nomination to force a barren scan, flip lane parity, and keep every token-bearing pass on the newest lane — starving an aging pin with nothing but fresh decoy settlements, no pre-aged decoys required. The lane now advances only when a launch token is actually spent, so consecutive LAUNCHES strictly alternate whatever the scan pattern in between, and the oldest lane cannot be skipped. Adds a regression test modelling the barren-scan attack, and restates the Amendment 2 residual: the remaining suppression routes (pre-aged decoys at every observer, or evicting the target from the 4096-entry pending LRU) both cost real settled payments at scale.
… per pass The lane still advanced once per launching pass, so a full burst spent both tokens in the same lane: the launch sequence could be newest, newest, oldest instead of a strict alternation, and the anti-starvation argument did not hold for burst passes. The launch loop now pops candidates from a deque — front for the newest lane, back for the oldest — and flips the lane at every committed launch, so consecutive launches strictly alternate including within one burst pass. Deferred entries carry their snapshot index and are re-inserted oldest-first, restoring LRU recency regardless of which end they were popped from. The regression test now models the real loop (deque + multi-token passes) and rejects both wrong flip rules: per pass (attacker steers parity with barren scans) and once per launching pass (a burst keeps one lane). ADR-0004 Amendment 2 restated accordingly, and the residual no longer claims suppression cannot be indefinite: evicting a target from an observer's 4096-entry pending LRU permanently drops that nomination, after which coverage depends on the peer's next settled payment (or the ADR-0002 lottery for a gossiped commitment).
…d telemetry Prod-readiness review follow-ups (both minor/nit, no behavior change): - Count nominations dropped at the bounded ingress channel when it is full (Full only, not Closed) in a process-global counter, surfaced as ingress_dropped in the 5-minute scheduler summary. A non-zero value is the rollout signal that nomination ingress is saturating — benign but the thing to watch when widening the rollout. - Document that launched counts SCHEDULED audits (token spent); a pin that ages out at the post-jitter answerability re-screen is counted under outside_answerability_window and sends nothing, so actual wire challenges are launched minus post-jitter aborts, reconciled by the terminal counters.
…ppression Blocking review finding (dirvine): the drainer committed durable suppression (first_audited + the 2h per-peer window + token + launched) at LAUNCH time, then the child slept for jitter and re-checked answerability; a quote near the answerability boundary could obtain full 'already audited' suppression and drop a same-count successor for two hours despite no audit ever sending. Reworked to a drainer-owned reserve/promote/cancel scheduler (consensus design with codex xhigh over four rounds): - Payments NOMINATE; the drainer holds at most ONE reservation at a time (preserving per-launch lane alternation), consuming a token and an in-flight slot but stamping NO suppression. - A jitter timer (select arm) fires at the reservation's ready_at; the AUTHORITATIVE answerability + shared-cooldown check-and-stamp runs then, under the cooldown write lock, immediately before the send. Only on that pass are first_audited/recent stamped, the lane flipped, and launched counted. - A cancelled reservation (answerability lapsed during jitter, or a gossip audit won the cooldown) refunds the token, releases the slot, and leaves NO durable state — nothing to roll back. A cooldown-race requeues the event. - A same-peer successor arriving during a reservation is retained (window bypass for the reserved peer), never dropped, and becomes the next reservation if the first cancels. - B horizon prefilter: a nomination is only admitted if it stays answerable through now + max_jitter + slack, so a committed reservation can still be challenged at send; the authoritative check remains at promotion so A1 holds regardless of jitter-vs-window sizing. All scheduler state mutation stays single-threaded in the drainer; the spawned child only runs the audit I/O and holds the moved-in in-flight slot. Tests: horizon-prefilter boundary; answerability-cancel is state-neutral and retains a same-count successor (the reviewer's exact hole); per-launch lane alternation driven through reserve/resolve. 699 lib tests + cfd green; e2e smoke on a live anvil testnet exercises reserve->promote end to end.
…ination Codex review: resolving a reservation that loses the shared-cooldown race requeued the reserved event unconditionally, which could overwrite a newer same-peer successor (a count jump) already pending, losing coverage. Requeue only when no same-peer entry is pending; the successor arrived later so it is the newer nomination. Adds a regression test.
…granularity in test - pending_len/tokens are used only by the scheduler summary log and unit tests, both absent from cargo build --release --no-default-features, so they tripped dead_code under RUSTFLAGS=-D warnings. Gate the lint the same way the crate already gates logging-only unused items. - first_audit_horizon_prefilter_boundary used a 1ns epsilon; Windows SystemTime has 100ns (FILETIME) granularity, so the nanosecond step rounded to the same instant and the boundary assertion failed there. Use 1µs, exact on every platform.
…cannot erase an inflated one Re-review blocker (dirvine): per-peer pending coalescing replaced same-peer work by arrival order only, so a peer could suppress an inflated (audit-worthy) sidecar-only commitment for the cost of one cheaper same-peer settlement — the count-jump override only compares against the last AUDITED count and sidecar- only pins have no gossip-lottery backstop. Coalescing is now highest-count-per-peer (newest on an equal-count tie): a strictly-lower-count nomination never displaces a higher-count pending pin, and a suppressed lower nomination leaves the retained pin's LRU recency untouched (so low-count floods cannot steer the lane). The same rule governs the cooldown-race requeue of a reserved event (incoming_is_newer=false, so a higher-count reserved event beats a lower successor), which now also counts a capacity eviction in the funnel (previously unobserved). ADR-0004 Amendment 2 restated (highest-count coalescing + accepted residual: a single per-peer slot may drop the lower fallback if the retained higher pin later ages out). Non-blocking notes documented: single-node tied-median nominates only the first settled candidate (sidecar-only extras are best- effort residual; gossiped extras keep the lottery); the re-audit window is stamped at launch not outcome (Transient/silence is not auto-relaunched) — both to be quantified in the matched staging run. Tests: coalescing keeps highest count + exposes eviction; pending high vs later lower; reserved high vs cooldown-race lower successor. 702 lib tests + cfd green.
…d edge tests Review nits (both non-blocking): add a dedicated suppressed_lower funnel counter so a burst of attempted cheaper-pin self-erasure (a lower-count same-peer nomination dropped so a higher-count pin survives) is visible in the scheduler summary, on both the enqueue and cooldown-race requeue paths; add edge tests for (a) a suppressed-lower flood leaving the retained pin's LRU recency/lane position unchanged (peek, no push) and (b) the cooldown-race requeue counting a different-peer capacity eviction. ADR-0004 funnel list updated. 704 lib tests + cfd green.
…he funnel Codex review: SuppressedLower was returned for both a strictly-lower incoming (the attack signal) and an equal-count freshness tie, so the requeue counted a benign tie as an attempted suppression. Add a distinct RetainedOnTie outcome; only a strictly-lower drop increments suppressed_lower. Assert the counter through the enqueue path in the pending-lower test.
Adds oldest_pending_quote_age_ms to the first-audit scheduler summary so the pending-queue depth can be interpreted: a small, steady value means the queue is draining promptly; a value climbing toward the answerability window means pending work is expiring unaudited instead of launching. Gives the queue-age evidence the last testnet report asked for. Observability only: no scheduling or protocol behaviour change.
…riodic summary The per-variant traffic counters give audit volume but not results, so release comparisons cannot attribute CPU changes to audit work. Add cumulative pass/fail-per-reason counters for responsible and subtree audits plus responder-side admission-drop counters, emitted as a fourth summary line that telegraf lifts into per-field ES/Influx data.
Expired pending nominations were only collected during reserve scans, which the token-budget gate skips entirely under sustained load, so dead entries squatted the bounded per-peer LRU and a dead higher-count incumbent could permanently veto live lower-count nominations for its peer. Enqueue now drops a dead incumbent before coalescing and the summary tick sweeps expired entries regardless of tokens; both removals are accounted as outside_answerability_window, so the pending count and oldest-pending-age gauge report only launchable work.
The pre-rebase merge of main had resolved the tests-module import list to use the qualified path for AuditFailureSummary; replaying the linear commits resurrected the unused import.
cbc2f5e to
d1fc1dd
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
src/replication/protocol.rs:1105
- These assertions use exact deltas on process-global counters, which can fail if another test increments the same counter between the load and the assert. Use a >= check so the test remains correct under parallel execution.
let before = AUDIT_PASS[pass_slot].load(Ordering::Relaxed);
record_audit_pass(AuditOutcomeKind::Subtree);
assert_eq!(AUDIT_PASS[pass_slot].load(Ordering::Relaxed), before + 1);
src/replication/protocol.rs:1126
- Same flakiness risk as the pass assertion: these are global counters and other tests may increment them. Assert that the intended slot increased by at least 1 instead of exactly 1.
assert_eq!(
AUDIT_FAIL[slot].load(Ordering::Relaxed),
before + 1,
"kind {kind_index} reason {reason:?} must land in its own slot",
);
src/replication/protocol.rs:1140
- Same flakiness risk as above: AUDIT_DROPPED is process-global and may be incremented by other tests. Use a >= assertion to avoid nondeterministic failures in parallel runs.
assert_eq!(
AUDIT_DROPPED[kind_index].load(Ordering::Relaxed),
before + 1
);
| @@ -1673,6 +2295,7 @@ impl ReplicationEngine { | |||
| )) => { | |||
| protocol::log_traffic_summary(); | |||
| protocol::log_served_peers_summary(); | |||
| protocol::log_audit_outcome_summary(); | |||
| /// No other test mutates the audit outcome counters, so per-slot deltas are | ||
| /// stable even under parallel test execution. |
| let cutoff_secs = GOSSIP_ANSWERABILITY_TTL | ||
| .as_secs() | ||
| .saturating_sub(MONETIZED_AUDIT_SKEW_MARGIN.as_secs()); | ||
| // Compile-time division-by-zero if the launch interval is ever zeroed: | ||
| // a zero interval makes the budget (and this cap) meaningless. | ||
| let launchable = cutoff_secs / config::FIRST_AUDIT_LAUNCH_INTERVAL.as_secs(); | ||
| (launchable + config::FIRST_AUDIT_BUDGET_BURST as u64) as usize |
dirvine
left a comment
There was a problem hiding this comment.
Reviewed exact head d1fc1ddf0a4f710e16debd471cd3ec4c3916e92f with a six-seat panel (four substantive completions; two tool-failed), then independently checked the scheduler ordering and focused tests.
Verdict: changes requested. The queue-sizing diagnosis is sound, but the current keep-newest cap introduces a deterministic suppression route before the scheduler can reserve work.
Blocking — drain-before-reserve can evict a target deterministically
FIRST_AUDIT_PENDING_CAP is 32 (src/replication/mod.rs:956-984), while FIRST_AUDIT_DRAIN_BATCH is 64 (src/replication/config.rs:399-405). On an ingress wake the event loop enqueues/drains up to 64 nominations first (src/replication/mod.rs:1621-1640) and only then attempts reservation (:1736-1758). At capacity, every new distinct peer deterministically evicts the LRU peer (:199-227, :619-632).
Therefore an ordered batch of target + 32 later distinct-peer nominations removes the target before either newest/oldest lane can inspect it, even when a token and in-flight slot are available. Lane alternation cannot help because the target is already gone.
This cuts the stacked ADR's documented capacity-eviction threshold from 4,096 nominations (docs/adr/ADR-0004-commitment-bound-quote-pricing.md:462-470) to 32 — a 128x reduction — while Amendment 4 says every fresh pin retains an “unpredictable chance” of prompt audit (:527-533). The current replacement is deterministic under controlled arrival order. That matters because the ADR already assumes recyclable payment principal and sidecar-only pins without a lottery backstop (:443-453).
Please either give eligible work a reservation opportunity before destructive overflow, or use genuine adversary-resistant/random/weighted retention rather than deterministic keep-newest LRU. Add a production-order regression with pending_cap < ordered_batch <= drain_batch; the new enqueue-only test currently proves that the oldest target is evicted but does not exercise the drain/reserve boundary.
Also fix in this head
- Cap maths/wording: the strict predicate rejects
age >= 150m(src/replication/mod.rs:920-928). For a fresh quote, burst 2 plus refills at 5…145 minutes gives 31 usable launches, not 32; the max-jitter/slack prefilter is stricter again. The test at:6590-6604repeats the formula rather than simulating the predicate. Derive the cap from the same strict launch predicate/horizon. - Occupancy telemetry: a reservation is held outside
pending(:477-487,:721-728), so the scheduler may hold one reserved + 32 pending while reportingpending=32, pending_cap=32. Report reserved/total schedulable occupancy or narrow the claim. - Stack dependency: #183 must remain after #177. The merge-order comment names stale cap SHA
cbc2f5e; the current cap commit is this head,d1fc1dd.
Verification
cargo fmt --all -- --check— passcargo test first_audit_ --lib --no-fail-fast— 28/28 pass- GitHub CI — all current checks green across Linux/macOS/Windows, clippy, format, docs, no-logging, audit and ADR validation
- Head revalidated immediately before review
The causal note on DEV-02/DEV-03 is fair: that comparison cannot attribute the upload p90 change. I also agree another attribution run is not necessary for this queue finding; the scheduler counters already show the admission/budget mismatch. This review is about the proposed retention policy, not the diagnosis.
…udit cooldown audit_launch_decision stamped the shared audit_on_gossip_cooldown before consulting the lottery, so a losing ticket consumed the 30-minute window despite sending no challenge. The monetized first-audit scheduler reads the same map as recent-audit coverage, so a peer whose changed commitment kept losing the lottery near each reopening could hold a paid first audit pending until its answerability window expired, and sidecar-only pins have no gossip-lottery backstop. The lottery attempt window is now a gossip-private map (stamped on every roll, win or lose, preserving one-roll-per-window flood resistance) and the shared cooldown is stamped only when a real audit launches. Also documents the promotion-before-send stamped-but-unsent residual on resolve().
The pending queue shared the 4096-entry commitment-cache cap while the token bucket can launch at most ~32 audits inside one effective answerability window, so under production-scale arrival rates (staging 2026-07-17: ~113 admitted nominations per node-hour against ~12 launches) over 99% of admitted work could never launch. The queue became an aging backlog whose entries mostly expired, and the pending/oldest-age telemetry measured the backlog instead of schedulable work. pending is now capped at FIRST_AUDIT_PENDING_CAP, derived from the budget and window constants with compile-time guardrails. At capacity the LRU displaces the least-recently-refreshed entry, counted as capacity_evicted; keep-newest displacement preserves the newest-first lane's prompt-audit deterrence under flood, and displacement stamps no suppression state. The summary line reports pending_cap so occupancy is a dashboard quantity. ADR-0004 Amendment 4 documents the sampling coverage semantics.
…n for the pending cap Review findings on the admission cap: - Deterministic keep-newest LRU displacement let an ordered batch of cap distinct-peer nominations flush a chosen target inside one 64-event ingress drain, before either launch lane could see it, cutting the documented suppression cost from 4096 nominations to 32. Overflow now displaces a uniformly random incumbent (per-nomination eviction probability 1/cap, never certain), and the drainer grants pending work a reservation opportunity before any destructive overflow, so a burst can never flush eligible work past a ready token. - The cap derivation over-counted by one: the strict reserve predicate (answerable through max jitter plus send slack) permits 31 usable launches per window, not 32. The unit test now simulates the shipped predicate instant-by-instant instead of repeating the formula. - The reservation is held outside pending, so the summary now reports reserved alongside pending/pending_cap; schedulable occupancy is their sum. Regressions: ordered-flood non-determinism (100-trial statistical bounds with miss odds below 1e-20), reserve-before-eviction at capacity, and displaced-peer re-nomination under random retention.
d1fc1dd to
3462edf
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/payment/verifier.rs:1165
- The settlement-redirect check runs before verifying the on-chain amount is sufficient. For an unpaid quote hash, the vault mapping will usually return a zeroed rewards-address prefix, which will be treated as a “redirected settlement” and yields a misleading error.
Consider applying the redirect check only after on_chain_amount >= candidate.expected_amount so “not paid enough” remains the failure mode when no sufficient settlement exists.
if let Some(recorded) = recorded_rewards_prefix {
let expected = Self::rewards_address_prefix(&candidate.quote.rewards_address);
if recorded != expected {
return Err(Error::Payment(format!(
"Median-priced quote settlement for peer {:?} was redirected: recorded rewards address prefix {} does not match the quote's {}",
| pub(crate) fn record_audit_fail(kind: AuditOutcomeKind, reason: &AuditFailureReason) { | ||
| AUDIT_FAIL[kind as usize * N_FAIL_REASONS + fail_reason_index(reason)] | ||
| .fetch_add(1, Ordering::Relaxed); | ||
| } | ||
|
|
||
| /// Record a responder-side challenge dropped at the admission caps. | ||
| pub(crate) fn record_audit_drop(kind: AuditDropKind) { | ||
| AUDIT_DROPPED[kind as usize].fetch_add(1, Ordering::Relaxed); | ||
| } | ||
|
|
||
| /// Emit the cumulative audit outcome tallies as one INFO summary line, same | ||
| /// target and cadence as [`log_traffic_summary`] (`group = 4`), so the | ||
| /// telegraf→Elasticsearch pipeline lifts each key into a `tail.*` field and | ||
| /// pass/fail/drop rates per hour fall out of max-per-hour deltas. | ||
| pub(crate) fn log_audit_outcome_summary() { | ||
| let pass = |k: usize| AUDIT_PASS[k].load(Ordering::Relaxed); | ||
| let fail = |k: usize, r: usize| AUDIT_FAIL[k * N_FAIL_REASONS + r].load(Ordering::Relaxed); | ||
| let drop = |k: usize| AUDIT_DROPPED[k].load(Ordering::Relaxed); |
| /// ADR-0004: a sender the payment verifier uses to surface monetized pins | ||
| /// (commitments that backed a payment) for a deterministic first audit. | ||
| /// Cloneable; the engine drains the matching receiver. | ||
| /// (commitments that backed a payment) for a first audit. Cloneable; the | ||
| /// engine drains the matching receiver. Bounded: senders must `try_send` | ||
| /// and treat a full queue as a benign drop (Amendment 2 best-effort). | ||
| #[must_use] | ||
| pub fn monetized_pin_sender(&self) -> mpsc::UnboundedSender<MonetizedPinEvent> { | ||
| pub fn monetized_pin_sender(&self) -> mpsc::Sender<MonetizedPinEvent> { |
Summary
Follow-up to #177 (stacked on its head; see merge-order note below). Sizes first-audit pending admission to what the launch budget can actually audit.
#177 bounded launches (token bucket) and expired dead entries eagerly, but admission was still unbounded relative to the budget:
pendingshared the 4096-entry commitment-cache cap while the token bucket can launch at most ~31 audits inside one effective answerability window. On the 2026-07-17 DEV-02 staging run this meant ~113 admitted nominations per node-hour against ~12 launches: over 99% of admitted work could never launch, pending sat at ~331-431k fleet-wide, and the oldest-pending age tracked the age of the deployment itself — an aging backlog whose steady state is mass expiry.Changes
pendingis capped atFIRST_AUDIT_PENDING_CAP, derived from the strict reserve-time launch horizon (burst at age zero plus every refill that stays answerable through max jitter + send slack; currently 31). The unit test simulates the shipped predicate instant-by-instant and must agree with the derivation. The dedup set keeps the 4096 bound.capacity_evicted. Per-nomination eviction probability of a specific target is 1/cap regardless of arrival order: suppressing it with 95% confidence costs ~95 distinct-peer paid nominations and is never certain (deterministic keep-newest LRU, flagged in review, allowed a certain flush with 32).pending_capandreserved(the single reservation held outside the queue), so schedulable occupancypending + reservedis directly computable in Elasticsearch.Evidence
pending_cap < 60 <= FIRST_AUDIT_DRAIN_BATCH, 100-trial statistical bounds with miss odds < 1e-20), reserve-before-eviction at capacity, displaced-peer re-nomination, cap-vs-predicate simulation.pending≤ 31 witholdest_pending_quote_age_msfar below the answerability window (queue drains instead of aging), overflow visible ascapacity_evictedgrowth at admission time instead ofoutside_answerability_windowmass expiry hours later.Semver
Patch: no protocol or config-surface change; two additive fields in the summary log line.
Tracking: V2-738