Found while reviewing #23033's code-store eviction wiring (codex review, point 3). The eviction mechanics themselves are fine; the size accounting is not rollback-safe.
Mechanism
CodeStore.Evict enforces the TblCodeCache byte cap using an in-memory counter (tableSizeBytes), seeded from the table once per process and decremented as DeleteCurrent walks. The decrements happen inside the caller's transaction, but the counter is not transactional: if the enclosing tx rolls back after Evict ran, MDBX restores the deleted rows while the counter keeps the decrements.
Result: the counter under-counts, so future evictions fire only when the true size exceeds cap + drift — the cap becomes slack, not unenforced. One failure costs roughly one eviction's worth (~10% of cap). It compounds only under repeated evict-then-rollback cycles (e.g. a prune failing every FCU), which is pathological but possible.
Where it triggers
A related accepted-transient, for completeness: catchup's in-loop eviction runs at the start of each cycle, so the final batch's code overshoots the cap until the first forkchoice prune evicts it — minutes of bounded overshoot, no action needed.
Fix direction
Not reseed-on-rollback call sites (a "remember to call X on every error path" contract is the forgettable-wiring pattern #23033 spends several commits eliminating). The proper fix makes drift impossible by construction: store the table size transactionally — a metadata row in TblCodeCache (or a sibling table) read and written inside the eviction's own transaction, so the size commits and rolls back together with the deletes. The once-per-process seed scan then disappears too.
The write direction needs nothing: a rolled-back PutByHash leaves the counter over-counting, which over-evicts — the safe direction.
Found while reviewing #23033's code-store eviction wiring (codex review, point 3). The eviction mechanics themselves are fine; the size accounting is not rollback-safe.
Mechanism
CodeStore.Evictenforces theTblCodeCachebyte cap using an in-memory counter (tableSizeBytes), seeded from the table once per process and decremented asDeleteCurrentwalks. The decrements happen inside the caller's transaction, but the counter is not transactional: if the enclosing tx rolls back afterEvictran, MDBX restores the deleted rows while the counter keeps the decrements.Result: the counter under-counts, so future evictions fire only when the true size exceeds
cap + drift— the cap becomes slack, not unenforced. One failure costs roughly one eviction's worth (~10% of cap). It compounds only under repeated evict-then-rollback cycles (e.g. a prune failing every FCU), which is pathological but possible.Where it triggers
main: the forkchoice prune path —Evictruns insideagg.CollateAndPrune's callback; an error there rolls back the tx after the deletes.A related accepted-transient, for completeness: catchup's in-loop eviction runs at the start of each cycle, so the final batch's code overshoots the cap until the first forkchoice prune evicts it — minutes of bounded overshoot, no action needed.
Fix direction
Not reseed-on-rollback call sites (a "remember to call X on every error path" contract is the forgettable-wiring pattern #23033 spends several commits eliminating). The proper fix makes drift impossible by construction: store the table size transactionally — a metadata row in
TblCodeCache(or a sibling table) read and written inside the eviction's own transaction, so the size commits and rolls back together with the deletes. The once-per-process seed scan then disappears too.The write direction needs nothing: a rolled-back
PutByHashleaves the counter over-counting, which over-evicts — the safe direction.