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.
PayloadProcessor::spawn_caching_withcallsself.cache_for(env.parent_hash)to fetch the cached state.- Immediately after,
.split()consumes theSavedCacheand returns(ExecutionCache, CachedStateMetrics)so we can wire them into thePrewarmContext. - Dropping that
SavedCacheclone releases theusage_guardArc, so the strong count on the shared instance inExecutionCache.innerfalls back to1. - The prewarm task only keeps the raw
ExecutionCacheand metrics clones. Those clones share the underlying moka caches, but they do not hold on to the guard anymore. - Any concurrent caller can now invoke
execution_cache.get_cache_for(parent_hash)andis_available()will happily returntrue, because the only live guard is the one inExecutionCache.inneritself.
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 thePrewarmContextstores only the cloned caches/metrics.payload_processor/prewarm.rs:158-162– wheresave_cachelater updates the shared cache, but by then the guard has been long gone.
- 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.
- Hold on to the guard for as long as prewarm execution uses the cache. The easiest fix is to store either the
SavedCacheitself or the guard Arc insidePrewarmContext, which lives untilsave_cacheruns. - 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.