1use super::{
17 bal_prewarm_pool::BalPrewarmPool,
18 reuse::{BalanceTracker, PoolTx, PrewarmResults, RecordingDb, RecordingHandle, SpeculativeTx},
19 StateRootHintStream, StateRootUpdateStream,
20};
21use crate::tree::{
22 CachedStateCacheMetrics, CachedStateMetrics, CachedStateProvider, ExecutionEnv,
23 PayloadExecutionCache, SavedCache,
24};
25use alloy_consensus::transaction::TxHashRef;
26use alloy_eip7928::bal::DecodedBal;
27use alloy_eips::eip4895::Withdrawal;
28use alloy_primitives::keccak256;
29use evm2::evm::Database;
30use metrics::{Counter, Gauge, Histogram};
31use rayon::prelude::*;
32use reth_evm::{
33 database::StateProviderDatabase, precompile_cache::PrecompileCacheMap, ConfigureEvm,
34 Evm as EvmInstance, EvmEnv, EvmEnvFor, EvmFor, EvmTypesFor, ExecutableTxFor, RecoveredTx,
35 SpecFor,
36};
37use reth_metrics::Metrics;
38use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
39use reth_provider::{
40 BlockExecutionOutput, BlockNumReader, ChangeSetReader, DatabaseProviderFactory,
41 DatabaseProviderROFactory, EvmStateProvider, EvmStateProviderBox, HistoryReader,
42 PruneCheckpointReader, StageCheckpointReader, StateProvider, StorageChangeSetReader,
43 StorageSettingsCache,
44};
45use reth_storage_overlay::OverlayStateProviderFactory;
46use reth_tasks::{Runtime, WorkerStatefulPool};
47use reth_trie_common::MultiProofTargetsV2;
48use std::{
49 sync::{
50 atomic::{AtomicBool, AtomicUsize, Ordering},
51 mpsc::{self, channel, Receiver, Sender},
52 Arc,
53 },
54 time::Duration,
55};
56use tokio::sync::oneshot;
57use tracing::{debug, debug_span, instrument, trace, trace_span, warn, Span};
58
59const MAX_REFRESH_ROUNDS: usize = 4;
61
62const REFRESH_WINDOW: usize = 64;
65
66const REFRESH_POLL_INTERVAL: Duration = Duration::from_micros(50);
68
69#[derive(Debug)]
74pub enum PrewarmMode<Tx> {
75 Transactions {
77 pending: Receiver<(usize, Tx)>,
79 hints: Option<StateRootHintStream>,
81 },
82 BlockAccessList {
84 bal: Arc<DecodedBal>,
86 updates: Option<StateRootUpdateStream>,
88 },
89 Skipped,
92}
93
94#[derive(Debug)]
99pub struct PrewarmCacheTask<N, P, Evm>
100where
101 N: NodePrimitives,
102 Evm: ConfigureEvm<Primitives = N>,
103{
104 executor: Runtime,
106 execution_cache: PayloadExecutionCache,
108 ctx: PrewarmContext<N, P, Evm>,
110 actions_rx: Receiver<PrewarmTaskEvent<N::Receipt>>,
112 parent_span: Span,
114}
115
116impl<N, P, Evm> PrewarmCacheTask<N, P, Evm>
117where
118 N: NodePrimitives,
119 P: DatabaseProviderFactory + Clone + 'static,
120 P::Provider: BlockNumReader
121 + PruneCheckpointReader
122 + StageCheckpointReader
123 + ChangeSetReader
124 + StorageChangeSetReader
125 + StorageSettingsCache
126 + HistoryReader
127 + 'static,
128 Evm: ConfigureEvm<Primitives = N> + 'static,
129{
130 pub fn new(
132 executor: Runtime,
133 execution_cache: PayloadExecutionCache,
134 ctx: PrewarmContext<N, P, Evm>,
135 ) -> (Self, Sender<PrewarmTaskEvent<N::Receipt>>) {
136 let (actions_tx, actions_rx) = channel();
137
138 trace!(
139 target: "engine::tree::payload_processor::prewarm",
140 prewarming_threads = executor.prewarming_pool().current_num_threads(),
141 transaction_count = ctx.env.transaction_count,
142 "Initialized prewarm task"
143 );
144
145 (
146 Self { executor, execution_cache, ctx, actions_rx, parent_span: Span::current() },
147 actions_tx,
148 )
149 }
150
151 fn spawn_txs_prewarm<Tx>(
158 &self,
159 pending: mpsc::Receiver<(usize, Tx)>,
160 actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
161 state_root_hint_stream: Option<StateRootHintStream>,
162 ) where
163 Tx: ExecutableTxFor<Evm> + Clone + Send + Sync + 'static,
164 {
165 let ctx = self.ctx.clone();
166 let pool = Arc::clone(self.executor.prewarming_pool());
167 let span = Span::current();
168
169 self.executor.spawn_blocking_named("prewarm-txs", move || {
170 let _enter = debug_span!(
171 target: "engine::tree::payload_processor::prewarm",
172 parent: &span,
173 "prewarm_txs"
174 )
175 .entered();
176
177 let ctx = &ctx;
178
179 let mut tx_count = 0usize;
180 let state_root_hint_stream = state_root_hint_stream.as_ref();
181 let mut refresh_txs = Vec::new();
183 {
184 let pool = WorkerStatefulPool::new(pool);
185 let pool = &pool;
186 pool.in_place_scope(|s| {
187 while let Ok((index, tx)) = pending.recv() {
188 if ctx.should_stop() {
189 trace!(
190 target: "engine::tree::payload_processor::prewarm",
191 "Termination requested, stopping transaction distribution"
192 );
193 break;
194 }
195
196 if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
198 continue;
199 }
200
201 tx_count += 1;
202 if ctx.prewarm_results.is_some() {
203 refresh_txs.push((index, tx.clone()));
204 }
205 let seeded = if let Some(results) = &ctx.prewarm_results &&
207 let Some(seeds) = &ctx.env.pool_seeds
208 {
209 results.seed(index, tx.tx().tx_hash(), seeds)
210 } else {
211 None
212 };
213 if seeded.is_some() {
214 ctx.metrics.seeded_transactions.increment(1);
215 }
216 let parent_span = Span::current();
217 s.spawn(move |_| {
218 let _enter = trace_span!(
219 target: "engine::tree::payload_processor::prewarm",
220 parent: parent_span,
221 "prewarm_tx",
222 i = index,
223 )
224 .entered();
225 Self::transact_worker(
226 ctx,
227 pool,
228 index,
229 tx,
230 seeded,
231 state_root_hint_stream,
232 );
233 });
234 }
235
236 if let Some(state_root_hint_stream) = state_root_hint_stream &&
238 let Some(withdrawals) = &ctx.env.withdrawals &&
239 !withdrawals.is_empty()
240 {
241 let targets = multiproof_targets_from_withdrawals(withdrawals);
242 state_root_hint_stream.on_access_hint(targets.into());
243 }
244 });
245
246 if let Some(results) = &ctx.prewarm_results {
247 Self::refresh_results(ctx, pool, results, &refresh_txs);
248 }
249 }
250
251 let _ = actions_tx
252 .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: tx_count });
253 });
254 }
255
256 fn refresh_results<Tx>(
264 ctx: &PrewarmContext<N, P, Evm>,
265 pool: &WorkerStatefulPool,
266 results: &PrewarmResults<EvmTypesFor<Evm>>,
267 txs: &[(usize, Tx)],
268 ) where
269 Tx: ExecutableTxFor<Evm> + Clone + Send + Sync,
270 {
271 let mut rounds = 0;
272 let mut window_len = usize::MAX;
275 while !ctx.should_stop() {
276 let generation = results.predicted().generation();
277 let next = ctx.executed_tx_index.load(Ordering::Relaxed) + 1;
279 let start = txs.partition_point(|(index, _)| *index <= next);
280 if start == txs.len() {
281 break
282 }
283 let window = &txs[start..txs.len().min(start.saturating_add(window_len))];
284 let stale = window
286 .iter()
287 .filter(|(index, _)| results.needs_refresh(*index))
288 .collect::<Vec<_>>();
289 let refreshed = !stale.is_empty();
290 if refreshed {
291 let cursor = AtomicUsize::new(0);
292 pool.in_place_scope(|s| {
293 for _ in 0..pool.current_num_threads().min(stale.len()) {
294 s.spawn(|_| {
295 while let Some((index, tx)) =
296 stale.get(cursor.fetch_add(1, Ordering::Relaxed))
297 {
298 if ctx.should_stop() {
299 break
300 }
301 Self::transact_worker(ctx, pool, *index, tx.clone(), None, None);
302 }
303 });
304 }
305 });
306 }
307
308 rounds += 1;
309 if refreshed && rounds < MAX_REFRESH_ROUNDS {
310 continue
311 }
312 rounds = 0;
313 window_len = REFRESH_WINDOW;
314 while results.predicted().generation() == generation && !ctx.should_stop() {
315 std::thread::sleep(REFRESH_POLL_INTERVAL);
316 }
317 }
318 }
319
320 fn transact_worker<Tx>(
326 ctx: &PrewarmContext<N, P, Evm>,
327 pool: &WorkerStatefulPool,
328 index: usize,
329 tx: Tx,
330 seeded: Option<Arc<PoolTx<EvmTypesFor<Evm>>>>,
331 state_root_hint_stream: Option<&StateRootHintStream>,
332 ) where
333 Tx: ExecutableTxFor<Evm>,
334 {
335 if ctx.should_stop() || index < ctx.executed_tx_index.load(Ordering::Relaxed) {
336 return;
337 }
338
339 pool.get_or_init(
340 || ctx.evm_for_ctx(),
341 |state| {
342 let Some(evm) = state.evm.as_mut() else { return };
343
344 let start = Instant::now();
345
346 let (tx_env, tx) = tx.into_parts();
347 let send_hints = |state: &evm2::evm::PendingState| {
348 if index > 0 &&
349 let Some(state_root_hint_stream) = state_root_hint_stream
350 {
351 let (targets, storage_targets) =
352 MultiProofTargetsV2::from_account_changes(state.account_changes());
353 ctx.metrics.prefetch_storage_targets.record(storage_targets as f64);
354 state_root_hint_stream.on_access_hint(targets.into());
355 }
356 };
357
358 if let Some(seeded) = seeded {
359 if let Err(err) = seeded.load_reads(evm.state_db_mut()) {
361 trace!(
362 target: "engine::tree::payload_processor::prewarm",
363 %err,
364 tx_hash=%tx.tx().tx_hash(),
365 "Failed to load reads of seeded pool transaction",
366 );
367 }
368 send_hints(seeded.state());
369 return;
370 }
371
372 let Some(results) = &ctx.prewarm_results else {
373 let hint_stream = state_root_hint_stream.filter(|_| index > 0);
374 let res = evm.transact_and_discard(&tx_env, |executed| {
375 hint_stream.is_some().then(|| {
376 MultiProofTargetsV2::from_account_changes(executed.account_changes())
377 })
378 });
379 let hints = match res {
380 Ok(hints) => hints,
381 Err(err) => {
382 trace!(
383 target: "engine::tree::payload_processor::prewarm",
384 %err,
385 tx_hash=%tx.tx().tx_hash(),
386 sender=%tx.signer(),
387 "Error when executing prewarm transaction",
388 );
389 ctx.metrics.transaction_errors.increment(1);
390 return;
391 }
392 };
393 ctx.metrics.execution_duration.record(start.elapsed());
394 if ctx.should_stop() {
395 return;
396 }
397 if let Some(stream) = hint_stream &&
398 let Some((targets, storage_targets)) = hints
399 {
400 ctx.metrics.prefetch_storage_targets.record(storage_targets as f64);
401 stream.on_access_hint(targets.into());
402 }
403 ctx.metrics.total_runtime.record(start.elapsed());
404 return;
405 };
406
407 let speculative = SpeculativeTx::new(*tx.signer(), tx.tx());
410 drop(evm.take_state_cache());
413 state.recording.start(Some((Arc::clone(results.predicted()), index)));
414 let res = evm
415 .transact_with_inspector(&tx_env, BalanceTracker::new(state.recording.clone()));
416 let reads = state.recording.take_reads();
417 let (mut balances, res) = match res {
418 Ok(res) => res,
419 Err(err) => {
420 trace!(
421 target: "engine::tree::payload_processor::prewarm",
422 %err,
423 tx_hash=%tx.tx().tx_hash(),
424 sender=%tx.signer(),
425 "Error when executing prewarm transaction",
426 );
427 ctx.metrics.transaction_errors.increment(1);
428 if let Some(reads) = reads {
429 results.reject(index, reads);
430 }
431 return;
432 }
433 };
434 ctx.metrics.execution_duration.record(start.elapsed());
435
436 if ctx.should_stop() {
437 return;
438 }
439
440 send_hints(&res.pending_state);
441
442 if let Some(reads) = reads &&
443 index >= ctx.executed_tx_index.load(Ordering::Relaxed)
444 {
445 results.store(index, res, reads, &mut balances, speculative);
446 }
447 ctx.metrics.total_runtime.record(start.elapsed());
448 },
449 );
450 }
451
452 #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
468 fn save_cache(
469 self,
470 execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
471 valid_block_rx: mpsc::Receiver<()>,
472 ) {
473 let start = Instant::now();
474
475 let Self {
476 execution_cache,
477 ctx: PrewarmContext { env, metrics, cache_state_metrics, saved_cache, .. },
478 ..
479 } = self;
480 let hash = env.hash;
481
482 if let Some(saved_cache) = saved_cache {
483 debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
484 let (previous, rejected) = execution_cache.update_with_guard(|cached| {
485 let new_cache = SavedCache::new(hash, saved_cache.into_cache());
486
487 if new_cache.cache().insert_state(&execution_outcome.state).is_err() {
489 debug!(target: "engine::caching", "cleared execution cache on update error");
490 return (cached.take(), Some(new_cache));
491 }
492
493 new_cache.update_metrics(cache_state_metrics.as_ref());
494
495 if valid_block_rx.recv().is_err() {
499 debug!(target: "engine::caching", "cleared execution cache on invalid block");
500 return (cached.take(), Some(new_cache));
501 }
502
503 let reused =
504 cached.as_ref().is_some_and(|previous| previous.shares_cache_with(&new_cache));
505 let previous = cached.replace(new_cache);
506 if reused {
507 drop(previous);
510 (None, None)
511 } else {
512 (previous, None)
514 }
515 });
516
517 drop((previous, rejected));
522
523 let elapsed = start.elapsed();
524 debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
525
526 metrics.cache_saving_duration.set(elapsed.as_secs_f64());
527 }
528 }
529
530 #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
538 fn run_bal_prewarm(
539 &self,
540 decoded_bal: Arc<DecodedBal>,
541 actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
542 hashed_update_stream: Option<StateRootUpdateStream>,
543 ) {
544 let bal = decoded_bal.as_bal();
545 if bal.is_empty() {
546 if let Some(hashed_update_stream) = hashed_update_stream {
547 hashed_update_stream.finish();
548 }
549 let _ =
550 actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
551 return;
552 }
553
554 trace!(
555 target: "engine::tree::payload_processor::prewarm",
556 accounts = bal.len(),
557 "Starting BAL prewarm"
558 );
559
560 let ctx = self.ctx.clone();
561 let parent_span = Span::current();
562 let stream_parent_span = parent_span;
563 let prefetch_bal = Arc::clone(&decoded_bal);
564 let stream_bal = Arc::clone(&decoded_bal);
565 let (stream_tx, stream_rx) = oneshot::channel();
566
567 if let Some(hashed_update_stream) = hashed_update_stream {
568 let ctx = ctx.clone();
569 let pool = Arc::clone(self.executor.bal_streaming_pool());
570 self.executor.bal_streaming_pool().spawn(move || {
571 let branch_span = debug_span!(
572 target: "engine::tree::payload_processor::prewarm",
573 parent: &stream_parent_span,
574 "bal_hashed_state_stream",
575 bal_accounts = stream_bal.as_bal().len(),
576 );
577 let parent_span = branch_span.clone();
578 let _span = branch_span.entered();
579
580 {
581 let pool = WorkerStatefulPool::new(pool);
582 stream_bal.as_bal().par_iter().for_each(|account_changes| {
583 pool.get_or_init(
584 || None,
585 |provider| {
586 ctx.send_bal_hashed_state(
587 &parent_span,
588 provider,
589 account_changes,
590 &hashed_update_stream,
591 );
592 },
593 );
594 });
595 }
596 hashed_update_stream.finish();
597 let _ = stream_tx.send(());
598 });
599 } else {
600 let _ = stream_tx.send(());
601 }
602
603 if let Some(saved_cache) = ctx.saved_cache &&
604 !ctx.disable_bal_batch_io &&
605 let Some(pool) = ctx.bal_prewarm_pool.as_ref()
606 {
607 let caches = saved_cache.cache().clone();
619 let state_provider_factory = ctx.provider.clone();
620 let build = Arc::new(move || {
621 state_provider_factory.database_provider_ro().map(|provider| {
622 Box::new(provider.into_evm_state_provider()) as EvmStateProviderBox
623 })
624 });
625
626 pool.begin_block(build, caches, ctx.env.txpool_snapshot.clone());
627 let dispatch_start = Instant::now();
628 for account in prefetch_bal.as_bal() {
629 pool.warm_account(account.address, account.storage_slots().map(Into::into));
630 }
631 ctx.metrics.bal_slot_iteration_duration.record(dispatch_start.elapsed());
632 pool.end_block();
633 }
634
635 stream_rx
636 .blocking_recv()
637 .expect("BAL hashed-state streaming task dropped without signaling completion");
638
639 let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
640 }
641
642 #[instrument(
647 parent = &self.parent_span,
648 level = "debug",
649 target = "engine::tree::payload_processor::prewarm",
650 name = "prewarm and caching",
651 skip_all
652 )]
653 pub fn run<Tx>(self, mode: PrewarmMode<Tx>, actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>)
654 where
655 Tx: ExecutableTxFor<Evm> + Clone + Send + Sync + 'static,
656 {
657 match mode {
661 PrewarmMode::Transactions { pending, hints } => {
662 self.spawn_txs_prewarm(pending, actions_tx, hints);
663 }
664 PrewarmMode::BlockAccessList { bal, updates } => {
665 self.run_bal_prewarm(bal, actions_tx, updates);
666 }
667 PrewarmMode::Skipped => {
668 let _ = actions_tx
669 .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
670 }
671 }
672
673 let mut final_execution_outcome = None;
674 let mut finished_execution = false;
675 while let Ok(event) = self.actions_rx.recv() {
676 match event {
677 PrewarmTaskEvent::TerminateTransactionExecution => {
678 debug!(target: "engine::tree::prewarm", "Terminating prewarm execution");
680 self.ctx.stop();
681 }
682 PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx } => {
683 trace!(target: "engine::tree::payload_processor::prewarm", "Received termination signal");
684 self.ctx.stop();
687 final_execution_outcome =
688 Some(execution_outcome.map(|outcome| (outcome, valid_block_rx)));
689
690 if finished_execution {
691 break
693 }
694 }
695 PrewarmTaskEvent::FinishedTxExecution { executed_transactions } => {
696 trace!(target: "engine::tree::payload_processor::prewarm", "Finished prewarm execution signal");
697 self.ctx.metrics.transactions.set(executed_transactions as f64);
698 self.ctx.metrics.transactions_histogram.record(executed_transactions as f64);
699
700 finished_execution = true;
701
702 if final_execution_outcome.is_some() {
703 break
705 }
706 }
707 }
708 }
709
710 debug!(target: "engine::tree::payload_processor::prewarm", "Completed prewarm execution");
711
712 if let Some(Some((execution_outcome, valid_block_rx))) = final_execution_outcome {
714 self.save_cache(execution_outcome, valid_block_rx);
715 }
716 }
717}
718
719#[derive(Debug, Clone)]
721pub struct PrewarmContext<N, P, Evm>
722where
723 N: NodePrimitives,
724 Evm: ConfigureEvm<Primitives = N>,
725{
726 pub env: ExecutionEnv<Evm>,
728 pub evm_config: Evm,
730 pub saved_cache: Option<SavedCache>,
732 pub provider: OverlayStateProviderFactory<P, N>,
734 pub(crate) bal_prewarm_pool: Option<Arc<BalPrewarmPool>>,
737 pub metrics: PrewarmMetrics,
739 pub cache_metrics: Option<CachedStateMetrics>,
742 pub cache_state_metrics: Option<CachedStateCacheMetrics>,
744 pub terminate_execution: Arc<AtomicBool>,
746 pub executed_tx_index: Arc<AtomicUsize>,
750 pub precompile_cache_disabled: bool,
752 pub precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
754 pub disable_bal_parallel_state_root: bool,
757 pub disable_bal_batch_io: bool,
759 pub(crate) prewarm_results: Option<Arc<PrewarmResults<EvmTypesFor<Evm>>>>,
761}
762
763struct PrewarmEvmState<Evm: ConfigureEvm> {
766 evm: Option<EvmFor<'static, Evm>>,
768 recording: RecordingHandle,
769}
770
771impl<N, P, Evm> PrewarmContext<N, P, Evm>
772where
773 N: NodePrimitives,
774 P: DatabaseProviderFactory,
775 P::Provider: BlockNumReader
776 + PruneCheckpointReader
777 + StageCheckpointReader
778 + ChangeSetReader
779 + StorageChangeSetReader
780 + StorageSettingsCache
781 + HistoryReader
782 + 'static,
783 Evm: ConfigureEvm<Primitives = N> + 'static,
784{
785 #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
787 fn evm_for_ctx(&self) -> PrewarmEvmState<Evm> {
788 let mut state_provider = match self.provider.database_provider_ro() {
789 Ok(provider) => Box::new(provider.into_evm_state_provider()) as EvmStateProviderBox,
790 Err(err) => {
791 trace!(
792 target: "engine::tree::payload_processor::prewarm",
793 %err,
794 "Failed to build state provider in prewarm thread"
795 );
796 return PrewarmEvmState { evm: None, recording: RecordingHandle::default() }
797 }
798 };
799
800 if let Some(saved_cache) = &self.saved_cache {
802 let caches = saved_cache.cache().clone();
803 state_provider = Box::new(
804 CachedStateProvider::new_prewarm(state_provider, caches)
805 .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
806 );
807 }
808
809 let evm_env =
810 self.env.evm_env.clone().with_nonce_check_disabled().with_balance_check_disabled();
811
812 let recording = RecordingHandle::default();
813 let config = if self.prewarm_results.is_some() {
814 self.evm_config.clone().with_jit_support_enabled(false)
815 } else {
816 self.evm_config.clone()
817 };
818 let db = StateProviderDatabase::new(state_provider);
819 let evm = if self.prewarm_results.is_some() {
820 self.create_evm(&config, RecordingDb::new(db, recording.clone()), evm_env)
821 } else {
822 self.create_evm(&config, db, evm_env)
823 };
824 PrewarmEvmState { evm: Some(evm), recording }
825 }
826
827 fn create_evm(
831 &self,
832 config: &Evm,
833 db: impl Database + 'static,
834 evm_env: EvmEnvFor<Evm>,
835 ) -> EvmFor<'static, Evm> {
836 if self.precompile_cache_disabled {
837 config.evm_with_env(db, evm_env)
838 } else {
839 config.evm_with_precompile_cache(db, evm_env, self.precompile_cache_map.clone(), false)
840 }
841 }
842
843 #[inline]
845 pub fn should_stop(&self) -> bool {
846 self.terminate_execution.load(Ordering::Relaxed)
847 }
848
849 #[inline]
851 pub fn stop(&self) {
852 self.terminate_execution.store(true, Ordering::Relaxed);
853 }
854
855 fn send_bal_hashed_state(
865 &self,
866 parent_span: &Span,
867 provider: &mut Option<EvmStateProviderBox>,
868 account_changes: &alloy_eip7928::AccountChanges,
869 hashed_update_stream: &StateRootUpdateStream,
870 ) {
871 if self.disable_bal_parallel_state_root {
872 return;
873 }
874 let address = account_changes.address;
875 let mut hashed_address = None;
876 let account_info = account_changes.account_info();
877
878 if !account_info.changes_state_root(account_changes) {
879 return;
880 }
881
882 if account_changes.has_storage_changes() {
886 let hashed_address = *hashed_address.get_or_insert_with(|| keccak256(address));
887 let storage_map = reth_trie::HashedStorage::from_iter(
888 account_changes
889 .storage_post_states()
890 .map(|(slot, value)| (keccak256(slot.to_be_bytes::<32>()), value)),
891 );
892
893 let mut hashed_state = reth_trie::HashedPostState::default();
894 hashed_state.storages.insert(hashed_address, storage_map);
895 hashed_update_stream.on_hashed_state_update(hashed_state);
896 }
897
898 let existing_account = if account_info.is_complete() {
899 None
900 } else {
901 if provider.is_none() {
902 let _span = debug_span!(
903 target: "engine::tree::payload_processor::prewarm",
904 parent: parent_span,
905 "bal_hashed_state_provider_init",
906 has_saved_cache = !self.disable_bal_batch_io && self.saved_cache.is_some(),
907 )
908 .entered();
909
910 let inner = match self.provider.database_provider_ro() {
911 Ok(p) => p.into_evm_state_provider(),
912 Err(err) => {
913 warn!(
914 target: "engine::tree::payload_processor::prewarm",
915 ?err,
916 "Failed to build provider for BAL account reads"
917 );
918 return;
919 }
920 };
921 let boxed: EvmStateProviderBox =
922 match (self.disable_bal_batch_io, &self.saved_cache) {
923 (false, Some(saved)) => {
924 let caches = saved.cache().clone();
925 Box::new(
926 CachedStateProvider::new_prewarm(inner, caches)
927 .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
928 )
929 }
930 _ => Box::new(inner),
931 };
932 *provider = Some(boxed);
933 }
934 let account_reader = provider.as_ref().expect("provider just initialized");
935 account_reader.basic_account(&address).ok().flatten()
936 };
937
938 let mut account = existing_account.unwrap_or_default();
939 account.apply_bal_info(account_info);
940 let hashed_address = hashed_address.unwrap_or_else(|| keccak256(address));
941
942 let account = (!account.is_empty()).then_some(account);
954
955 let mut hashed_state = reth_trie::HashedPostState::default();
956 hashed_state.accounts.insert(hashed_address, account);
957 hashed_update_stream.on_hashed_state_update(hashed_state);
958 }
959}
960
961fn multiproof_targets_from_withdrawals(withdrawals: &[Withdrawal]) -> MultiProofTargetsV2 {
966 MultiProofTargetsV2 {
967 account_targets: withdrawals.iter().map(|w| keccak256(w.address).into()).collect(),
968 ..Default::default()
969 }
970}
971
972#[derive(Debug)]
977pub enum PrewarmTaskEvent<R> {
978 TerminateTransactionExecution,
984 Terminate {
992 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
996 valid_block_rx: mpsc::Receiver<()>,
1001 },
1002 FinishedTxExecution {
1005 executed_transactions: usize,
1007 },
1008}
1009
1010#[derive(Metrics, Clone)]
1012#[metrics(scope = "sync.prewarm")]
1013pub struct PrewarmMetrics {
1014 pub(crate) transactions: Gauge,
1016 pub(crate) transactions_histogram: Histogram,
1018 pub(crate) total_runtime: Histogram,
1020 pub(crate) execution_duration: Histogram,
1022 pub(crate) prefetch_storage_targets: Histogram,
1024 pub(crate) cache_saving_duration: Gauge,
1027 pub(crate) transaction_errors: Counter,
1029 pub(crate) seeded_transactions: Counter,
1032 pub(crate) bal_slot_iteration_duration: Histogram,
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038
1039 use super::*;
1040 use crate::tree::payload_processor::{CacheTaskHandle, PayloadHandle};
1041 use alloy_consensus::{transaction::Recovered, TxLegacy};
1042 use alloy_eips::eip7702::constants::EIP7702_CLEARED_DELEGATION;
1043 use alloy_primitives::{Address, Signature, TxKind, B256, U256};
1044 use evm2::{evm::EmptyDB, BaseEvmTypes};
1045 use reth_chainspec::ChainSpec;
1046 use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
1047 use reth_evm::{execute::WithTxEnv, TxEnvFor};
1048 use reth_evm_ethereum::EthEvmConfig;
1049 use reth_provider::test_utils::MockEthProvider;
1050 use reth_stages_api::{StageCheckpoint, StageId};
1051 use reth_storage_overlay::OverlayManager;
1052 use reth_tasks::WorkerPool;
1053 use reth_trie_common::bundle_state::{AccountStatus, BundleAccount, BundleState};
1054 use std::time::Duration;
1055
1056 #[test]
1057 fn terminate_event_stops_transaction_execution() {
1058 let terminate_execution = Arc::new(AtomicBool::new(false));
1059 let ctx = PrewarmContext {
1060 env: ExecutionEnv::test_default(),
1061 evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1062 saved_cache: None,
1063 provider: OverlayStateProviderFactory::new(
1064 MockEthProvider::default(),
1065 OverlayManager::default().overlay_builder(B256::ZERO),
1066 ),
1067 bal_prewarm_pool: None,
1068 metrics: PrewarmMetrics::default(),
1069 cache_metrics: None,
1070 cache_state_metrics: None,
1071 terminate_execution: Arc::clone(&terminate_execution),
1072 executed_tx_index: Arc::new(AtomicUsize::new(0)),
1073 precompile_cache_disabled: false,
1074 precompile_cache_map: PrecompileCacheMap::default(),
1075 disable_bal_parallel_state_root: false,
1076 disable_bal_batch_io: false,
1077 prewarm_results: None,
1078 };
1079 let (task, actions_tx) =
1080 PrewarmCacheTask::new(Runtime::test(), PayloadExecutionCache::default(), ctx);
1081 actions_tx
1082 .send(PrewarmTaskEvent::Terminate {
1083 execution_outcome: None,
1084 valid_block_rx: mpsc::channel().1,
1085 })
1086 .unwrap();
1087
1088 task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
1089 PrewarmMode::Skipped,
1090 actions_tx,
1091 );
1092
1093 assert!(terminate_execution.load(Ordering::Relaxed));
1094 }
1095
1096 #[test]
1097 fn rebuilds_evm_for_each_prewarm_run() {
1098 let caller = Address::repeat_byte(1);
1099 let provider = MockEthProvider::default();
1100 provider.enable_database_provider();
1101 provider.add_header(B256::ZERO, Default::default());
1102 provider.add_stage_checkpoint(StageId::Finish, StageCheckpoint::new(0));
1103 let tx = Recovered::new_unchecked(
1104 TransactionSigned::new_unhashed(
1105 TxLegacy {
1107 gas_limit: 21_000,
1108 to: TxKind::Call(Address::repeat_byte(2)),
1109 ..Default::default()
1110 }
1111 .into(),
1112 Signature::test_signature(),
1113 ),
1114 caller,
1115 );
1116 let pool = Arc::new(WorkerPool::new(1, "prewarm-test"));
1117 let prewarm = |beneficiary| {
1119 let mut env = ExecutionEnv::<EthEvmConfig>::test_default();
1120 env.evm_env.block.beneficiary = beneficiary;
1121 let results = Arc::new(PrewarmResults::new(1));
1122 let ctx = PrewarmContext {
1123 env,
1124 evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1125 saved_cache: None,
1126 provider: OverlayStateProviderFactory::new(
1127 provider.clone(),
1128 OverlayManager::default().overlay_builder(B256::ZERO),
1129 ),
1130 bal_prewarm_pool: None,
1131 metrics: PrewarmMetrics::default(),
1132 cache_metrics: None,
1133 cache_state_metrics: None,
1134 terminate_execution: Arc::new(AtomicBool::new(false)),
1135 executed_tx_index: Arc::new(AtomicUsize::new(0)),
1136 precompile_cache_disabled: false,
1137 precompile_cache_map: PrecompileCacheMap::default(),
1138 disable_bal_parallel_state_root: false,
1139 disable_bal_batch_io: false,
1140 prewarm_results: Some(Arc::clone(&results)),
1141 };
1142 let tx =
1143 WithTxEnv { tx_env: ctx.evm_config.tx_env(tx.clone()), tx: Arc::new(tx.clone()) };
1144 let stateful_pool = WorkerStatefulPool::new(Arc::clone(&pool));
1145 stateful_pool.install_fn(|| {
1146 PrewarmCacheTask::transact_worker(&ctx, &stateful_pool, 0, tx, None, None)
1147 });
1148 results.take_valid(0, &mut EmptyDB::default()).unwrap().unwrap().pending_state
1149 };
1150
1151 assert!(prewarm(Address::repeat_byte(0xa))
1152 .account_changes()
1153 .any(|a| a.address == Address::repeat_byte(0xa)));
1154 let state = prewarm(Address::repeat_byte(0xb));
1155 assert!(state.account_changes().any(|a| a.address == Address::repeat_byte(0xb)));
1156 assert!(!state.account_changes().any(|a| a.address == Address::repeat_byte(0xa)));
1157 }
1158
1159 #[test]
1160 fn transaction_prewarm_releases_worker_cache_before_cache_task_finishes() {
1161 let runtime = Runtime::test();
1162 let saved = SavedCache::new(B256::ZERO, crate::tree::ExecutionCache::new(1_000));
1163 let mut ctx = test_prewarm_context(saved.clone(), Gauge::noop());
1164 let provider = MockEthProvider::default();
1165 provider.enable_database_provider();
1166 provider.add_header(B256::ZERO, Default::default());
1167 provider.add_stage_checkpoint(StageId::Finish, StageCheckpoint::new(0));
1168 ctx.provider = OverlayStateProviderFactory::new(
1169 provider,
1170 OverlayManager::default().overlay_builder(B256::ZERO),
1171 );
1172 assert!(ctx.evm_for_ctx().evm.is_some());
1173 let tx = Recovered::new_unchecked(
1174 TransactionSigned::new_unhashed(
1175 TxLegacy {
1176 gas_limit: 21_000,
1177 to: TxKind::Call(Address::repeat_byte(2)),
1178 ..Default::default()
1179 }
1180 .into(),
1181 Signature::test_signature(),
1182 ),
1183 Address::repeat_byte(1),
1184 );
1185 let tx = WithTxEnv { tx_env: ctx.evm_config.tx_env(tx.clone()), tx: Arc::new(tx) };
1186 let (task, actions_tx) =
1187 PrewarmCacheTask::new(runtime, PayloadExecutionCache::default(), ctx);
1188 let (pending_tx, pending_rx) = mpsc::channel();
1189 pending_tx.send((0, tx)).unwrap();
1190 drop(pending_tx);
1191 task.spawn_txs_prewarm(pending_rx, actions_tx, None);
1192 assert!(matches!(
1193 task.actions_rx.recv_timeout(Duration::from_secs(5)).unwrap(),
1194 PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 1 }
1195 ));
1196
1197 let deadline = Instant::now() + Duration::from_secs(5);
1200 while saved.usage_count() != 2 {
1201 assert!(Instant::now() < deadline, "worker state outlived transaction prewarming");
1202 std::thread::yield_now();
1203 }
1204 assert!(task.ctx.saved_cache.is_some());
1205 }
1206
1207 fn test_prewarm_context(
1208 saved_cache: SavedCache,
1209 saving_duration: Gauge,
1210 ) -> PrewarmContext<EthPrimitives, MockEthProvider, EthEvmConfig> {
1211 PrewarmContext {
1212 env: ExecutionEnv { hash: B256::repeat_byte(2), ..ExecutionEnv::test_default() },
1213 evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1214 saved_cache: Some(saved_cache),
1215 provider: OverlayStateProviderFactory::new(
1216 MockEthProvider::default(),
1217 OverlayManager::default().overlay_builder(B256::ZERO),
1218 ),
1219 bal_prewarm_pool: None,
1220 metrics: PrewarmMetrics {
1221 cache_saving_duration: saving_duration,
1222 ..Default::default()
1223 },
1224 cache_metrics: None,
1225 cache_state_metrics: None,
1226 terminate_execution: Arc::new(AtomicBool::new(false)),
1227 executed_tx_index: Arc::new(AtomicUsize::new(0)),
1228 precompile_cache_disabled: false,
1229 precompile_cache_map: PrecompileCacheMap::default(),
1230 disable_bal_parallel_state_root: false,
1231 disable_bal_batch_io: false,
1232 prewarm_results: None,
1233 }
1234 }
1235
1236 fn save_test_cache(
1237 runtime: &Runtime,
1238 execution_cache: &PayloadExecutionCache,
1239 saved_cache: SavedCache,
1240 state: reth_trie_common::bundle_state::BundleState,
1241 valid: bool,
1242 saving_duration: Gauge,
1243 ) {
1244 let ctx = test_prewarm_context(saved_cache, saving_duration);
1245 let (task, _) = PrewarmCacheTask::new(runtime.clone(), execution_cache.clone(), ctx);
1246 let (valid_tx, valid_rx) = mpsc::channel();
1247 if valid {
1248 valid_tx.send(()).unwrap();
1249 }
1250 drop(valid_tx);
1251 task.save_cache(
1252 Arc::new(BlockExecutionOutput { state, result: Default::default() }),
1253 valid_rx,
1254 );
1255 }
1256
1257 struct CacheSaveObserver {
1260 cache: PayloadExecutionCache,
1261 observed: Arc<AtomicBool>,
1262 }
1263
1264 impl metrics::GaugeFn for CacheSaveObserver {
1265 fn increment(&self, _: f64) {}
1266 fn decrement(&self, _: f64) {}
1267 fn set(&self, _: f64) {
1268 assert!(
1269 self.cache.get_cache_for(B256::repeat_byte(2)).is_some(),
1270 "saved cache must be available before the saving task returns"
1271 );
1272 self.observed.store(true, Ordering::Relaxed);
1273 }
1274 }
1275
1276 #[test]
1277 fn save_cache_releases_warm_cache_before_duration_metric() {
1278 let runtime = Runtime::test();
1279 let execution_cache = PayloadExecutionCache::default();
1280 let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1281 let address = Address::with_last_byte(1);
1282 saved.cache().insert_storage(address, B256::ZERO, Some(U256::from(7)));
1283 execution_cache.update_with_guard(|slot| *slot = Some(saved.clone()));
1284 let (release_tx, release_rx) = mpsc::channel::<()>();
1286 runtime.spawn_blocking_named("drop", move || {
1287 let _ = release_rx.recv();
1288 });
1289 let observed = Arc::new(AtomicBool::new(false));
1290 let observer = Arc::new(CacheSaveObserver {
1291 cache: execution_cache.clone(),
1292 observed: observed.clone(),
1293 });
1294 save_test_cache(
1295 &runtime,
1296 &execution_cache,
1297 saved,
1298 Default::default(),
1299 true,
1300 Gauge::from_arc(observer),
1301 );
1302 assert!(observed.load(Ordering::Relaxed), "save duration was not recorded");
1303 let saved = execution_cache.get_cache_for(B256::repeat_byte(2)).unwrap();
1304 assert_eq!(
1305 saved.cache().get_or_try_insert_storage_with(address, B256::ZERO, || Err(())),
1306 Ok(reth_execution_cache::CachedStatus::Cached(U256::from(7))),
1307 "handoff must preserve the warmed contents",
1308 );
1309 drop(release_tx);
1310 }
1311
1312 #[test]
1313 fn save_cache_blocks_reuse_while_execution_cache_is_cloned() {
1314 let runtime = Runtime::test();
1315 let execution_cache = PayloadExecutionCache::default();
1316 let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1317 execution_cache.update_with_guard(|slot| *slot = Some(saved.clone()));
1318 let other_cache_clone = saved.cache().clone();
1319
1320 save_test_cache(&runtime, &execution_cache, saved, Default::default(), true, Gauge::noop());
1321
1322 assert!(execution_cache.get_cache_for(B256::repeat_byte(2)).is_none());
1323 drop(other_cache_clone);
1324 assert!(execution_cache.get_cache_for(B256::repeat_byte(2)).is_some());
1325 }
1326
1327 #[derive(Clone, Copy, Debug)]
1328 enum CacheSlot {
1329 Empty,
1330 Shared,
1331 Distinct,
1332 }
1333
1334 struct CacheDropProbe {
1336 started: Sender<std::thread::ThreadId>,
1337 inspected: Receiver<()>,
1338 result: Sender<bool>,
1339 }
1340
1341 impl AsRef<[u8]> for CacheDropProbe {
1342 fn as_ref(&self) -> &[u8] {
1343 &EIP7702_CLEARED_DELEGATION
1344 }
1345 }
1346
1347 impl Drop for CacheDropProbe {
1348 fn drop(&mut self) {
1349 let _ = self.started.send(std::thread::current().id());
1350 let unlocked = self.inspected.recv_timeout(std::time::Duration::from_secs(5)).is_ok();
1352 let _ = self.result.send(unlocked);
1353 }
1354 }
1355
1356 fn observe_cache_drop(
1357 saved: &SavedCache,
1358 execution_cache: &PayloadExecutionCache,
1359 expect_saved_cache: bool,
1360 ) -> (Receiver<bool>, std::thread::JoinHandle<std::thread::ThreadId>) {
1361 let (started_tx, started_rx) = mpsc::channel();
1362 let (inspected_tx, inspected_rx) = mpsc::channel();
1363 let (result_tx, result_rx) = mpsc::channel();
1364 let cache = execution_cache.clone();
1365 let reader = std::thread::spawn(move || {
1366 let drop_thread = started_rx.recv().unwrap();
1367 cache.wait_for_availability();
1369 assert_eq!(cache.get_cache_for(B256::repeat_byte(2)).is_some(), expect_saved_cache);
1370 cache.update_with_guard(|slot| assert_eq!(slot.is_some(), expect_saved_cache));
1371 let _ = inspected_tx.send(());
1372 drop_thread
1373 });
1374 let bytes = alloy_primitives::bytes::Bytes::from_owner(CacheDropProbe {
1375 started: started_tx,
1376 inspected: inspected_rx,
1377 result: result_tx,
1378 });
1379 let code = evm2::bytecode::Bytecode::new_eip7702_raw(bytes.into()).unwrap();
1380 saved
1381 .cache()
1382 .insert_code(B256::repeat_byte(3), Some(reth_primitives_traits::Bytecode(code)));
1383 (result_rx, reader)
1384 }
1385
1386 #[test]
1387 fn save_cache_freeing_waits_for_validator_cache_references() {
1388 let runtime = Runtime::test();
1389 let execution_cache = PayloadExecutionCache::default();
1390 let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1391 execution_cache.update_with_guard(|cached| *cached = Some(saved.clone()));
1392 let (dropped, reader) = observe_cache_drop(&saved, &execution_cache, false);
1393 let ctx = test_prewarm_context(saved, Gauge::noop());
1394 let (task, actions_tx) =
1395 PrewarmCacheTask::new(runtime.clone(), execution_cache.clone(), ctx);
1396 let mut payload = PayloadHandle {
1397 prewarm_handle: CacheTaskHandle::<_, BaseEvmTypes> {
1398 saved_cache: task.ctx.saved_cache.clone(),
1399 to_prewarm_task: Some(actions_tx.clone()),
1400 executed_tx_index: task.ctx.executed_tx_index.clone(),
1401 prewarm_results: None,
1402 cache_metrics: None,
1403 },
1404 transactions: crossbeam_channel::never::<(usize, Result<(), ()>)>(),
1405 _span: Span::none(),
1406 };
1407 let validator_cache = payload.caches().unwrap();
1409 let valid_block_tx = payload.terminate_caching(Some(Arc::new(BlockExecutionOutput {
1410 state: Default::default(),
1411 result: Default::default(),
1412 })));
1413 drop(valid_block_tx);
1414 let prewarm = runtime.spawn_blocking_named("prewarm", move || {
1415 task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
1416 PrewarmMode::Skipped,
1417 actions_tx,
1418 );
1419 std::thread::current().id()
1420 });
1421 let prewarm_thread = *prewarm.get();
1422
1423 execution_cache.update_with_guard(|cached| assert!(cached.is_none()));
1424 assert_eq!(payload.prewarm_handle.saved_cache.as_ref().unwrap().usage_count(), 2);
1425 assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Empty)));
1426 drop(payload);
1427 assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Empty)));
1428
1429 drop(validator_cache);
1430 assert!(dropped.try_recv().expect("last ExecutionCache drop must free the cache"));
1431 let drop_thread = reader.join().unwrap();
1432 assert_eq!(drop_thread, std::thread::current().id());
1433 assert_ne!(drop_thread, prewarm_thread);
1434 }
1435
1436 fn assert_save_cache_drops_removed_caches(slot: CacheSlot, valid: bool, insert_error: bool) {
1437 let runtime = Runtime::test();
1438 let execution_cache = PayloadExecutionCache::default();
1439 let cache_to_save =
1440 SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1441 let distinct_previous = matches!(slot, CacheSlot::Distinct).then(|| {
1442 SavedCache::new(B256::repeat_byte(2), crate::tree::ExecutionCache::new(1_000))
1444 });
1445 execution_cache.update_with_guard(|cached| {
1446 *cached = match slot {
1447 CacheSlot::Empty => None,
1448 CacheSlot::Shared => Some(cache_to_save.clone()),
1449 CacheSlot::Distinct => distinct_previous.clone(),
1450 };
1451 });
1452 let expect_saved_cache = valid && !insert_error;
1453 let mut drops = Vec::new();
1454 if let Some(previous) = &distinct_previous {
1455 drops.push(observe_cache_drop(previous, &execution_cache, expect_saved_cache));
1456 }
1457 if !expect_saved_cache {
1458 drops.push(observe_cache_drop(&cache_to_save, &execution_cache, expect_saved_cache));
1459 }
1460 drop(distinct_previous);
1462
1463 let (release_tx, release_rx) = mpsc::channel::<()>();
1465 runtime.spawn_blocking_named("drop", move || {
1466 let _ = release_rx.recv();
1467 });
1468
1469 let mut state = BundleState::default();
1470 if insert_error {
1471 state.state.insert(
1473 Address::with_last_byte(1),
1474 BundleAccount::new(None, None, Default::default(), AccountStatus::Changed),
1475 );
1476 }
1477 save_test_cache(&runtime, &execution_cache, cache_to_save, state, valid, Gauge::noop());
1478
1479 for (result, reader) in drops {
1480 assert!(
1481 result.try_recv().expect("removed cache must be destroyed before save returns"),
1482 "cache mutex must be unlocked during destruction"
1483 );
1484 assert_eq!(reader.join().unwrap(), std::thread::current().id());
1485 }
1486 execution_cache.update_with_guard(|slot| {
1487 if expect_saved_cache {
1488 let saved = slot.as_ref().expect("valid cache saved");
1489 assert_eq!(saved.executed_block_hash(), B256::repeat_byte(2));
1490 } else {
1491 assert!(slot.is_none(), "polluted cache must be removed");
1492 }
1493 });
1494 assert_eq!(
1495 execution_cache.get_cache_for(B256::repeat_byte(2)).is_some(),
1496 expect_saved_cache
1497 );
1498 drop(release_tx);
1499 }
1500
1501 #[test]
1502 fn save_cache_drops_replaced_allocation_after_unlock() {
1503 assert_save_cache_drops_removed_caches(CacheSlot::Distinct, true, false);
1504 }
1505
1506 #[test]
1507 fn save_cache_drops_invalid_allocations_after_unlock() {
1508 assert_save_cache_drops_removed_caches(CacheSlot::Distinct, false, false);
1509 }
1510
1511 #[test]
1512 fn save_cache_drops_allocations_after_unlock_on_insert_error() {
1513 assert_save_cache_drops_removed_caches(CacheSlot::Distinct, true, true);
1514 }
1515
1516 #[test]
1517 fn save_cache_drops_shared_allocation_after_unlock_on_invalid_block() {
1518 assert_save_cache_drops_removed_caches(CacheSlot::Shared, false, false);
1519 }
1520
1521 #[test]
1522 fn save_cache_drops_shared_allocation_after_unlock_on_insert_error() {
1523 assert_save_cache_drops_removed_caches(CacheSlot::Shared, true, true);
1524 }
1525
1526 #[test]
1527 fn save_cache_handles_empty_slot() {
1528 for (valid, insert_error) in [(true, false), (false, false), (true, true)] {
1529 assert_save_cache_drops_removed_caches(CacheSlot::Empty, valid, insert_error);
1530 }
1531 }
1532}