fix(l1): break out of prefix iterator on mismatch in get_transaction_location#6702
Conversation
|
🤖 Kimi Code ReviewThis is a correct and necessary fix for a critical performance issue. The change properly addresses the behavior of RocksDB's prefix iterator, which doesn't automatically terminate when keys no longer match the prefix. Correctness & Logic
Safety
Code Quality
Minor Observation (pre-existing, not changed in this PR)
Conclusion: The fix is correct, safe, and addresses a severe performance regression. Ship it. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewPR Review:
|
🤖 Codex Code ReviewNo findings. crates/storage/store.rs looks correct: stopping at the first non-matching key preserves correctness because RocksDB iterates in lexicographic key order, so all Residual risk is only test coverage: I did not find a regression test that exercises RocksDB Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryThis PR fixes a critical performance regression in
Confidence Score: 5/5Safe to merge — the change is a single-clause addition that correctly bounds a previously unbounded iterator loop, supported by both the RocksDB ordering guarantee and extensive benchmarking data. The fix is minimal, logically sound, and well-motivated. The No files require special attention; the single changed region in
|
| Filename | Overview |
|---|---|
| crates/storage/store.rs | Adds else { break; } to stop the prefix iterator scan once a non-matching key is found, fixing an O(table-size) scan per eth_getTransactionByHash call and replacing it with an O(matches + 1) bounded scan. |
Sequence Diagram
sequenceDiagram
participant Caller as eth_getTransactionByHash
participant SpawnBlocking as spawn_blocking task
participant RocksDB as RocksDB (TRANSACTION_LOCATIONS CF)
Caller->>SpawnBlocking: get_transaction_location(tx_hash)
SpawnBlocking->>RocksDB: prefix_iterator(tx_hash_bytes)
Note over RocksDB: Seeks to first key >= tx_hash_bytes
loop Bounded scan (matches + 1 reads)
RocksDB-->>SpawnBlocking: key, value
alt "key.len()==64 && key[0..32]==tx_hash"
SpawnBlocking->>SpawnBlocking: push to transaction_locations
else key does not match (first mismatch)
SpawnBlocking->>SpawnBlocking: break (NEW)
Note over SpawnBlocking: No further matches possible due to lexicographic ordering
end
end
SpawnBlocking->>SpawnBlocking: Filter by canonical chain
SpawnBlocking-->>Caller: "Option<(BlockNumber, BlockHash, Index)>"
Reviews (1): Last reviewed commit: "fix(storage): break out of prefix iterat..." | Re-trigger Greptile
Lines of code reportTotal lines added: Detailed view |
…tion_location `rust-rocksdb`'s prefix_iterator_cf seeks to the prefix but iterates forward indefinitely — the caller must break manually when the key no longer matches. The previous loop filtered non-matching keys via an `if` check but kept iterating, silently scanning to the end of the TRANSACTION_LOCATIONS column family on every call (hundreds of GB on mainnet, taking seconds to minutes per call). Add the missing `break`: once we see a non-matching key, all subsequent keys also can't match (RocksDB key order is lexicographic and all entries with the same tx_hash prefix are contiguous), so iteration can stop. Closes #6688
c21820e to
4f04508
Compare
Motivation
Store::get_transaction_locationwas performing an unbounded RocksDB scan on everyeth_getTransactionByHashcall. On mainnet, this meant each call scanned hundreds of GB of compressed SSTs taking seconds per request, and under modest concurrent load (a few hundred persistent connections), it cascaded into multi-hour latencies and full saturation of the Tokio blocking pool — see #6688 for the full incident write-up.Description
rust-rocksdb'sprefix_iterator_cfseeks to the prefix but iterates forward indefinitely; the caller must break manually when the key no longer matches. The previous loop incrates/storage/store.rs:596-602used aniffilter that correctly ignored non-matching keys but didn't stop iteration, so every call scanned from the seek point to the end of theTRANSACTION_LOCATIONScolumn family.Fix is one line:
else { break; }. RocksDB stores keys in lexicographic order and all entries sharing atx_hashprefix are contiguous; once we see a non-matching key, no further matches can appear ahead. Iteration is now bounded to(matches + 1)reads — typically 1-2 entries.Verification on mainnet-10 (fresh sync, hot OS cache)
Single-call latency, identical reproducer to the issue body:
0x0000…0010x8000…0000x454a…b6110xfff…ffeThe linear-with-position pattern disappears entirely — all calls flat at sub-millisecond.
Persistent-connection storm (
oha -z 3m -c 200 --no-tuiagainst worst-case low hash, same as the reproduction in issue #6688):99.98% of requests now complete in <6 ms. The 22× bandwidth-contention amplifier observed under storm conditions is gone — the node finds a stable operating point at 264K req/s with no thread-pool saturation. The mathematical model from the issue (Little's Law-based queue growth) cleanly explains why eliminating the per-call O(table) cost defuses the entire cascade.
Follow-up improvements (separate PRs)
These are referenced in #6688 but worth restating:
prefix_iteratorstorage trait method to construct iterators withset_iterate_upper_bound(prefix + 1)so RocksDB itself stops at the bound. Removes the dependence on caller discipline. The same-PR fix here is sufficient for correctness, but a typed bounded-iterator API would prevent recurrence in any future call site.TRANSACTION_LOCATIONSfor negative-lookup speedup. Only helps after compaction rewrites existing SSTs.tx_hashonly with the value being aVec<(BlockNumber, BlockHash, Index)>. Reduces this from a prefix scan to a singleget_cf. Best long-term option.spawn_blocking. Independent of this bug: orphaned blocking tasks from cancelled futures kept the pool saturated even after the rogue IP was blocked on mainnet-1.Checklist
Closes #6688