apollo_mempool: drop emptied gap accounts on tx expiry to fix accounts_with_gap leak#14766
apollo_mempool: drop emptied gap accounts on tx expiry to fix accounts_with_gap leak#14766matanl-starkware wants to merge 1 commit into
Conversation
PR SummaryLow Risk Overview
Adds regression tests for one stuck tx and for two stuck txs expiring in one sweep (the latter documents the two-loop requirement). Reviewed by Cursor Bugbot for commit 9c2cd20. Bugbot is set up for automated code reviews on this repo. Configure here. |
e79fa29 to
d466d93
Compare
…s_with_gap leak remove_expired_txs decremented n_stuck_txs for expired stuck txs but only fed the queued txs into update_accounts_with_gap. Stuck txs are never queued (fee mode enqueues only nonce == account_nonce), so a gap account whose stuck txs all expire was never removed from accounts_with_gap, leaving mempool_accounts_with_gap > 0 while mempool_stuck_txs == 0 (observed on mainnet). Add a cleanup loop, after the decrement loop, that removes any account expiry left with no pool txs (mirrors try_make_space). Runs separately so per-tx stuck decrements aren't skipped by early set removal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d466d93 to
9c2cd20
Compare
Problem
On mainnet Grafana (v0.14.3),
mempool_accounts_with_gapnever drops below 1, whilemempool_stuck_txssometimes hits 0 — an impossible pair, since a nonce-gap account must hold at least one stuck tx. One counter is leaking.Root cause (
Mempool::remove_expired_txs): when a gap account's stuck txs expire (TTL), the per-tx loop correctly decrementsn_stuck_txs, but theAddressToNoncemap it returns — the only input toupdate_accounts_with_gap— is built fromqueued_txsonly. Stuck txs are never intx_queue(fee mode enqueues only txs whose nonce == account nonce; a gap tx has a higher nonce), so a gap account whose stuck txs all expire is never re-evaluated and lingers inaccounts_with_gap. There's no GC (only aTODO(clean_accounts)), so it self-heals only if a new tx for that account later arrives or it's committed/evicted → a persistent floor of ~1 ghost account (accounts_with_gap> 0 whilestuck_txs== 0).Evidence (GCP logs,
apollo-mainnet-0,sequencer-mempool, 11:02–11:07 UTC)Account 0x04678eb4…61b5 has a nonce gap; 1 transaction(s) are now stuck., also0x048ddc…441f,0x05aed2…2cba).0x3e17…ff87c,0x4c30…1e84) areRemoved expired transactionsthree times, exactly 5 min apart (10:51/10:56/11:01) — stuck txs expiring, P2P-rebroadcast, re-expiring.Fix
After the existing decrement loop, drop any account expiry left with no pool txs (mirrors the emptiness cleanup already in
try_make_space):Must be a separate loop after the decrements, otherwise removing an account from the set early would make its remaining per-tx
decrement_stuck_txs_if_gap_accountcalls no-op and undercount.remove_from_accounts_with_gapsubtractsn_txs_for_address= 0 at cleanup time (no double-decrement) and is idempotent on duplicate addresses.Note: deliberately not fixed by mapping all
removed_txsinto the returned nonce map —resolve_nonce(addr, hint)falls back tohintonly when state has no committed/staged nonce; for a stuck txhint = tx.nonce > account_nonce, which would mis-evaluate accounts that still hold txs. The correct signal is nonce-independent: the account has no pool txs left.Tests
Added
stuck_txs_counter_expiry_removes_emptied_gap_account(the existing_expiry_decrements_countonly expires 1 of 2 txs, so the account legitimately stays and the leak was uncovered). A single stuck tx fully expires → asserts both counters return to 0. Fails pre-fix withaccounts_with_gap mismatch left=1 right=0(the exact prod symptom); passes post-fix.Verified: full
apollo_mempoolsuite 113/0,rust_fmt.shclean,clippy -D warningsclean.🤖 Generated with Claude Code