Skip to content

Instantly share code, notes, and snippets.

@yongkangc
Created September 21, 2025 03:43
Show Gist options
  • Select an option

  • Save yongkangc/8d448be8dfa0650ef042215be683f12a to your computer and use it in GitHub Desktop.

Select an option

Save yongkangc/8d448be8dfa0650ef042215be683f12a to your computer and use it in GitHub Desktop.
Execution cache usage guard review

Execution Cache Usage Guard Review (mattsse comment)

Context

The PR introduces SavedCache::usage_guard, an Arc<()> that is meant to ensure the cached execution state for a parent block cannot be reused while prewarm tasks are still mutating it. ExecutionCache::get_cache_for only returns a cache when SavedCache::is_available() sees the guard's strong count at 1.

Why the guard never trips today

  1. PayloadProcessor::spawn_caching_with calls self.cache_for(env.parent_hash) to fetch the cached state.
  2. Immediately after, .split() consumes the SavedCache and returns (ExecutionCache, CachedStateMetrics) so we can wire them into the PrewarmContext.
  3. Dropping that SavedCache clone releases the usage_guard Arc, so the strong count on the shared instance in ExecutionCache.inner falls back to 1.
  4. The prewarm task only keeps the raw ExecutionCache and metrics clones. Those clones share the underlying moka caches, but they do not hold on to the guard anymore.
  5. Any concurrent caller can now invoke execution_cache.get_cache_for(parent_hash) and is_available() will happily return true, because the only live guard is the one in ExecutionCache.inner itself.

This is exactly what mattsse pointed out by linking:

  • payload_processor/mod.rs:317 – where we call .split() and drop the guard.
  • payload_processor/prewarm.rs:249 – where the PrewarmContext stores only the cloned caches/metrics.
  • payload_processor/prewarm.rs:158-162 – where save_cache later updates the shared cache, but by then the guard has been long gone.

Consequences

  • Two prewarm tasks can operate concurrently on the same cached state if another payload for the same parent hash sneaks in. The guard was supposed to prevent that, so we currently have no protection against cache corruption.
  • Because the guard is public (pub(crate) usage_guard), even if we fixed the lifetime issue, other modules could still clone/drop it, making reasoning about exclusivity harder.

What needs to change

  • Hold on to the guard for as long as prewarm execution uses the cache. The easiest fix is to store either the SavedCache itself or the guard Arc inside PrewarmContext, which lives until save_cache runs.
  • Make the guard inaccessible from outside the cached-state module and expose a dedicated API (e.g. SavedCache::lease()) so future call sites cannot accidentally drop it too early.

Once that is in place, SavedCache::is_available() will only return true after prewarm is completely done and has either updated or discarded that cached state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment