Skip to content

execution/execmodule: drain read-ahead only before an actual unwind - #23003

Closed
yperbasis wants to merge 1 commit into
mainfrom
yperbasis/fcu-drain-unwind-only
Closed

execution/execmodule: drain read-ahead only before an actual unwind#23003
yperbasis wants to merge 1 commit into
mainfrom
yperbasis/fcu-drain-unwind-only

Conversation

@yperbasis

Copy link
Copy Markdown
Member

Stacked on #22444 (base branch: test/statecache-delete-rpc-repro); draft until it merges. Performance change, not a fix — it depends on #22444's fill admission for its safety argument.

What

updateForkChoice drained any in-flight read-ahead warmup at the top of every FCU — duplicate and non-reorg FCUs included — blocking the tip hot path on the preceding newPayload's full warmup. This moves the drain into unwindIfNeeded's unwind branch, so only an actual reorg waits.

Why it is safe

  • Forward direction: with execution, db: bind StateCache fills to transaction views and reject stale fills #22444, a laggard warmup fill is admission-ordered against the flush cache-apply (frontier >= appliedEnd under the same lock), so nothing needs to wait for it.
  • Unwind direction: still needs the drain — an unwind lowers the applied frontier, so a pre-unwind view passes admission and would pin a dead-fork value under the live epoch. The drain now sits immediately before UnwindTo/RunUnwind, matching unwindToCommonCanonical and SetHead, which already drain there.
  • Same window: no new warmup can start while the FCU holds the semaphore, so draining at the unwind site bounds the same set of in-flight warmups as draining at FCU entry did.
  • forkValidator.ClearWithUnwind (the deferred FCU cleanup) only clears fork-validator bookkeeping — no cache epoch bump — so no other epoch-bump site in the FCU flow needs the drain.

Testing

go test ./execution/execmodule/ ./execution/exec/ and -race over the Unwind/ForkChoice/SetHead tests; make lint clean.

Base automatically changed from test/statecache-delete-rpc-repro to main August 5, 2026 13:46
pull Bot pushed a commit to Dustin4444/erigon that referenced this pull request Aug 13, 2026
…igontech#23005)

Fixes erigontech#22463.

## Summary

`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. erigontech#23028 still requires erigontech#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
erigontech#23253.

## Review guide

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.

## Correctness invariants

| 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.

## Commit and unwind flow

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 erigontech#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 erigontech#23003.

## Performance

- 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.

## Validation

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>
updateForkChoice drained in-flight warmup at the top of every FCU —
duplicate and non-reorg FCUs included — waiting for the whole warmup on the
tip hot path. Fill admission orders warmup fills against the flush
cache-apply, so the forward direction needs no drain; only the unwind
epoch bump does (a pre-unwind view passes admission once the applied
frontier is lowered). Move the drain into unwindIfNeeded's unwind branch,
matching unwindToCommonCanonical and SetHead, which already drain there.
No new warmup can start while the FCU holds the semaphore, so the later
drain covers the same window.
@yperbasis
yperbasis force-pushed the yperbasis/fcu-drain-unwind-only branch from 8c5dd9a to b22d4d3 Compare August 15, 2026 07:30
@yperbasis yperbasis closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant