[r3.6] execution/cache: prevent dead-fork StateCache fills across unwind - #23260
Merged
Conversation
…3005) Fixes #22463. `StateCache` stores latest committed state. Unwind already made resident dead-fork entries stale, but readers could add those values again from an old or transient view: a transaction could survive or first bind during unwind, staged unwind rows still existed in the backing database, and read-ahead could fill concurrently. This PR closes those windows by binding fill authority to both the durable `PlainStateVersion` and the lifetime of the original `ReadView`. Reads constrained by a staged unwind cannot fill, cache changes are published only after the database commit, and read-ahead cannot cross the unwind transition. Snapshot and immutable-file publication are a separate coherence boundary. #23028 still requires #23047 or an equivalent publication hook and is not addressed here. Bounded speculative-unwind fills in the separate commitment `BranchCache` pre-exist this PR and are tracked in Suggested order: 1. `execution/cache/view.go` and `state_cache.go`: fill admission and publication. 2. `db/state/execctx/domain_shared.go`: transaction identity, bounded reads, and commit/unwind integration. 3. `db/kv/membatchwithdb/memory_mutation.go` and `db/state/temporal_mem_batch.go`: `PlainStateVersion` ownership and monotonicity. 4. `execution/exec/blocks_read_ahead.go` and `execution/execmodule`: read-ahead exclusion and lifecycle. Focused regression tests sit beside each area. | Marker | Protects | | --- | --- | | `PlainStateVersion` | The durable state visible to a transaction | | `readViewEpoch` | Whether a `ReadView` predates the latest unwind or state discontinuity | | Per-cache entry epoch and unwind floor | Whether a stored value belongs to the retained fork | Once the cache has a durable state version, an admission-gated state fill is accepted only if the view has the published state version and current epoch, publication is not in progress, its exact domain frontier is not behind the cache, and the read has no staged-unwind step bound. Content-addressed code-size fills do not need these state-view checks. An ineligible view may still read cache hits; only its fill authority is revoked. `WithFrontier` preserves the original epoch, so rebinding cannot renew an old view. Stored entries remain O(1) to invalidate and are discarded lazily. The three markers stay separate because durable state, reader, and stored-entry lifetimes change at different boundaries. 1. Staging an unwind revokes existing views, invalidates stored entries, and records the lowest staged boundary. Bounded reads cannot fill. 2. Flush advances `PlainStateVersion` exactly once with the domain writes and collects cache updates without publishing them. 3. The database transaction commits. 4. Cache publication applies the complete batch. It repeats unwind invalidation at the durable boundary, rejects delayed or out-of-order versions, preserves entries after a continuous forward commit, and clears them when continuity is unknown. During publication, reads and view binding remain available, but fills are disabled. A view bound during publication remains fill-inert until explicitly rebound. `MemoryMutation` resolves untouched sequences from its backing transaction and flushes only changed sequence keys, so it cannot replay an older state version. Pre-commit notifications receive the projected version explicitly rather than deriving it from overlay sequence writes. Notification ordering itself is unchanged and remains tracked in #23240. One semaphore permit covers read-ahead warmup and unwind exclusion. A warmup acquires it without blocking, so work requested while another warmup or an unwind owns or waits for the permit is skipped rather than queued. Unwind callers acquire it with their context and abort before staging if cancellation wins. `updateForkChoice` and `SetHead` hold the permit through unwind and publication; `ValidateChain` acquires it only when it stages an unwind. Every FCU currently excludes warmup, including FCUs that do not unwind; narrowing that scope is tracked in #23003. - The cache-hit path is unchanged. - `View(nil)` adds one atomic load. Binding a fill-enabled `ReadView` also takes `admissionMu.RLock` to check publication and state-version eligibility; getters retain that view instead of paying the binding cost per key. - Normal getters reuse the `SharedDomains` transaction's memoized state version. A different transaction resolves its version at initial binding. If that resolution temporarily fails, later cache misses retry it; each retry is local to that miss. - Unwind invalidation remains O(1), with no cache scan or diff replay. - Each accepted warmup performs one uncontended semaphore acquisition. Rejected warmups do not start a goroutine, and the gate is never touched per key. Regression tests cover old and newly bound views across every unwind phase, bounded state and code-hash reads, delayed publications, memory-overlay state versions, read-ahead exclusion and cancellation, and valid forward fills. --------- Co-authored-by: Alexey Sharov <askalexsharov@gmail.com>
yperbasis
requested review from
AskAlexSharov,
mh0lt and
sudeepdino008
as code owners
August 13, 2026 16:09
yperbasis
enabled auto-merge
August 13, 2026 20:50
AskAlexSharov
approved these changes
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cherry-pick of #23005 to release/3.6.
r3.6-specific adaptations
DISCARD_COMMITMENTat process startup.