Skip to main content

sui_core/execution_cache/
writeback_cache.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! MemoryCache is a cache for the transaction execution which delays writes to the database until
5//! transaction results are certified (i.e. they appear in a certified checkpoint, or an effects cert
6//! is observed by a fullnode). The cache also stores committed data in memory in order to serve
7//! future reads without hitting the database.
8//!
9//! For storing uncommitted transaction outputs, we cannot evict the data at all until it is written
10//! to disk. Committed data not only can be evicted, but it is also unbounded (imagine a stream of
11//! transactions that keep splitting a coin into smaller coins).
12//!
13//! We also want to be able to support negative cache hits (i.e. the case where we can determine an
14//! object does not exist without hitting the database).
15//!
16//! To achieve both of these goals, we split the cache data into two pieces, a dirty set and a cached
17//! set. The dirty set has no automatic evictions, data is only removed after being committed. The
18//! cached set is in a bounded-sized cache with automatic evictions. In order to support negative
19//! cache hits, we treat the two halves of the cache as FIFO queue. Newly written (dirty) versions are
20//! inserted to one end of the dirty queue. As versions are committed to disk, they are
21//! removed from the other end of the dirty queue and inserted into the cache queue. The cache queue
22//! is truncated if it exceeds its maximum size, by removing all but the N newest versions.
23//!
24//! This gives us the property that the sequence of versions in the dirty and cached queues are the
25//! most recent versions of the object, i.e. there can be no "gaps". This allows for the following:
26//!
27//!   - Negative cache hits: If the queried version is not in memory, but is higher than the smallest
28//!     version in the cached queue, it does not exist in the db either.
29//!   - Bounded reads: When reading the most recent version that is <= some version bound, we can
30//!     correctly satisfy this query from the cache, or determine that we must go to the db.
31//!
32//! Note that at any time, either or both the dirty or the cached queue may be non-existent. There may be no
33//! dirty versions of the objects, in which case there will be no dirty queue. And, the cached queue
34//! may be evicted from the cache, in which case there will be no cached queue. Because only the cached
35//! queue can be evicted (the dirty queue can only become empty by moving versions from it to the cached
36//! queue), the "highest versions" property still holds in all cases.
37//!
38//! The above design is used for both objects and markers.
39
40use crate::accumulators::funds_read::AccountFundsRead;
41use crate::authority::AuthorityStore;
42use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
43use crate::authority::authority_store::ExecutionLockWriteGuard;
44#[cfg(test)]
45use crate::authority::authority_store::{LockDetailsDeprecated, ObjectLockStatus, SuiLockResult};
46use crate::authority::authority_store_tables::LiveObject;
47use crate::authority::backpressure::BackpressureManager;
48use crate::authority::epoch_start_configuration::{EpochFlag, EpochStartConfiguration};
49use crate::fallback_fetch::do_fallback_lookup;
50use crate::global_state_hasher::GlobalStateHashStore;
51use crate::transaction_outputs::TransactionOutputs;
52
53use dashmap::DashMap;
54use dashmap::mapref::entry::Entry as DashMapEntry;
55use futures::{FutureExt, future::BoxFuture};
56use moka::sync::SegmentedCache as MokaCache;
57use mysten_common::ZipDebugEqIteratorExt;
58use mysten_common::debug_fatal;
59use mysten_common::random_util::randomize_cache_capacity_in_tests;
60use mysten_common::sync::notify_read::NotifyRead;
61use parking_lot::Mutex;
62use rayon::prelude::*;
63use std::collections::{BTreeMap, HashSet};
64use std::hash::Hash;
65use std::sync::Arc;
66use std::sync::atomic::AtomicU64;
67use std::time::Instant;
68use sui_config::ExecutionCacheConfig;
69use sui_macros::fail_point;
70use sui_protocol_config::ProtocolVersion;
71use sui_types::SUI_ACCUMULATOR_ROOT_OBJECT_ID;
72use sui_types::accumulator_event::AccumulatorEvent;
73use sui_types::accumulator_root::{AccumulatorObjId, AccumulatorValue};
74use sui_types::base_types::{
75    ConsensusObjectVersion, EpochId, FullObjectID, ObjectID, ObjectRef, SequenceNumber,
76    VerifiedExecutionData,
77};
78use sui_types::bridge::{Bridge, get_bridge};
79use sui_types::digests::{ObjectDigest, TransactionDigest, TransactionEffectsDigest};
80use sui_types::effects::{TransactionEffects, TransactionEvents};
81#[cfg(test)]
82use sui_types::error::SuiError;
83use sui_types::error::{SuiErrorKind, SuiResult, UserInputError};
84use sui_types::executable_transaction::VerifiedExecutableTransaction;
85use sui_types::global_state_hash::GlobalStateHash;
86use sui_types::message_envelope::Message;
87use sui_types::messages_checkpoint::CheckpointSequenceNumber;
88use sui_types::object::Object;
89use sui_types::storage::{
90    FullObjectKey, InputKey, MarkerValue, ObjectKey, ObjectOrTombstone, ObjectStore, PackageObject,
91};
92use sui_types::sui_system_state::{SuiSystemState, get_sui_system_state};
93use sui_types::transaction::{TransactionDataAPI, VerifiedTransaction};
94use tap::TapOptional;
95use tracing::{debug, info, instrument, trace, warn};
96
97use super::ExecutionCacheAPI;
98use super::cache_types::Ticket;
99use super::{
100    Batch, CheckpointCache, ExecutionCacheCommit, ExecutionCacheMetrics, ExecutionCacheReconfigAPI,
101    ExecutionCacheWrite, ObjectCacheRead, StateSyncAPI, TestingAPI, TransactionCacheRead,
102    cache_types::{CacheResult, CachedVersionMap, IsNewer, MonotonicCache},
103    implement_passthrough_traits,
104    object_locks::ObjectLocks,
105};
106
107#[cfg(test)]
108#[path = "unit_tests/writeback_cache_tests.rs"]
109pub mod writeback_cache_tests;
110
111#[cfg(test)]
112#[path = "unit_tests/notify_read_input_objects_tests.rs"]
113mod notify_read_input_objects_tests;
114
115#[derive(Clone, PartialEq, Eq)]
116enum ObjectEntry {
117    Object(Object),
118    Deleted,
119    Wrapped,
120}
121
122impl ObjectEntry {
123    #[cfg(test)]
124    fn unwrap_object(&self) -> &Object {
125        match self {
126            ObjectEntry::Object(o) => o,
127            _ => panic!("unwrap_object called on non-Object"),
128        }
129    }
130
131    fn is_tombstone(&self) -> bool {
132        match self {
133            ObjectEntry::Deleted | ObjectEntry::Wrapped => true,
134            ObjectEntry::Object(_) => false,
135        }
136    }
137}
138
139impl std::fmt::Debug for ObjectEntry {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            ObjectEntry::Object(o) => {
143                write!(f, "ObjectEntry::Object({:?})", o.compute_object_reference())
144            }
145            ObjectEntry::Deleted => write!(f, "ObjectEntry::Deleted"),
146            ObjectEntry::Wrapped => write!(f, "ObjectEntry::Wrapped"),
147        }
148    }
149}
150
151impl From<Object> for ObjectEntry {
152    fn from(object: Object) -> Self {
153        ObjectEntry::Object(object)
154    }
155}
156
157impl From<ObjectOrTombstone> for ObjectEntry {
158    fn from(object: ObjectOrTombstone) -> Self {
159        match object {
160            ObjectOrTombstone::Object(o) => o.into(),
161            ObjectOrTombstone::Tombstone(obj_ref) => {
162                if obj_ref.2.is_deleted() {
163                    ObjectEntry::Deleted
164                } else if obj_ref.2.is_wrapped() {
165                    ObjectEntry::Wrapped
166                } else {
167                    panic!("tombstone digest must either be deleted or wrapped");
168                }
169            }
170        }
171    }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175enum LatestObjectCacheEntry {
176    Object(SequenceNumber, ObjectEntry),
177    NonExistent,
178}
179
180impl LatestObjectCacheEntry {
181    #[cfg(test)]
182    fn version(&self) -> Option<SequenceNumber> {
183        match self {
184            LatestObjectCacheEntry::Object(version, _) => Some(*version),
185            LatestObjectCacheEntry::NonExistent => None,
186        }
187    }
188
189    fn is_alive(&self) -> bool {
190        match self {
191            LatestObjectCacheEntry::Object(_, entry) => !entry.is_tombstone(),
192            LatestObjectCacheEntry::NonExistent => false,
193        }
194    }
195}
196
197impl IsNewer for LatestObjectCacheEntry {
198    fn is_newer_than(&self, other: &LatestObjectCacheEntry) -> bool {
199        match (self, other) {
200            (LatestObjectCacheEntry::Object(v1, _), LatestObjectCacheEntry::Object(v2, _)) => {
201                v1 > v2
202            }
203            (LatestObjectCacheEntry::Object(_, _), LatestObjectCacheEntry::NonExistent) => true,
204            _ => false,
205        }
206    }
207}
208
209type MarkerKey = (EpochId, FullObjectID);
210
211/// UncommittedData stores execution outputs that are not yet written to the db. Entries in this
212/// struct can only be purged after they are committed.
213struct UncommittedData {
214    /// The object dirty set. All writes go into this table first. After we flush the data to the
215    /// db, the data is removed from this table and inserted into the object_cache.
216    ///
217    /// This table may contain both live and dead objects, since we flush both live and dead
218    /// objects to the db in order to support past object queries on fullnodes.
219    ///
220    /// Further, we only remove objects in FIFO order, which ensures that the cached
221    /// sequence of objects has no gaps. In other words, if we have versions 4, 8, 13 of
222    /// an object, we can deduce that version 9 does not exist. This also makes child object
223    /// reads efficient. `object_cache` cannot contain a more recent version of an object than
224    /// `objects`, and neither can have any gaps. Therefore if there is any object <= the version
225    /// bound for a child read in objects, it is the correct object to return.
226    objects: DashMap<ObjectID, CachedVersionMap<ObjectEntry>>,
227
228    // Markers for received objects and deleted shared objects. This contains all of the dirty
229    // marker state, which is committed to the db at the same time as other transaction data.
230    // After markers are committed to the db we remove them from this table and insert them into
231    // marker_cache.
232    markers: DashMap<MarkerKey, CachedVersionMap<MarkerValue>>,
233
234    transaction_effects: DashMap<TransactionEffectsDigest, TransactionEffects>,
235
236    transaction_events: DashMap<TransactionDigest, TransactionEvents>,
237
238    unchanged_loaded_runtime_objects: DashMap<TransactionDigest, Vec<ObjectKey>>,
239
240    executed_effects_digests: DashMap<TransactionDigest, TransactionEffectsDigest>,
241
242    // Transaction outputs that have not yet been written to the DB. Items are removed from this
243    // table as they are flushed to the db.
244    pending_transaction_writes: DashMap<TransactionDigest, Arc<TransactionOutputs>>,
245
246    total_transaction_inserts: AtomicU64,
247    total_transaction_commits: AtomicU64,
248}
249
250impl UncommittedData {
251    fn new() -> Self {
252        Self {
253            objects: DashMap::with_shard_amount(2048),
254            markers: DashMap::with_shard_amount(2048),
255            transaction_effects: DashMap::with_shard_amount(2048),
256            executed_effects_digests: DashMap::with_shard_amount(2048),
257            pending_transaction_writes: DashMap::with_shard_amount(2048),
258            transaction_events: DashMap::with_shard_amount(2048),
259            unchanged_loaded_runtime_objects: DashMap::with_shard_amount(2048),
260            total_transaction_inserts: AtomicU64::new(0),
261            total_transaction_commits: AtomicU64::new(0),
262        }
263    }
264
265    fn clear(&self) {
266        self.objects.clear();
267        self.markers.clear();
268        self.transaction_effects.clear();
269        self.executed_effects_digests.clear();
270        self.pending_transaction_writes.clear();
271        self.transaction_events.clear();
272        self.unchanged_loaded_runtime_objects.clear();
273        self.total_transaction_inserts
274            .store(0, std::sync::atomic::Ordering::Relaxed);
275        self.total_transaction_commits
276            .store(0, std::sync::atomic::Ordering::Relaxed);
277    }
278
279    fn is_empty(&self) -> bool {
280        let empty = self.pending_transaction_writes.is_empty();
281        if empty && cfg!(debug_assertions) {
282            assert!(
283                self.objects.is_empty()
284                    && self.markers.is_empty()
285                    && self.transaction_effects.is_empty()
286                    && self.executed_effects_digests.is_empty()
287                    && self.transaction_events.is_empty()
288                    && self.unchanged_loaded_runtime_objects.is_empty()
289                    && self
290                        .total_transaction_inserts
291                        .load(std::sync::atomic::Ordering::Relaxed)
292                        == self
293                            .total_transaction_commits
294                            .load(std::sync::atomic::Ordering::Relaxed),
295            );
296        }
297        empty
298    }
299}
300
301// Point items (anything without a version number) can be negatively cached as None
302type PointCacheItem<T> = Option<T>;
303
304// PointCacheItem can only be used for insert-only collections, so a Some entry
305// is always newer than a None entry.
306impl<T: Eq + std::fmt::Debug> IsNewer for PointCacheItem<T> {
307    fn is_newer_than(&self, other: &PointCacheItem<T>) -> bool {
308        match (self, other) {
309            (Some(_), None) => true,
310
311            (Some(a), Some(b)) => {
312                // conflicting inserts should never happen
313                debug_assert_eq!(a, b);
314                false
315            }
316
317            _ => false,
318        }
319    }
320}
321
322/// CachedData stores data that has been committed to the db, but is likely to be read soon.
323struct CachedCommittedData {
324    // See module level comment for an explanation of caching strategy.
325    object_cache: MokaCache<ObjectID, Arc<Mutex<CachedVersionMap<ObjectEntry>>>>,
326
327    // See module level comment for an explanation of caching strategy.
328    marker_cache: MokaCache<MarkerKey, Arc<Mutex<CachedVersionMap<MarkerValue>>>>,
329
330    transactions: MonotonicCache<TransactionDigest, PointCacheItem<Arc<VerifiedTransaction>>>,
331
332    transaction_effects:
333        MonotonicCache<TransactionEffectsDigest, PointCacheItem<Arc<TransactionEffects>>>,
334
335    transaction_events: MonotonicCache<TransactionDigest, PointCacheItem<Arc<TransactionEvents>>>,
336
337    executed_effects_digests:
338        MonotonicCache<TransactionDigest, PointCacheItem<TransactionEffectsDigest>>,
339
340    transaction_executed_in_last_epoch:
341        MonotonicCache<(EpochId, TransactionDigest), PointCacheItem<()>>,
342
343    // Objects that were read at transaction signing time - allows us to access them again at
344    // execution time with a single lock / hash lookup
345    _transaction_objects: MokaCache<TransactionDigest, Vec<Object>>,
346}
347
348impl CachedCommittedData {
349    fn new(config: &ExecutionCacheConfig) -> Self {
350        let object_cache = MokaCache::builder(8)
351            .max_capacity(randomize_cache_capacity_in_tests(
352                config.object_cache_size(),
353            ))
354            .build();
355        let marker_cache = MokaCache::builder(8)
356            .max_capacity(randomize_cache_capacity_in_tests(
357                config.marker_cache_size(),
358            ))
359            .build();
360
361        let transactions = MonotonicCache::new(randomize_cache_capacity_in_tests(
362            config.transaction_cache_size(),
363        ));
364        let transaction_effects = MonotonicCache::new(randomize_cache_capacity_in_tests(
365            config.effect_cache_size(),
366        ));
367        let transaction_events = MonotonicCache::new(randomize_cache_capacity_in_tests(
368            config.events_cache_size(),
369        ));
370        let executed_effects_digests = MonotonicCache::new(randomize_cache_capacity_in_tests(
371            config.executed_effect_cache_size(),
372        ));
373
374        let transaction_objects = MokaCache::builder(8)
375            .max_capacity(randomize_cache_capacity_in_tests(
376                config.transaction_objects_cache_size(),
377            ))
378            .build();
379
380        let transaction_executed_in_last_epoch = MonotonicCache::new(
381            randomize_cache_capacity_in_tests(config.executed_effect_cache_size()),
382        );
383
384        Self {
385            object_cache,
386            marker_cache,
387            transactions,
388            transaction_effects,
389            transaction_events,
390            executed_effects_digests,
391            transaction_executed_in_last_epoch,
392            _transaction_objects: transaction_objects,
393        }
394    }
395
396    fn clear_and_assert_empty(&self) {
397        self.object_cache.invalidate_all();
398        self.marker_cache.invalidate_all();
399        self.transactions.invalidate_all();
400        self.transaction_effects.invalidate_all();
401        self.transaction_events.invalidate_all();
402        self.executed_effects_digests.invalidate_all();
403        self.transaction_executed_in_last_epoch.invalidate_all();
404        self._transaction_objects.invalidate_all();
405
406        assert_empty(&self.object_cache);
407        assert_empty(&self.marker_cache);
408        assert!(self.transactions.is_empty());
409        assert!(self.transaction_effects.is_empty());
410        assert!(self.transaction_events.is_empty());
411        assert!(self.executed_effects_digests.is_empty());
412        assert!(self.transaction_executed_in_last_epoch.is_empty());
413        assert_empty(&self._transaction_objects);
414    }
415}
416
417fn assert_empty<K, V>(cache: &MokaCache<K, V>)
418where
419    K: std::hash::Hash + std::cmp::Eq + std::cmp::PartialEq + Send + Sync + 'static,
420    V: std::clone::Clone + std::marker::Send + std::marker::Sync + 'static,
421{
422    if cache.iter().next().is_some() {
423        panic!("cache should be empty");
424    }
425}
426
427pub struct WritebackCache {
428    dirty: UncommittedData,
429    cached: CachedCommittedData,
430
431    // We separately cache the latest version of each object. Although this seems
432    // redundant, it is the only way to support populating the cache after a read.
433    // We cannot simply insert objects that we read off the disk into `object_cache`,
434    // since that may violate the no-missing-versions property.
435    // `object_by_id_cache` is also written to on writes so that it is always coherent.
436    // Hence it contains both committed and dirty object data.
437    object_by_id_cache: MonotonicCache<ObjectID, LatestObjectCacheEntry>,
438
439    // The packages cache is treated separately from objects, because they are immutable and can be
440    // used by any number of transactions. Additionally, many operations require loading large
441    // numbers of packages (due to dependencies), so we want to try to keep all packages in memory.
442    //
443    // Also, this cache can contain packages that are dirty or committed, so it does not live in
444    // UncachedData or CachedCommittedData. The cache is populated in two ways:
445    // - when packages are written (in which case they will also be present in the dirty set)
446    // - after a cache miss. Because package IDs are unique (only one version exists for each ID)
447    //   we do not need to worry about the contiguous version property.
448    // - note that we removed any unfinalized packages from the cache during revert_state_update().
449    packages: MokaCache<ObjectID, PackageObject>,
450
451    object_locks: ObjectLocks,
452
453    executed_effects_digests_notify_read: NotifyRead<TransactionDigest, TransactionEffectsDigest>,
454    object_notify_read: NotifyRead<InputKey, ()>,
455
456    store: Arc<AuthorityStore>,
457    backpressure_threshold: u64,
458    backpressure_manager: Arc<BackpressureManager>,
459    metrics: Arc<ExecutionCacheMetrics>,
460}
461
462macro_rules! check_cache_entry_by_version {
463    ($self: ident, $table: expr, $level: expr, $cache: expr, $version: expr) => {
464        $self.metrics.record_cache_request($table, $level);
465        if let Some(cache) = $cache {
466            if let Some(entry) = cache.get(&$version) {
467                $self.metrics.record_cache_hit($table, $level);
468                return CacheResult::Hit(entry.clone());
469            }
470
471            if let Some(least_version) = cache.get_least() {
472                if least_version.0 < $version {
473                    // If the version is greater than the least version in the cache, then we know
474                    // that the object does not exist anywhere
475                    $self.metrics.record_cache_negative_hit($table, $level);
476                    return CacheResult::NegativeHit;
477                }
478            }
479        }
480        $self.metrics.record_cache_miss($table, $level);
481    };
482}
483
484macro_rules! check_cache_entry_by_latest {
485    ($self: ident, $table: expr, $level: expr, $cache: expr) => {
486        $self.metrics.record_cache_request($table, $level);
487        if let Some(cache) = $cache {
488            if let Some((version, entry)) = cache.get_highest() {
489                $self.metrics.record_cache_hit($table, $level);
490                return CacheResult::Hit((*version, entry.clone()));
491            } else {
492                panic!("empty CachedVersionMap should have been removed");
493            }
494        }
495        $self.metrics.record_cache_miss($table, $level);
496    };
497}
498
499impl WritebackCache {
500    /// Load an implicitly read system object at the requested version.
501    /// In normal execution, this function can block wait until the object is available at the requested version,
502    /// and it is guaranteed to return an object with the requested version.
503    /// In dry-runs, this function will never block wait, but may return None if the requested version was pruned by this point.
504    pub(crate) fn load_implicitly_read_system_object(
505        &self,
506        object_id: &ObjectID,
507        version: ConsensusObjectVersion,
508    ) -> Option<Object> {
509        assert!(
510            sui_types::IMPLICITLY_READ_SYSTEM_OBJECTS.contains(object_id),
511            "{object_id} is not an implicitly read system object"
512        );
513        let ConsensusObjectVersion {
514            initial_shared_version,
515            version,
516        } = version;
517        if let Some(object) = ObjectCacheRead::get_object_by_key(self, object_id, version) {
518            return Some(object);
519        }
520        self.metrics
521            .implicit_system_object_read_waits
522            .with_label_values(&[object_id.to_string().as_str()])
523            .inc();
524        let wait_start = Instant::now();
525        let key = InputKey::VersionedObject {
526            id: FullObjectID::Consensus((*object_id, initial_shared_version)),
527            version,
528        };
529        // Block wait until the object is available at the requested version.
530        // Note that before blocking, we check if the latest version already passed the requested version,
531        // if so it must imply that we have already produced the requested version.
532        // We are doing this check instead of exact version comparison to handle the rare case during
533        // dry-runs where the requested version was pruned by this point.
534        // Also note that in the case of dry-run, this will never block wait.
535        self.object_notify_read.read_one_blocking(
536            "load_implicitly_read_system_object",
537            &key,
538            |_key| {
539                ObjectCacheRead::get_object(self, object_id)
540                    .is_some_and(|latest| latest.version() >= version)
541                    .then_some(())
542            },
543        );
544        self.metrics
545            .implicit_system_object_read_wait_latency
546            .with_label_values(&[object_id.to_string().as_str()])
547            .observe(wait_start.elapsed().as_secs_f64());
548        ObjectCacheRead::get_object_by_key(self, object_id, version)
549    }
550
551    pub fn new(
552        config: &ExecutionCacheConfig,
553        store: Arc<AuthorityStore>,
554        metrics: Arc<ExecutionCacheMetrics>,
555        backpressure_manager: Arc<BackpressureManager>,
556    ) -> Self {
557        let packages = MokaCache::builder(8)
558            .max_capacity(randomize_cache_capacity_in_tests(
559                config.package_cache_size(),
560            ))
561            .build();
562        Self {
563            dirty: UncommittedData::new(),
564            cached: CachedCommittedData::new(config),
565            object_by_id_cache: MonotonicCache::new(randomize_cache_capacity_in_tests(
566                config.object_by_id_cache_size(),
567            )),
568            packages,
569            object_locks: ObjectLocks::new(),
570            executed_effects_digests_notify_read: NotifyRead::new(),
571            object_notify_read: NotifyRead::new(),
572            store,
573            backpressure_manager,
574            backpressure_threshold: config.backpressure_threshold(),
575            metrics,
576        }
577    }
578
579    pub fn new_for_tests(store: Arc<AuthorityStore>) -> Self {
580        Self::new(
581            &Default::default(),
582            store,
583            ExecutionCacheMetrics::new(&prometheus::Registry::new()).into(),
584            BackpressureManager::new_for_tests(),
585        )
586    }
587
588    #[cfg(test)]
589    pub fn reset_for_test(&mut self) {
590        let mut new = Self::new(
591            &Default::default(),
592            self.store.clone(),
593            self.metrics.clone(),
594            self.backpressure_manager.clone(),
595        );
596        std::mem::swap(self, &mut new);
597    }
598
599    pub fn evict_executed_effects_from_cache_for_testing(&self, tx_digest: &TransactionDigest) {
600        self.cached.executed_effects_digests.invalidate(tx_digest);
601        self.cached.transaction_events.invalidate(tx_digest);
602        self.cached.transactions.invalidate(tx_digest);
603    }
604
605    fn write_object_entry(
606        &self,
607        object_id: &ObjectID,
608        version: SequenceNumber,
609        object: ObjectEntry,
610    ) {
611        trace!(?object_id, ?version, ?object, "inserting object entry");
612        self.metrics.record_cache_write("object");
613
614        // We must hold the lock for the object entry while inserting to the
615        // object_by_id_cache. Otherwise, a surprising bug can occur:
616        //
617        // 1. A thread executing TX1 can write object (O,1) to the dirty set and then pause.
618        // 2. TX2, which reads (O,1) can begin executing, because ExecutionScheduler immediately
619        //    schedules transactions if their inputs are available. It does not matter that TX1
620        //    hasn't finished executing yet.
621        // 3. TX2 can write (O,2) to both the dirty set and the object_by_id_cache.
622        // 4. The thread executing TX1 can resume and write (O,1) to the object_by_id_cache.
623        //
624        // Now, any subsequent attempt to get the latest version of O will return (O,1) instead of
625        // (O,2).
626        //
627        // This seems very unlikely, but it may be possible under the following circumstances:
628        // - While a thread is unlikely to pause for so long, moka cache uses optimistic
629        //   lock-free algorithms that have retry loops. Possibly, under high contention, this
630        //   code might spin for a surprisingly long time.
631        // - Additionally, many concurrent re-executions of the same tx could happen due to
632        //   the tx finalizer, plus checkpoint executor, consensus, and RPCs from fullnodes.
633        let mut entry = self.dirty.objects.entry(*object_id).or_default();
634
635        self.object_by_id_cache
636            .insert(
637                object_id,
638                LatestObjectCacheEntry::Object(version, object.clone()),
639                Ticket::Write,
640            )
641            // While Ticket::Write cannot expire, this insert may still fail.
642            // See the comment in `MonotonicCache::insert`.
643            .ok();
644
645        entry.insert(version, object.clone());
646
647        if let ObjectEntry::Object(object) = &object {
648            if object.is_package() {
649                self.object_notify_read
650                    .notify(&InputKey::Package { id: *object_id }, &());
651            } else if !object.is_child_object() {
652                self.object_notify_read.notify(
653                    &InputKey::VersionedObject {
654                        id: object.full_id(),
655                        version: object.version(),
656                    },
657                    &(),
658                );
659            }
660        }
661    }
662
663    fn write_marker_value(
664        &self,
665        epoch_id: EpochId,
666        object_key: FullObjectKey,
667        marker_value: MarkerValue,
668    ) {
669        tracing::trace!("inserting marker value {object_key:?}: {marker_value:?}",);
670        self.metrics.record_cache_write("marker");
671        self.dirty
672            .markers
673            .entry((epoch_id, object_key.id()))
674            .or_default()
675            .value_mut()
676            .insert(object_key.version(), marker_value);
677        // It is possible for a transaction to use a consensus stream ended
678        // object in the input, hence we must notify that it is now available
679        // at the assigned version, so that any transaction waiting for this
680        // object version can start execution.
681        if matches!(marker_value, MarkerValue::ConsensusStreamEnded(_)) {
682            self.object_notify_read.notify(
683                &InputKey::VersionedObject {
684                    id: object_key.id(),
685                    version: object_key.version(),
686                },
687                &(),
688            );
689        }
690    }
691
692    // lock both the dirty and committed sides of the cache, and then pass the entries to
693    // the callback. Written with the `with` pattern because any other way of doing this
694    // creates lifetime hell.
695    fn with_locked_cache_entries<K, V, R>(
696        dirty_map: &DashMap<K, CachedVersionMap<V>>,
697        cached_map: &MokaCache<K, Arc<Mutex<CachedVersionMap<V>>>>,
698        key: &K,
699        cb: impl FnOnce(Option<&CachedVersionMap<V>>, Option<&CachedVersionMap<V>>) -> R,
700    ) -> R
701    where
702        K: Copy + Eq + Hash + Send + Sync + 'static,
703        V: Send + Sync + 'static,
704    {
705        let dirty_entry = dirty_map.entry(*key);
706        let dirty_entry = match &dirty_entry {
707            DashMapEntry::Occupied(occupied) => Some(occupied.get()),
708            DashMapEntry::Vacant(_) => None,
709        };
710
711        let cached_entry = cached_map.get(key);
712        let cached_lock = cached_entry.as_ref().map(|entry| entry.lock());
713        let cached_entry = cached_lock.as_deref();
714
715        cb(dirty_entry, cached_entry)
716    }
717
718    // Attempt to get an object from the cache. The DB is not consulted.
719    // Can return Hit, Miss, or NegativeHit (if the object is known to not exist).
720    fn get_object_entry_by_key_cache_only(
721        &self,
722        object_id: &ObjectID,
723        version: SequenceNumber,
724    ) -> CacheResult<ObjectEntry> {
725        Self::with_locked_cache_entries(
726            &self.dirty.objects,
727            &self.cached.object_cache,
728            object_id,
729            |dirty_entry, cached_entry| {
730                check_cache_entry_by_version!(
731                    self,
732                    "object_by_version",
733                    "uncommitted",
734                    dirty_entry,
735                    version
736                );
737                check_cache_entry_by_version!(
738                    self,
739                    "object_by_version",
740                    "committed",
741                    cached_entry,
742                    version
743                );
744                CacheResult::Miss
745            },
746        )
747    }
748
749    fn get_object_by_key_cache_only(
750        &self,
751        object_id: &ObjectID,
752        version: SequenceNumber,
753    ) -> CacheResult<Object> {
754        match self.get_object_entry_by_key_cache_only(object_id, version) {
755            CacheResult::Hit(entry) => match entry {
756                ObjectEntry::Object(object) => CacheResult::Hit(object),
757                ObjectEntry::Deleted | ObjectEntry::Wrapped => CacheResult::NegativeHit,
758            },
759            CacheResult::Miss => CacheResult::Miss,
760            CacheResult::NegativeHit => CacheResult::NegativeHit,
761        }
762    }
763
764    fn get_object_entry_by_id_cache_only(
765        &self,
766        request_type: &'static str,
767        object_id: &ObjectID,
768    ) -> CacheResult<(SequenceNumber, ObjectEntry)> {
769        self.metrics
770            .record_cache_request(request_type, "object_by_id");
771        let entry = self.object_by_id_cache.get(object_id);
772
773        if cfg!(debug_assertions)
774            && let Some(entry) = &entry
775        {
776            // check that cache is coherent
777            let highest: Option<ObjectEntry> = self
778                .dirty
779                .objects
780                .get(object_id)
781                .and_then(|entry| entry.get_highest().map(|(_, o)| o.clone()))
782                .or_else(|| {
783                    let obj: Option<ObjectEntry> = self
784                        .store
785                        .get_latest_object_or_tombstone(*object_id)
786                        .unwrap()
787                        .map(|(_, o)| o.into());
788                    obj
789                });
790
791            let cache_entry = match &*entry.lock() {
792                LatestObjectCacheEntry::Object(_, entry) => Some(entry.clone()),
793                LatestObjectCacheEntry::NonExistent => None,
794            };
795
796            // If the cache entry is a tombstone, the db entry may be missing if it was pruned.
797            let tombstone_possibly_pruned = highest.is_none()
798                && cache_entry
799                    .as_ref()
800                    .map(|e| e.is_tombstone())
801                    .unwrap_or(false);
802
803            if highest != cache_entry && !tombstone_possibly_pruned {
804                tracing::error!(
805                    ?highest,
806                    ?cache_entry,
807                    ?tombstone_possibly_pruned,
808                    "object_by_id cache is incoherent for {:?}",
809                    object_id
810                );
811                panic!("object_by_id cache is incoherent for {:?}", object_id);
812            }
813        }
814
815        if let Some(entry) = entry {
816            let entry = entry.lock();
817            match &*entry {
818                LatestObjectCacheEntry::Object(latest_version, latest_object) => {
819                    self.metrics.record_cache_hit(request_type, "object_by_id");
820                    return CacheResult::Hit((*latest_version, latest_object.clone()));
821                }
822                LatestObjectCacheEntry::NonExistent => {
823                    self.metrics
824                        .record_cache_negative_hit(request_type, "object_by_id");
825                    return CacheResult::NegativeHit;
826                }
827            }
828        } else {
829            self.metrics.record_cache_miss(request_type, "object_by_id");
830        }
831
832        Self::with_locked_cache_entries(
833            &self.dirty.objects,
834            &self.cached.object_cache,
835            object_id,
836            |dirty_entry, cached_entry| {
837                check_cache_entry_by_latest!(self, request_type, "uncommitted", dirty_entry);
838                check_cache_entry_by_latest!(self, request_type, "committed", cached_entry);
839                CacheResult::Miss
840            },
841        )
842    }
843
844    fn get_marker_value_cache_only(
845        &self,
846        object_key: FullObjectKey,
847        epoch_id: EpochId,
848    ) -> CacheResult<MarkerValue> {
849        Self::with_locked_cache_entries(
850            &self.dirty.markers,
851            &self.cached.marker_cache,
852            &(epoch_id, object_key.id()),
853            |dirty_entry, cached_entry| {
854                check_cache_entry_by_version!(
855                    self,
856                    "marker_by_version",
857                    "uncommitted",
858                    dirty_entry,
859                    object_key.version()
860                );
861                check_cache_entry_by_version!(
862                    self,
863                    "marker_by_version",
864                    "committed",
865                    cached_entry,
866                    object_key.version()
867                );
868                CacheResult::Miss
869            },
870        )
871    }
872
873    fn get_latest_marker_value_cache_only(
874        &self,
875        object_id: FullObjectID,
876        epoch_id: EpochId,
877    ) -> CacheResult<(SequenceNumber, MarkerValue)> {
878        Self::with_locked_cache_entries(
879            &self.dirty.markers,
880            &self.cached.marker_cache,
881            &(epoch_id, object_id),
882            |dirty_entry, cached_entry| {
883                check_cache_entry_by_latest!(self, "marker_latest", "uncommitted", dirty_entry);
884                check_cache_entry_by_latest!(self, "marker_latest", "committed", cached_entry);
885                CacheResult::Miss
886            },
887        )
888    }
889
890    fn get_object_impl(&self, request_type: &'static str, id: &ObjectID) -> Option<Object> {
891        let ticket = self.object_by_id_cache.get_ticket_for_read(id);
892        match self.get_object_entry_by_id_cache_only(request_type, id) {
893            CacheResult::Hit((_, entry)) => match entry {
894                ObjectEntry::Object(object) => Some(object),
895                ObjectEntry::Deleted | ObjectEntry::Wrapped => None,
896            },
897            CacheResult::NegativeHit => None,
898            CacheResult::Miss => {
899                let obj = self
900                    .store
901                    .get_latest_object_or_tombstone(*id)
902                    .expect("db error");
903                match obj {
904                    Some((key, obj)) => {
905                        self.cache_latest_object_by_id(
906                            id,
907                            LatestObjectCacheEntry::Object(key.1, obj.clone().into()),
908                            ticket,
909                        );
910                        match obj {
911                            ObjectOrTombstone::Object(object) => Some(object),
912                            ObjectOrTombstone::Tombstone(_) => None,
913                        }
914                    }
915                    None => {
916                        self.cache_object_not_found(id, ticket);
917                        None
918                    }
919                }
920            }
921        }
922    }
923
924    fn record_db_get(&self, request_type: &'static str) -> &AuthorityStore {
925        self.metrics.record_cache_request(request_type, "db");
926        &self.store
927    }
928
929    fn record_db_multi_get(&self, request_type: &'static str, count: usize) -> &AuthorityStore {
930        self.metrics
931            .record_cache_multi_request(request_type, "db", count);
932        &self.store
933    }
934
935    #[instrument(level = "debug", skip_all)]
936    fn write_transaction_outputs(&self, epoch_id: EpochId, tx_outputs: Arc<TransactionOutputs>) {
937        let tx_digest = *tx_outputs.transaction.digest();
938        trace!(?tx_digest, "writing transaction outputs to cache");
939
940        assert!(
941            !self.transaction_executed_in_last_epoch(&tx_digest, epoch_id),
942            "Transaction {:?} was already executed in epoch {}",
943            tx_digest,
944            epoch_id.saturating_sub(1)
945        );
946
947        let TransactionOutputs {
948            transaction,
949            effects,
950            markers,
951            written,
952            deleted,
953            wrapped,
954            events,
955            unchanged_loaded_runtime_objects,
956            ..
957        } = &*tx_outputs;
958
959        // Deletions and wraps must be written first. The reason is that one of the deletes
960        // may be a child object, and if we write the parent object first, a reader may or may
961        // not see the previous version of the child object, instead of the deleted/wrapped
962        // tombstone, which would cause an execution fork
963        for ObjectKey(id, version) in deleted.iter() {
964            self.write_object_entry(id, *version, ObjectEntry::Deleted);
965        }
966
967        for ObjectKey(id, version) in wrapped.iter() {
968            self.write_object_entry(id, *version, ObjectEntry::Wrapped);
969        }
970
971        // Update all markers
972        for (object_key, marker_value) in markers.iter() {
973            self.write_marker_value(epoch_id, *object_key, *marker_value);
974        }
975
976        // Write children before parents to ensure that readers do not observe a parent object
977        // before its most recent children are visible.
978        for (object_id, object) in written.iter() {
979            if object.is_child_object() {
980                self.write_object_entry(object_id, object.version(), object.clone().into());
981            }
982        }
983        for (object_id, object) in written.iter() {
984            if !object.is_child_object() {
985                self.write_object_entry(object_id, object.version(), object.clone().into());
986                if object.is_package() {
987                    debug!("caching package: {:?}", object.compute_object_reference());
988                    self.packages
989                        .insert(*object_id, PackageObject::new(object.clone()));
990                }
991            }
992        }
993
994        let tx_digest = *transaction.digest();
995        debug!(
996            ?tx_digest,
997            "Writing transaction output objects to cache: {:?}",
998            written
999                .values()
1000                .map(|o| (o.id(), o.version()))
1001                .collect::<Vec<_>>(),
1002        );
1003        let effects_digest = effects.digest();
1004
1005        self.metrics.record_cache_write("transaction_block");
1006        self.dirty
1007            .pending_transaction_writes
1008            .insert(tx_digest, tx_outputs.clone());
1009
1010        // insert transaction effects before executed_effects_digests so that there
1011        // are never dangling entries in executed_effects_digests
1012        self.metrics.record_cache_write("transaction_effects");
1013        self.dirty
1014            .transaction_effects
1015            .insert(effects_digest, effects.clone());
1016
1017        // note: if events.data.is_empty(), then there are no events for this transaction. We
1018        // store it anyway to avoid special cases in commint_transaction_outputs, and translate
1019        // an empty events structure to None when reading.
1020        self.metrics.record_cache_write("transaction_events");
1021        self.dirty
1022            .transaction_events
1023            .insert(tx_digest, events.clone());
1024
1025        self.metrics
1026            .record_cache_write("unchanged_loaded_runtime_objects");
1027        self.dirty
1028            .unchanged_loaded_runtime_objects
1029            .insert(tx_digest, unchanged_loaded_runtime_objects.clone());
1030
1031        self.metrics.record_cache_write("executed_effects_digests");
1032        self.dirty
1033            .executed_effects_digests
1034            .insert(tx_digest, effects_digest);
1035
1036        self.executed_effects_digests_notify_read
1037            .notify(&tx_digest, &effects_digest);
1038
1039        self.metrics
1040            .pending_notify_read
1041            .set(self.executed_effects_digests_notify_read.num_pending() as i64);
1042
1043        let prev = self
1044            .dirty
1045            .total_transaction_inserts
1046            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1047
1048        let pending_count = (prev + 1).saturating_sub(
1049            self.dirty
1050                .total_transaction_commits
1051                .load(std::sync::atomic::Ordering::Relaxed),
1052        );
1053
1054        self.set_backpressure(pending_count);
1055    }
1056
1057    fn build_db_batch(&self, epoch: EpochId, digests: &[TransactionDigest]) -> Batch {
1058        let _metrics_guard = mysten_metrics::monitored_scope("WritebackCache::build_db_batch");
1059        let mut all_outputs = Vec::with_capacity(digests.len());
1060        for tx in digests {
1061            let Some(outputs) = self
1062                .dirty
1063                .pending_transaction_writes
1064                .get(tx)
1065                .map(|o| o.clone())
1066            else {
1067                // This can happen in the following rare case:
1068                // All transactions in the checkpoint are committed to the db (by commit_transaction_outputs,
1069                // called in CheckpointExecutor::process_executed_transactions), but the process crashes before
1070                // the checkpoint water mark is bumped. We will then re-commit the checkpoint at startup,
1071                // despite that all transactions are already executed.
1072                warn!("Attempt to commit unknown transaction {:?}", tx);
1073                continue;
1074            };
1075            all_outputs.push(outputs);
1076        }
1077
1078        let batch = self
1079            .store
1080            .build_db_batch(epoch, &all_outputs)
1081            .expect("db error");
1082        (all_outputs, batch)
1083    }
1084
1085    // Commits dirty data for the given TransactionDigest to the db.
1086    #[instrument(level = "debug", skip_all)]
1087    fn commit_transaction_outputs(
1088        &self,
1089        epoch: EpochId,
1090        (all_outputs, db_batch): Batch,
1091        digests: &[TransactionDigest],
1092    ) {
1093        let _metrics_guard =
1094            mysten_metrics::monitored_scope("WritebackCache::commit_transaction_outputs");
1095        fail_point!("writeback-cache-commit");
1096        trace!(?digests);
1097
1098        // Flush writes to disk before removing anything from dirty set. otherwise,
1099        // a cache eviction could cause a value to disappear briefly, even if we insert to the
1100        // cache before removing from the dirty set.
1101        db_batch.write().expect("db error");
1102
1103        let _metrics_guard =
1104            mysten_metrics::monitored_scope("WritebackCache::commit_transaction_outputs::flush");
1105        // Parallel phase: tx-level metadata is keyed by unique tx_digest/effects_digest,
1106        // so there are no cross-transaction ordering constraints.
1107        all_outputs.par_iter().with_min_len(16).for_each(|outputs| {
1108            let tx_digest = outputs.transaction.digest();
1109            assert!(
1110                self.dirty
1111                    .pending_transaction_writes
1112                    .remove(tx_digest)
1113                    .is_some()
1114            );
1115            self.flush_tx_metadata_from_dirty_to_cached(*tx_digest, outputs);
1116        });
1117
1118        // Sequential phase: object/marker versions must be popped in causal order
1119        // (oldest first) per object_id. Multiple transactions in the batch can touch
1120        // the same shared object at consecutive versions, so this loop must preserve
1121        // the order of all_outputs.
1122        for outputs in all_outputs.iter() {
1123            self.flush_objects_from_dirty_to_cached(epoch, outputs);
1124        }
1125
1126        let num_outputs = all_outputs.len() as u64;
1127        let num_commits = self
1128            .dirty
1129            .total_transaction_commits
1130            .fetch_add(num_outputs, std::sync::atomic::Ordering::Relaxed)
1131            + num_outputs;
1132
1133        let pending_count = self
1134            .dirty
1135            .total_transaction_inserts
1136            .load(std::sync::atomic::Ordering::Relaxed)
1137            .saturating_sub(num_commits);
1138
1139        self.set_backpressure(pending_count);
1140    }
1141
1142    fn approximate_pending_transaction_count(&self) -> u64 {
1143        let num_commits = self
1144            .dirty
1145            .total_transaction_commits
1146            .load(std::sync::atomic::Ordering::Relaxed);
1147
1148        self.dirty
1149            .total_transaction_inserts
1150            .load(std::sync::atomic::Ordering::Relaxed)
1151            .saturating_sub(num_commits)
1152    }
1153
1154    fn set_backpressure(&self, pending_count: u64) {
1155        let backpressure = pending_count > self.backpressure_threshold;
1156        let backpressure_changed = self.backpressure_manager.set_backpressure(backpressure);
1157        if backpressure_changed {
1158            self.metrics.backpressure_toggles.inc();
1159        }
1160        self.metrics
1161            .backpressure_status
1162            .set(if backpressure { 1 } else { 0 });
1163    }
1164
1165    // Flushes tx-level metadata for a single transaction from dirty to cache.
1166    // All keys are unique per transaction (tx_digest, effects_digest), so this
1167    // is safe to call in parallel across transactions.
1168    fn flush_tx_metadata_from_dirty_to_cached(
1169        &self,
1170        tx_digest: TransactionDigest,
1171        outputs: &TransactionOutputs,
1172    ) {
1173        // TODO: outputs should have a strong count of 1 so we should be able to move out of it
1174        let TransactionOutputs {
1175            transaction,
1176            effects,
1177            events,
1178            ..
1179        } = outputs;
1180
1181        let effects_digest = effects.digest();
1182
1183        // Update cache before removing from self.dirty to avoid
1184        // unnecessary cache misses
1185        self.cached
1186            .transactions
1187            .insert(
1188                &tx_digest,
1189                PointCacheItem::Some(transaction.clone()),
1190                Ticket::Write,
1191            )
1192            .ok();
1193        self.cached
1194            .transaction_effects
1195            .insert(
1196                &effects_digest,
1197                PointCacheItem::Some(effects.clone().into()),
1198                Ticket::Write,
1199            )
1200            .ok();
1201        self.cached
1202            .executed_effects_digests
1203            .insert(
1204                &tx_digest,
1205                PointCacheItem::Some(effects_digest),
1206                Ticket::Write,
1207            )
1208            .ok();
1209        self.cached
1210            .transaction_events
1211            .insert(
1212                &tx_digest,
1213                PointCacheItem::Some(events.clone().into()),
1214                Ticket::Write,
1215            )
1216            .ok();
1217
1218        self.dirty
1219            .transaction_effects
1220            .remove(&effects_digest)
1221            .expect("effects must exist");
1222
1223        self.dirty
1224            .transaction_events
1225            .remove(&tx_digest)
1226            .expect("events must exist");
1227
1228        self.dirty
1229            .unchanged_loaded_runtime_objects
1230            .remove(&tx_digest)
1231            .expect("unchanged_loaded_runtime_objects must exist");
1232
1233        self.dirty
1234            .executed_effects_digests
1235            .remove(&tx_digest)
1236            .expect("executed effects must exist");
1237    }
1238
1239    // Flushes object and marker versions for a single transaction from dirty to cache.
1240    // Multiple transactions in the same batch can modify the same shared object at
1241    // consecutive versions, so callers must invoke this in causal (checkpoint) order.
1242    fn flush_objects_from_dirty_to_cached(&self, epoch: EpochId, outputs: &TransactionOutputs) {
1243        let TransactionOutputs {
1244            markers,
1245            written,
1246            deleted,
1247            wrapped,
1248            ..
1249        } = outputs;
1250
1251        for (object_key, marker_value) in markers.iter() {
1252            Self::move_version_from_dirty_to_cache(
1253                &self.dirty.markers,
1254                &self.cached.marker_cache,
1255                (epoch, object_key.id()),
1256                object_key.version(),
1257                marker_value,
1258            );
1259        }
1260
1261        for (object_id, object) in written.iter() {
1262            Self::move_version_from_dirty_to_cache(
1263                &self.dirty.objects,
1264                &self.cached.object_cache,
1265                *object_id,
1266                object.version(),
1267                &ObjectEntry::Object(object.clone()),
1268            );
1269        }
1270
1271        for ObjectKey(object_id, version) in deleted.iter() {
1272            Self::move_version_from_dirty_to_cache(
1273                &self.dirty.objects,
1274                &self.cached.object_cache,
1275                *object_id,
1276                *version,
1277                &ObjectEntry::Deleted,
1278            );
1279        }
1280
1281        for ObjectKey(object_id, version) in wrapped.iter() {
1282            Self::move_version_from_dirty_to_cache(
1283                &self.dirty.objects,
1284                &self.cached.object_cache,
1285                *object_id,
1286                *version,
1287                &ObjectEntry::Wrapped,
1288            );
1289        }
1290    }
1291
1292    // Move the oldest/least entry from the dirty queue to the cache queue.
1293    // This is called after the entry is committed to the db.
1294    fn move_version_from_dirty_to_cache<K, V>(
1295        dirty: &DashMap<K, CachedVersionMap<V>>,
1296        cache: &MokaCache<K, Arc<Mutex<CachedVersionMap<V>>>>,
1297        key: K,
1298        version: SequenceNumber,
1299        value: &V,
1300    ) where
1301        K: Eq + std::hash::Hash + Clone + Send + Sync + Copy + 'static,
1302        V: Send + Sync + Clone + Eq + std::fmt::Debug + 'static,
1303    {
1304        static MAX_VERSIONS: usize = 3;
1305
1306        // IMPORTANT: lock both the dirty set entry and the cache entry before modifying either.
1307        // this ensures that readers cannot see a value temporarily disappear.
1308        let dirty_entry = dirty.entry(key);
1309        let cache_entry = cache.entry(key).or_default();
1310        let mut cache_map = cache_entry.value().lock();
1311
1312        // insert into cache and drop old versions.
1313        cache_map.insert(version, value.clone());
1314        // TODO: make this automatic by giving CachedVersionMap an optional max capacity
1315        cache_map.truncate_to(MAX_VERSIONS);
1316
1317        let DashMapEntry::Occupied(mut occupied_dirty_entry) = dirty_entry else {
1318            panic!("dirty map must exist");
1319        };
1320
1321        let removed = occupied_dirty_entry.get_mut().pop_oldest(&version);
1322
1323        assert_eq!(removed.as_ref(), Some(value), "dirty version must exist");
1324
1325        // if there are no versions remaining, remove the map entry
1326        if occupied_dirty_entry.get().is_empty() {
1327            occupied_dirty_entry.remove();
1328        }
1329    }
1330
1331    // Updates the latest object id cache with an entry that was read from the db.
1332    fn cache_latest_object_by_id(
1333        &self,
1334        object_id: &ObjectID,
1335        object: LatestObjectCacheEntry,
1336        ticket: Ticket,
1337    ) {
1338        trace!("caching object by id: {:?} {:?}", object_id, object);
1339        if self
1340            .object_by_id_cache
1341            .insert(object_id, object, ticket)
1342            .is_ok()
1343        {
1344            self.metrics.record_cache_write("object_by_id");
1345        } else {
1346            trace!("discarded cache write due to expired ticket");
1347            self.metrics.record_ticket_expiry();
1348        }
1349    }
1350
1351    fn cache_object_not_found(&self, object_id: &ObjectID, ticket: Ticket) {
1352        self.cache_latest_object_by_id(object_id, LatestObjectCacheEntry::NonExistent, ticket);
1353    }
1354
1355    fn clear_state_end_of_epoch_impl(&self, execution_guard: &ExecutionLockWriteGuard<'_>) {
1356        info!("clearing state at end of epoch");
1357
1358        // Note: there cannot be any concurrent writes to self.dirty while we are in this function,
1359        // as all transaction execution is paused.
1360        for r in self.dirty.pending_transaction_writes.iter() {
1361            let outputs = r.value();
1362            if !outputs
1363                .transaction
1364                .transaction_data()
1365                .shared_input_objects()
1366                .is_empty()
1367            {
1368                debug_fatal!("transaction must be single writer");
1369            }
1370            info!(
1371                "clearing state for transaction {:?}",
1372                outputs.transaction.digest()
1373            );
1374            for (object_id, object) in outputs.written.iter() {
1375                if object.is_package() {
1376                    info!("removing non-finalized package from cache: {:?}", object_id);
1377                    self.packages.invalidate(object_id);
1378                }
1379                self.object_by_id_cache.invalidate(object_id);
1380                self.cached.object_cache.invalidate(object_id);
1381            }
1382
1383            for ObjectKey(object_id, _) in outputs.deleted.iter().chain(outputs.wrapped.iter()) {
1384                self.object_by_id_cache.invalidate(object_id);
1385                self.cached.object_cache.invalidate(object_id);
1386            }
1387        }
1388
1389        self.dirty.clear();
1390
1391        info!("clearing old transaction locks");
1392        self.object_locks.clear();
1393        info!("clearing object per epoch marker table");
1394        self.store
1395            .clear_object_per_epoch_marker_table(execution_guard)
1396            .expect("db error");
1397    }
1398
1399    fn bulk_insert_genesis_objects_impl(&self, objects: &[Object]) {
1400        self.store
1401            .bulk_insert_genesis_objects(objects)
1402            .expect("db error");
1403        for obj in objects {
1404            self.cached.object_cache.invalidate(&obj.id());
1405            self.object_by_id_cache.invalidate(&obj.id());
1406        }
1407    }
1408
1409    fn insert_genesis_object_impl(&self, object: Object) {
1410        self.object_by_id_cache.invalidate(&object.id());
1411        self.cached.object_cache.invalidate(&object.id());
1412        self.store.insert_genesis_object(object).expect("db error");
1413    }
1414
1415    pub fn clear_caches_and_assert_empty(&self) {
1416        info!("clearing caches");
1417        self.cached.clear_and_assert_empty();
1418        self.object_by_id_cache.invalidate_all();
1419        assert!(&self.object_by_id_cache.is_empty());
1420        self.packages.invalidate_all();
1421        assert_empty(&self.packages);
1422    }
1423}
1424
1425fn account_amount_from_object(account_obj: &Object) -> u128 {
1426    let (_, AccumulatorValue::U128(value)) =
1427        account_obj.data.try_as_move().unwrap().try_into().unwrap();
1428    value.value
1429}
1430
1431impl AccountFundsRead for WritebackCache {
1432    fn get_latest_account_amount(&self, account_id: &AccumulatorObjId) -> u128 {
1433        ObjectCacheRead::get_object(self, account_id.inner())
1434            .map(|account_obj| account_amount_from_object(&account_obj))
1435            .unwrap_or(0)
1436    }
1437
1438    fn get_consistent_latest_account_amount_and_version(
1439        &self,
1440        account_id: &AccumulatorObjId,
1441    ) -> (u128, SequenceNumber) {
1442        // Settlement is not atomic. A settlement transaction writes the accumulator
1443        // objects at version V+1 first, and then a later barrier transaction bumps the
1444        // root from V to V+1. A reader that observes the state in between sees
1445        // post-settlement account objects alongside the pre-settlement root version,
1446        // and reading the "latest" account can therefore disagree with the root we
1447        // just captured.
1448        //
1449        // We handle this with two pieces:
1450        //
1451        // 1. MVCC read capped at the captured root version (see the call site below).
1452        //    By construction of the settlement/barrier ordering, every account object's
1453        //    version is <= the root version after the corresponding barrier runs, so
1454        //    capping at the captured root strips away any newer-settlement writes that
1455        //    have raced ahead of the barrier. The returned amount is the balance at or
1456        //    before the captured root version, even if the account object has a newer
1457        //    latest version by the time this method returns.
1458        //
1459        // 2. Root-version stability check (pre == post). `get_account_amount_at_version`
1460        //    is only safe to call when the target version has not been pruned. Pruning
1461        //    is tied to root advancement, so by reading the root before and after the
1462        //    MVCC read and retrying on mismatch, we ensure that no root advance (and
1463        //    therefore no pruning of the version we read at) could have happened while
1464        //    we were reading — the data we read is still live in the system (memory or
1465        //    db) throughout the call.
1466        let mut pre_root_version =
1467            ObjectCacheRead::get_object(self, &SUI_ACCUMULATOR_ROOT_OBJECT_ID)
1468                .unwrap()
1469                .version();
1470        let starting_root_version = pre_root_version;
1471        let mut loop_iter = 0;
1472        loop {
1473            loop_iter += 1;
1474            // Safe because of (1) and (2) above: the stability check below bounds the
1475            // lifetime of `pre_root_version` to a window in which no pruning happens.
1476            let value = self.get_account_amount_at_version(account_id, pre_root_version);
1477            let post_root_version =
1478                ObjectCacheRead::get_object(self, &SUI_ACCUMULATOR_ROOT_OBJECT_ID)
1479                    .unwrap()
1480                    .version();
1481            if pre_root_version == post_root_version {
1482                if loop_iter > 10 {
1483                    debug!(
1484                        iterations = loop_iter,
1485                        starting_root_version = %starting_root_version,
1486                        ending_root_version = %post_root_version,
1487                        "Root version stabilized after multiple iterations during MVCC read"
1488                    );
1489                }
1490                return (value, pre_root_version);
1491            }
1492            debug!(
1493                "Root version changed from {} to {} during MVCC read, retrying",
1494                pre_root_version, post_root_version
1495            );
1496            pre_root_version = post_root_version;
1497        }
1498    }
1499
1500    fn get_account_amount_at_version(
1501        &self,
1502        account_id: &AccumulatorObjId,
1503        version: SequenceNumber,
1504    ) -> u128 {
1505        let account_obj = self.find_object_lt_or_eq_version(*account_id.inner(), version);
1506        account_obj
1507            .map(|account_obj| account_amount_from_object(&account_obj))
1508            .unwrap_or(0)
1509    }
1510}
1511
1512impl ExecutionCacheAPI for WritebackCache {}
1513
1514impl ExecutionCacheCommit for WritebackCache {
1515    fn build_db_batch(&self, epoch: EpochId, digests: &[TransactionDigest]) -> Batch {
1516        self.build_db_batch(epoch, digests)
1517    }
1518
1519    fn set_highest_committed_checkpoint_in_batch(
1520        &self,
1521        batch: &mut Batch,
1522        checkpoint: CheckpointSequenceNumber,
1523    ) {
1524        self.store
1525            .perpetual_tables
1526            .set_highest_committed_checkpoint(&mut batch.1, checkpoint)
1527            .expect("db error");
1528    }
1529
1530    fn commit_transaction_outputs(
1531        &self,
1532        epoch: EpochId,
1533        batch: Batch,
1534        digests: &[TransactionDigest],
1535    ) {
1536        WritebackCache::commit_transaction_outputs(self, epoch, batch, digests)
1537    }
1538
1539    fn persist_transaction(&self, tx: &VerifiedExecutableTransaction) {
1540        self.store.persist_transaction(tx).expect("db error");
1541    }
1542
1543    fn approximate_pending_transaction_count(&self) -> u64 {
1544        WritebackCache::approximate_pending_transaction_count(self)
1545    }
1546}
1547
1548impl ObjectCacheRead for WritebackCache {
1549    fn get_package_object(&self, package_id: &ObjectID) -> SuiResult<Option<PackageObject>> {
1550        self.metrics
1551            .record_cache_request("package", "package_cache");
1552        if let Some(p) = self.packages.get(package_id) {
1553            if cfg!(debug_assertions) {
1554                let canonical_package = self
1555                    .dirty
1556                    .objects
1557                    .get(package_id)
1558                    .and_then(|v| match v.get_highest().map(|v| v.1.clone()) {
1559                        Some(ObjectEntry::Object(object)) => Some(object),
1560                        _ => None,
1561                    })
1562                    .or_else(|| self.store.get_object(package_id));
1563
1564                if let Some(canonical_package) = canonical_package {
1565                    assert_eq!(
1566                        canonical_package.digest(),
1567                        p.object().digest(),
1568                        "Package object cache is inconsistent for package {:?}",
1569                        package_id
1570                    );
1571                }
1572            }
1573            self.metrics.record_cache_hit("package", "package_cache");
1574            return Ok(Some(p));
1575        } else {
1576            self.metrics.record_cache_miss("package", "package_cache");
1577        }
1578
1579        // We try the dirty objects cache as well before going to the database. This is necessary
1580        // because the package could be evicted from the package cache before it is committed
1581        // to the database.
1582        if let Some(p) = self.get_object_impl("package", package_id) {
1583            if p.is_package() {
1584                let p = PackageObject::new(p);
1585                tracing::trace!(
1586                    "caching package: {:?}",
1587                    p.object().compute_object_reference()
1588                );
1589                self.metrics.record_cache_write("package");
1590                self.packages.insert(*package_id, p.clone());
1591                Ok(Some(p))
1592            } else {
1593                Err(SuiErrorKind::UserInputError {
1594                    error: UserInputError::MoveObjectAsPackage {
1595                        object_id: *package_id,
1596                    },
1597                }
1598                .into())
1599            }
1600        } else {
1601            Ok(None)
1602        }
1603    }
1604
1605    fn force_reload_system_packages(&self, _system_package_ids: &[ObjectID]) {
1606        // This is a no-op because all writes go through the cache, therefore it can never
1607        // be incoherent
1608    }
1609
1610    // get_object and variants.
1611
1612    fn get_object(&self, id: &ObjectID) -> Option<Object> {
1613        self.get_object_impl("object_latest", id)
1614    }
1615
1616    fn get_object_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> Option<Object> {
1617        match self.get_object_by_key_cache_only(object_id, version) {
1618            CacheResult::Hit(object) => Some(object),
1619            CacheResult::NegativeHit => None,
1620            CacheResult::Miss => self
1621                .record_db_get("object_by_version")
1622                .get_object_by_key(object_id, version),
1623        }
1624    }
1625
1626    fn multi_get_objects_by_key(&self, object_keys: &[ObjectKey]) -> Vec<Option<Object>> {
1627        do_fallback_lookup(
1628            object_keys,
1629            |key| match self.get_object_by_key_cache_only(&key.0, key.1) {
1630                CacheResult::Hit(maybe_object) => CacheResult::Hit(Some(maybe_object)),
1631                CacheResult::NegativeHit => CacheResult::NegativeHit,
1632                CacheResult::Miss => CacheResult::Miss,
1633            },
1634            |remaining| {
1635                self.record_db_multi_get("object_by_version", remaining.len())
1636                    .multi_get_objects_by_key(remaining)
1637                    .expect("db error")
1638            },
1639        )
1640    }
1641
1642    fn object_exists_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> bool {
1643        match self.get_object_by_key_cache_only(object_id, version) {
1644            CacheResult::Hit(_) => true,
1645            CacheResult::NegativeHit => false,
1646            CacheResult::Miss => self
1647                .record_db_get("object_by_version")
1648                .object_exists_by_key(object_id, version)
1649                .expect("db error"),
1650        }
1651    }
1652
1653    fn multi_object_exists_by_key(&self, object_keys: &[ObjectKey]) -> Vec<bool> {
1654        do_fallback_lookup(
1655            object_keys,
1656            |key| match self.get_object_by_key_cache_only(&key.0, key.1) {
1657                CacheResult::Hit(_) => CacheResult::Hit(true),
1658                CacheResult::NegativeHit => CacheResult::Hit(false),
1659                CacheResult::Miss => CacheResult::Miss,
1660            },
1661            |remaining| {
1662                self.record_db_multi_get("object_by_version", remaining.len())
1663                    .multi_object_exists_by_key(remaining)
1664                    .expect("db error")
1665            },
1666        )
1667    }
1668
1669    fn get_latest_object_ref_or_tombstone(&self, object_id: ObjectID) -> Option<ObjectRef> {
1670        match self.get_object_entry_by_id_cache_only("latest_objref_or_tombstone", &object_id) {
1671            CacheResult::Hit((version, entry)) => Some(match entry {
1672                ObjectEntry::Object(object) => object.compute_object_reference(),
1673                ObjectEntry::Deleted => (object_id, version, ObjectDigest::OBJECT_DIGEST_DELETED),
1674                ObjectEntry::Wrapped => (object_id, version, ObjectDigest::OBJECT_DIGEST_WRAPPED),
1675            }),
1676            CacheResult::NegativeHit => None,
1677            CacheResult::Miss => self
1678                .record_db_get("latest_objref_or_tombstone")
1679                .get_latest_object_ref_or_tombstone(object_id)
1680                .expect("db error"),
1681        }
1682    }
1683
1684    fn get_latest_object_or_tombstone(
1685        &self,
1686        object_id: ObjectID,
1687    ) -> Option<(ObjectKey, ObjectOrTombstone)> {
1688        match self.get_object_entry_by_id_cache_only("latest_object_or_tombstone", &object_id) {
1689            CacheResult::Hit((version, entry)) => {
1690                let key = ObjectKey(object_id, version);
1691                Some(match entry {
1692                    ObjectEntry::Object(object) => (key, object.into()),
1693                    ObjectEntry::Deleted => (
1694                        key,
1695                        ObjectOrTombstone::Tombstone((
1696                            object_id,
1697                            version,
1698                            ObjectDigest::OBJECT_DIGEST_DELETED,
1699                        )),
1700                    ),
1701                    ObjectEntry::Wrapped => (
1702                        key,
1703                        ObjectOrTombstone::Tombstone((
1704                            object_id,
1705                            version,
1706                            ObjectDigest::OBJECT_DIGEST_WRAPPED,
1707                        )),
1708                    ),
1709                })
1710            }
1711            CacheResult::NegativeHit => None,
1712            CacheResult::Miss => self
1713                .record_db_get("latest_object_or_tombstone")
1714                .get_latest_object_or_tombstone(object_id)
1715                .expect("db error"),
1716        }
1717    }
1718
1719    fn multi_input_objects_available_cache_only(&self, keys: &[InputKey]) -> Vec<bool> {
1720        keys.iter()
1721            .map(|key| {
1722                if key.is_cancelled() {
1723                    true
1724                } else {
1725                    match key {
1726                        // See multi_input_objects_available: a consensus input is not satisfied
1727                        // by an object at the same version on a different stream.
1728                        InputKey::VersionedObject { id, version } => {
1729                            match self.get_object_by_key_cache_only(&id.id(), *version) {
1730                                CacheResult::Hit(object) => {
1731                                    !matches!(id, FullObjectID::Consensus(_))
1732                                        || object.full_id() == *id
1733                                }
1734                                _ => false,
1735                            }
1736                        }
1737                        InputKey::Package { id } => self.packages.contains_key(id),
1738                    }
1739                }
1740            })
1741            .collect()
1742    }
1743
1744    #[instrument(level = "trace", skip_all, fields(object_id, version_bound))]
1745    fn find_object_lt_or_eq_version(
1746        &self,
1747        object_id: ObjectID,
1748        version_bound: SequenceNumber,
1749    ) -> Option<Object> {
1750        macro_rules! check_cache_entry {
1751            ($level: expr, $objects: expr) => {
1752                self.metrics
1753                    .record_cache_request("object_lt_or_eq_version", $level);
1754                if let Some(objects) = $objects {
1755                    if let Some((_, object)) = objects
1756                        .all_versions_lt_or_eq_descending(&version_bound)
1757                        .next()
1758                    {
1759                        if let ObjectEntry::Object(object) = object {
1760                            self.metrics
1761                                .record_cache_hit("object_lt_or_eq_version", $level);
1762                            return Some(object.clone());
1763                        } else {
1764                            // if we find a tombstone, the object does not exist
1765                            self.metrics
1766                                .record_cache_negative_hit("object_lt_or_eq_version", $level);
1767                            return None;
1768                        }
1769                    } else {
1770                        self.metrics
1771                            .record_cache_miss("object_lt_or_eq_version", $level);
1772                    }
1773                }
1774            };
1775        }
1776
1777        // if we have the latest version cached, and it is within the bound, we are done
1778        self.metrics
1779            .record_cache_request("object_lt_or_eq_version", "object_by_id");
1780        let latest_cache_entry = self.object_by_id_cache.get(&object_id);
1781        if let Some(latest) = &latest_cache_entry {
1782            let latest = latest.lock();
1783            match &*latest {
1784                LatestObjectCacheEntry::Object(latest_version, object) => {
1785                    if *latest_version <= version_bound {
1786                        if let ObjectEntry::Object(object) = object {
1787                            self.metrics
1788                                .record_cache_hit("object_lt_or_eq_version", "object_by_id");
1789                            return Some(object.clone());
1790                        } else {
1791                            // object is a tombstone, but is still within the version bound
1792                            self.metrics.record_cache_negative_hit(
1793                                "object_lt_or_eq_version",
1794                                "object_by_id",
1795                            );
1796                            return None;
1797                        }
1798                    }
1799                    // latest object is not within the version bound. fall through.
1800                }
1801                // No object by this ID exists at all
1802                LatestObjectCacheEntry::NonExistent => {
1803                    self.metrics
1804                        .record_cache_negative_hit("object_lt_or_eq_version", "object_by_id");
1805                    return None;
1806                }
1807            }
1808        }
1809        self.metrics
1810            .record_cache_miss("object_lt_or_eq_version", "object_by_id");
1811
1812        Self::with_locked_cache_entries(
1813            &self.dirty.objects,
1814            &self.cached.object_cache,
1815            &object_id,
1816            |dirty_entry, cached_entry| {
1817                check_cache_entry!("committed", dirty_entry);
1818                check_cache_entry!("uncommitted", cached_entry);
1819
1820                // Much of the time, the query will be for the very latest object version, so
1821                // try that first. But we have to be careful:
1822                // 1. We must load the tombstone if it is present, because its version may exceed
1823                //    the version_bound, in which case we must do a scan.
1824                // 2. You might think we could just call `self.store.get_latest_object_or_tombstone` here.
1825                //    But we cannot, because there may be a more recent version in the dirty set, which
1826                //    we skipped over in check_cache_entry! because of the version bound. However, if we
1827                //    skipped it above, we will skip it here as well, again due to the version bound.
1828                // 3. Despite that, we really want to warm the cache here. Why? Because if the object is
1829                //    cold (not being written to), then we will very soon be able to start serving reads
1830                //    of it from the object_by_id cache, IF we can warm the cache. If we don't warm the
1831                //    the cache here, and no writes to the object occur, then we will always have to go
1832                //    to the db for the object.
1833                //
1834                // Lastly, it is important to understand the rationale for all this: If the object is
1835                // write-hot, we will serve almost all reads to it from the dirty set (or possibly the
1836                // cached set if it is only written to once every few checkpoints). If the object is
1837                // write-cold (or non-existent) and read-hot, then we will serve almost all reads to it
1838                // from the object_by_id cache check above.  Most of the apparently wasteful code here
1839                // exists only to ensure correctness in all the edge cases.
1840                let latest: Option<(SequenceNumber, ObjectEntry)> =
1841                    if let Some(dirty_set) = dirty_entry {
1842                        dirty_set
1843                            .get_highest()
1844                            .cloned()
1845                            .tap_none(|| panic!("dirty set cannot be empty"))
1846                    } else {
1847                        // TODO: we should try not to read from the db while holding the locks.
1848                        self.record_db_get("object_lt_or_eq_version_latest")
1849                            .get_latest_object_or_tombstone(object_id)
1850                            .expect("db error")
1851                            .map(|(ObjectKey(_, version), obj_or_tombstone)| {
1852                                (version, ObjectEntry::from(obj_or_tombstone))
1853                            })
1854                    };
1855
1856                if let Some((obj_version, obj_entry)) = latest {
1857                    // we can always cache the latest object (or tombstone), even if it is not within the
1858                    // version_bound. This is done in order to warm the cache in the case where a sequence
1859                    // of transactions all read the same child object without writing to it.
1860
1861                    // Note: no need to call with_object_by_id_cache_update here, because we are holding
1862                    // the lock on the dirty cache entry, and `latest` cannot become out-of-date
1863                    // while we hold that lock.
1864                    self.cache_latest_object_by_id(
1865                        &object_id,
1866                        LatestObjectCacheEntry::Object(obj_version, obj_entry.clone()),
1867                        // We can get a ticket at the last second, because we are holding the lock
1868                        // on dirty, so there cannot be any concurrent writes.
1869                        self.object_by_id_cache.get_ticket_for_read(&object_id),
1870                    );
1871
1872                    if obj_version <= version_bound {
1873                        match obj_entry {
1874                            ObjectEntry::Object(object) => Some(object),
1875                            ObjectEntry::Deleted | ObjectEntry::Wrapped => None,
1876                        }
1877                    } else {
1878                        // The latest object exceeded the bound, so now we have to do a scan
1879                        // But we already know there is no dirty entry within the bound,
1880                        // so we go to the db.
1881                        self.record_db_get("object_lt_or_eq_version_scan")
1882                            .find_object_lt_or_eq_version(object_id, version_bound)
1883                            .expect("db error")
1884                    }
1885
1886                // no object found in dirty set or db, object does not exist
1887                // When this is called from a read api (i.e. not the execution path) it is
1888                // possible that the object has been deleted and pruned. In this case,
1889                // there would be no entry at all on disk, but we may have a tombstone in the
1890                // cache
1891                } else if let Some(latest_cache_entry) = latest_cache_entry {
1892                    // If there is a latest cache entry, it had better not be a live object!
1893                    assert!(!latest_cache_entry.lock().is_alive());
1894                    None
1895                } else {
1896                    // If there is no latest cache entry, we can insert one.
1897                    let highest = cached_entry.and_then(|c| c.get_highest());
1898                    assert!(highest.is_none() || highest.unwrap().1.is_tombstone());
1899                    self.cache_object_not_found(
1900                        &object_id,
1901                        // okay to get ticket at last second - see above
1902                        self.object_by_id_cache.get_ticket_for_read(&object_id),
1903                    );
1904                    None
1905                }
1906            },
1907        )
1908    }
1909
1910    fn get_sui_system_state_object_unsafe(&self) -> SuiResult<SuiSystemState> {
1911        get_sui_system_state(self)
1912    }
1913
1914    fn get_bridge_object_unsafe(&self) -> SuiResult<Bridge> {
1915        get_bridge(self)
1916    }
1917
1918    fn get_marker_value(
1919        &self,
1920        object_key: FullObjectKey,
1921        epoch_id: EpochId,
1922    ) -> Option<MarkerValue> {
1923        match self.get_marker_value_cache_only(object_key, epoch_id) {
1924            CacheResult::Hit(marker) => Some(marker),
1925            CacheResult::NegativeHit => None,
1926            CacheResult::Miss => self
1927                .record_db_get("marker_by_version")
1928                .get_marker_value(object_key, epoch_id)
1929                .expect("db error"),
1930        }
1931    }
1932
1933    fn get_latest_marker(
1934        &self,
1935        object_id: FullObjectID,
1936        epoch_id: EpochId,
1937    ) -> Option<(SequenceNumber, MarkerValue)> {
1938        match self.get_latest_marker_value_cache_only(object_id, epoch_id) {
1939            CacheResult::Hit((v, marker)) => Some((v, marker)),
1940            CacheResult::NegativeHit => {
1941                panic!("cannot have negative hit when getting latest marker")
1942            }
1943            CacheResult::Miss => self
1944                .record_db_get("marker_latest")
1945                .get_latest_marker(object_id, epoch_id)
1946                .expect("db error"),
1947        }
1948    }
1949
1950    #[cfg(test)]
1951    fn get_lock(&self, obj_ref: ObjectRef, epoch_store: &AuthorityPerEpochStore) -> SuiLockResult {
1952        let cur_epoch = epoch_store.epoch();
1953        let Some(obj) = self.get_object_impl("lock", &obj_ref.0) else {
1954            return Err(SuiError::from(UserInputError::ObjectNotFound {
1955                object_id: obj_ref.0,
1956                // even though we know the requested version, we leave it as None to indicate
1957                // that the object does not exist at any version
1958                version: None,
1959            }));
1960        };
1961        let actual_objref = obj.compute_object_reference();
1962        if obj_ref != actual_objref {
1963            Ok(ObjectLockStatus::LockedAtDifferentVersion {
1964                locked_ref: actual_objref,
1965            })
1966        } else {
1967            // requested object ref is live, check if there is a lock
1968            Ok(
1969                match self
1970                    .object_locks
1971                    .get_transaction_lock(&obj_ref, epoch_store)?
1972                {
1973                    Some(tx_digest) => ObjectLockStatus::LockedToTx {
1974                        locked_by_tx: LockDetailsDeprecated {
1975                            epoch: cur_epoch,
1976                            tx_digest,
1977                        },
1978                    },
1979                    None => ObjectLockStatus::Initialized,
1980                },
1981            )
1982        }
1983    }
1984
1985    fn _get_live_objref(&self, object_id: ObjectID) -> SuiResult<ObjectRef> {
1986        let obj = self.get_object_impl("live_objref", &object_id).ok_or(
1987            UserInputError::ObjectNotFound {
1988                object_id,
1989                version: None,
1990            },
1991        )?;
1992        Ok(obj.compute_object_reference())
1993    }
1994
1995    fn get_highest_pruned_checkpoint(&self) -> Option<CheckpointSequenceNumber> {
1996        self.store
1997            .perpetual_tables
1998            .get_highest_pruned_checkpoint()
1999            .expect("db error")
2000    }
2001
2002    fn notify_read_input_objects<'a>(
2003        &'a self,
2004        input_and_receiving_keys: &'a [InputKey],
2005        receiving_keys: &'a HashSet<InputKey>,
2006        epoch: EpochId,
2007    ) -> BoxFuture<'a, ()> {
2008        self.object_notify_read
2009            .read(
2010                "notify_read_input_objects",
2011                input_and_receiving_keys,
2012                move |keys| {
2013                    self.multi_input_objects_available(keys, receiving_keys, epoch)
2014                        .into_iter()
2015                        .map(|available| if available { Some(()) } else { None })
2016                        .collect::<Vec<_>>()
2017                },
2018            )
2019            .map(|_| ())
2020            .boxed()
2021    }
2022}
2023
2024impl TransactionCacheRead for WritebackCache {
2025    fn multi_get_transaction_blocks(
2026        &self,
2027        digests: &[TransactionDigest],
2028    ) -> Vec<Option<Arc<VerifiedTransaction>>> {
2029        let digests_and_tickets: Vec<_> = digests
2030            .iter()
2031            .map(|d| (*d, self.cached.transactions.get_ticket_for_read(d)))
2032            .collect();
2033        do_fallback_lookup(
2034            &digests_and_tickets,
2035            |(digest, _)| {
2036                self.metrics
2037                    .record_cache_request("transaction_block", "uncommitted");
2038                if let Some(tx) = self.dirty.pending_transaction_writes.get(digest) {
2039                    self.metrics
2040                        .record_cache_hit("transaction_block", "uncommitted");
2041                    return CacheResult::Hit(Some(tx.transaction.clone()));
2042                }
2043                self.metrics
2044                    .record_cache_miss("transaction_block", "uncommitted");
2045
2046                self.metrics
2047                    .record_cache_request("transaction_block", "committed");
2048
2049                match self
2050                    .cached
2051                    .transactions
2052                    .get(digest)
2053                    .map(|l| l.lock().clone())
2054                {
2055                    Some(PointCacheItem::Some(tx)) => {
2056                        self.metrics
2057                            .record_cache_hit("transaction_block", "committed");
2058                        CacheResult::Hit(Some(tx))
2059                    }
2060                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2061                    None => {
2062                        self.metrics
2063                            .record_cache_miss("transaction_block", "committed");
2064
2065                        CacheResult::Miss
2066                    }
2067                }
2068            },
2069            |remaining| {
2070                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2071                let results: Vec<_> = self
2072                    .record_db_multi_get("transaction_block", remaining.len())
2073                    .multi_get_transaction_blocks(&remaining_digests)
2074                    .expect("db error")
2075                    .into_iter()
2076                    .map(|o| o.map(Arc::new))
2077                    .collect();
2078                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2079                    if result.is_none() {
2080                        self.cached.transactions.insert(digest, None, *ticket).ok();
2081                    }
2082                }
2083                results
2084            },
2085        )
2086    }
2087
2088    fn multi_get_executed_effects_digests(
2089        &self,
2090        digests: &[TransactionDigest],
2091    ) -> Vec<Option<TransactionEffectsDigest>> {
2092        let digests_and_tickets: Vec<_> = digests
2093            .iter()
2094            .map(|d| {
2095                (
2096                    *d,
2097                    self.cached.executed_effects_digests.get_ticket_for_read(d),
2098                )
2099            })
2100            .collect();
2101        do_fallback_lookup(
2102            &digests_and_tickets,
2103            |(digest, _)| {
2104                self.metrics
2105                    .record_cache_request("executed_effects_digests", "uncommitted");
2106                if let Some(digest) = self.dirty.executed_effects_digests.get(digest) {
2107                    self.metrics
2108                        .record_cache_hit("executed_effects_digests", "uncommitted");
2109                    return CacheResult::Hit(Some(*digest));
2110                }
2111                self.metrics
2112                    .record_cache_miss("executed_effects_digests", "uncommitted");
2113
2114                self.metrics
2115                    .record_cache_request("executed_effects_digests", "committed");
2116                match self
2117                    .cached
2118                    .executed_effects_digests
2119                    .get(digest)
2120                    .map(|l| *l.lock())
2121                {
2122                    Some(PointCacheItem::Some(digest)) => {
2123                        self.metrics
2124                            .record_cache_hit("executed_effects_digests", "committed");
2125                        CacheResult::Hit(Some(digest))
2126                    }
2127                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2128                    None => {
2129                        self.metrics
2130                            .record_cache_miss("executed_effects_digests", "committed");
2131                        CacheResult::Miss
2132                    }
2133                }
2134            },
2135            |remaining| {
2136                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2137                let results = self
2138                    .record_db_multi_get("executed_effects_digests", remaining.len())
2139                    .multi_get_executed_effects_digests(&remaining_digests)
2140                    .expect("db error");
2141                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2142                    if result.is_none() {
2143                        self.cached
2144                            .executed_effects_digests
2145                            .insert(digest, None, *ticket)
2146                            .ok();
2147                    }
2148                }
2149                results
2150            },
2151        )
2152    }
2153
2154    fn multi_get_effects(
2155        &self,
2156        digests: &[TransactionEffectsDigest],
2157    ) -> Vec<Option<TransactionEffects>> {
2158        let digests_and_tickets: Vec<_> = digests
2159            .iter()
2160            .map(|d| (*d, self.cached.transaction_effects.get_ticket_for_read(d)))
2161            .collect();
2162        do_fallback_lookup(
2163            &digests_and_tickets,
2164            |(digest, _)| {
2165                self.metrics
2166                    .record_cache_request("transaction_effects", "uncommitted");
2167                if let Some(effects) = self.dirty.transaction_effects.get(digest) {
2168                    self.metrics
2169                        .record_cache_hit("transaction_effects", "uncommitted");
2170                    return CacheResult::Hit(Some(effects.clone()));
2171                }
2172                self.metrics
2173                    .record_cache_miss("transaction_effects", "uncommitted");
2174
2175                self.metrics
2176                    .record_cache_request("transaction_effects", "committed");
2177                match self
2178                    .cached
2179                    .transaction_effects
2180                    .get(digest)
2181                    .map(|l| l.lock().clone())
2182                {
2183                    Some(PointCacheItem::Some(effects)) => {
2184                        self.metrics
2185                            .record_cache_hit("transaction_effects", "committed");
2186                        CacheResult::Hit(Some((*effects).clone()))
2187                    }
2188                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2189                    None => {
2190                        self.metrics
2191                            .record_cache_miss("transaction_effects", "committed");
2192                        CacheResult::Miss
2193                    }
2194                }
2195            },
2196            |remaining| {
2197                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2198                let results = self
2199                    .record_db_multi_get("transaction_effects", remaining.len())
2200                    .multi_get_effects(remaining_digests.iter())
2201                    .expect("db error");
2202                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2203                    if result.is_none() {
2204                        self.cached
2205                            .transaction_effects
2206                            .insert(digest, None, *ticket)
2207                            .ok();
2208                    }
2209                }
2210                results
2211            },
2212        )
2213    }
2214
2215    fn transaction_executed_in_last_epoch(
2216        &self,
2217        digest: &TransactionDigest,
2218        current_epoch: EpochId,
2219    ) -> bool {
2220        if current_epoch == 0 {
2221            return false;
2222        }
2223        let last_epoch = current_epoch - 1;
2224        let cache_key = (last_epoch, *digest);
2225
2226        let ticket = self
2227            .cached
2228            .transaction_executed_in_last_epoch
2229            .get_ticket_for_read(&cache_key);
2230
2231        if let Some(cached) = self
2232            .cached
2233            .transaction_executed_in_last_epoch
2234            .get(&cache_key)
2235        {
2236            return cached.lock().is_some();
2237        }
2238
2239        let was_executed = self
2240            .store
2241            .perpetual_tables
2242            .was_transaction_executed_in_last_epoch(digest, current_epoch);
2243
2244        let value = if was_executed { Some(()) } else { None };
2245        self.cached
2246            .transaction_executed_in_last_epoch
2247            .insert(&cache_key, value, ticket)
2248            .ok();
2249
2250        was_executed
2251    }
2252
2253    fn notify_read_executed_effects_digests<'a>(
2254        &'a self,
2255        task_name: &'static str,
2256        digests: &'a [TransactionDigest],
2257    ) -> BoxFuture<'a, Vec<TransactionEffectsDigest>> {
2258        self.executed_effects_digests_notify_read
2259            .read(task_name, digests, |digests| {
2260                self.multi_get_executed_effects_digests(digests)
2261            })
2262            .boxed()
2263    }
2264
2265    fn multi_get_events(
2266        &self,
2267        event_digests: &[TransactionDigest],
2268    ) -> Vec<Option<TransactionEvents>> {
2269        fn map_events(events: TransactionEvents) -> Option<TransactionEvents> {
2270            if events.data.is_empty() {
2271                None
2272            } else {
2273                Some(events)
2274            }
2275        }
2276
2277        let digests_and_tickets: Vec<_> = event_digests
2278            .iter()
2279            .map(|d| (*d, self.cached.transaction_events.get_ticket_for_read(d)))
2280            .collect();
2281        do_fallback_lookup(
2282            &digests_and_tickets,
2283            |(digest, _)| {
2284                self.metrics
2285                    .record_cache_request("transaction_events", "uncommitted");
2286                if let Some(events) = self.dirty.transaction_events.get(digest).map(|e| e.clone()) {
2287                    self.metrics
2288                        .record_cache_hit("transaction_events", "uncommitted");
2289
2290                    return CacheResult::Hit(map_events(events));
2291                }
2292                self.metrics
2293                    .record_cache_miss("transaction_events", "uncommitted");
2294
2295                self.metrics
2296                    .record_cache_request("transaction_events", "committed");
2297                match self
2298                    .cached
2299                    .transaction_events
2300                    .get(digest)
2301                    .map(|l| l.lock().clone())
2302                {
2303                    Some(PointCacheItem::Some(events)) => {
2304                        self.metrics
2305                            .record_cache_hit("transaction_events", "committed");
2306                        CacheResult::Hit(map_events((*events).clone()))
2307                    }
2308                    Some(PointCacheItem::None) => CacheResult::NegativeHit,
2309                    None => {
2310                        self.metrics
2311                            .record_cache_miss("transaction_events", "committed");
2312
2313                        CacheResult::Miss
2314                    }
2315                }
2316            },
2317            |remaining| {
2318                let remaining_digests: Vec<_> = remaining.iter().map(|(d, _)| *d).collect();
2319                let results = self
2320                    .store
2321                    .multi_get_events(&remaining_digests)
2322                    .expect("db error");
2323                for ((digest, ticket), result) in remaining.iter().zip_debug_eq(results.iter()) {
2324                    if result.is_none() {
2325                        self.cached
2326                            .transaction_events
2327                            .insert(digest, None, *ticket)
2328                            .ok();
2329                    }
2330                }
2331                results
2332            },
2333        )
2334    }
2335
2336    fn get_unchanged_loaded_runtime_objects(
2337        &self,
2338        digest: &TransactionDigest,
2339    ) -> Option<Vec<ObjectKey>> {
2340        self.dirty
2341            .unchanged_loaded_runtime_objects
2342            .get(digest)
2343            .map(|b| b.clone())
2344            .or_else(|| {
2345                self.store
2346                    .get_unchanged_loaded_runtime_objects(digest)
2347                    .expect("db error")
2348            })
2349    }
2350
2351    fn multi_get_unchanged_loaded_runtime_objects(
2352        &self,
2353        digests: &[TransactionDigest],
2354    ) -> Vec<Option<Vec<ObjectKey>>> {
2355        do_fallback_lookup(
2356            digests,
2357            |digest| match self.dirty.unchanged_loaded_runtime_objects.get(digest) {
2358                Some(objects) => CacheResult::Hit(Some(objects.clone())),
2359                None => CacheResult::Miss,
2360            },
2361            |digests| {
2362                self.store
2363                    .multi_get_unchanged_loaded_runtime_objects(digests)
2364                    .expect("db error")
2365            },
2366        )
2367    }
2368
2369    fn take_accumulator_events(&self, digest: &TransactionDigest) -> Option<Vec<AccumulatorEvent>> {
2370        self.dirty
2371            .pending_transaction_writes
2372            .get(digest)
2373            .map(|transaction_output| transaction_output.take_accumulator_events())
2374    }
2375}
2376
2377impl ExecutionCacheWrite for WritebackCache {
2378    fn validate_owned_object_versions(&self, owned_input_objects: &[ObjectRef]) -> SuiResult {
2379        ObjectLocks::validate_owned_object_versions(self, owned_input_objects)
2380    }
2381
2382    fn write_transaction_outputs(&self, epoch_id: EpochId, tx_outputs: Arc<TransactionOutputs>) {
2383        WritebackCache::write_transaction_outputs(self, epoch_id, tx_outputs);
2384    }
2385
2386    #[cfg(test)]
2387    fn write_object_entry_for_test(&self, object: Object) {
2388        self.write_object_entry(&object.id(), object.version(), object.into());
2389    }
2390}
2391
2392implement_passthrough_traits!(WritebackCache);
2393
2394impl GlobalStateHashStore for WritebackCache {
2395    fn get_object_ref_prior_to_key_deprecated(
2396        &self,
2397        object_id: &ObjectID,
2398        version: SequenceNumber,
2399    ) -> SuiResult<Option<ObjectRef>> {
2400        // There is probably a more efficient way to implement this, but since this is only used by
2401        // old protocol versions, it is better to do the simple thing that is obviously correct.
2402        // In this case we previous version from all sources and choose the highest
2403        let mut candidates = Vec::new();
2404
2405        let check_versions =
2406            |versions: &CachedVersionMap<ObjectEntry>| match versions.get_prior_to(&version) {
2407                Some((version, object_entry)) => match object_entry {
2408                    ObjectEntry::Object(object) => {
2409                        assert_eq!(object.version(), version);
2410                        Some(object.compute_object_reference())
2411                    }
2412                    ObjectEntry::Deleted => {
2413                        Some((*object_id, version, ObjectDigest::OBJECT_DIGEST_DELETED))
2414                    }
2415                    ObjectEntry::Wrapped => {
2416                        Some((*object_id, version, ObjectDigest::OBJECT_DIGEST_WRAPPED))
2417                    }
2418                },
2419                None => None,
2420            };
2421
2422        // first check dirty data
2423        if let Some(objects) = self.dirty.objects.get(object_id)
2424            && let Some(prior) = check_versions(&objects)
2425        {
2426            candidates.push(prior);
2427        }
2428
2429        if let Some(objects) = self.cached.object_cache.get(object_id)
2430            && let Some(prior) = check_versions(&objects.lock())
2431        {
2432            candidates.push(prior);
2433        }
2434
2435        if let Some(prior) = self
2436            .store
2437            .get_object_ref_prior_to_key_deprecated(object_id, version)?
2438        {
2439            candidates.push(prior);
2440        }
2441
2442        // sort candidates by version, and return the highest
2443        candidates.sort_by_key(|(_, version, _)| *version);
2444        Ok(candidates.pop())
2445    }
2446
2447    fn get_root_state_hash_for_epoch(
2448        &self,
2449        epoch: EpochId,
2450    ) -> SuiResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
2451        self.store.get_root_state_hash_for_epoch(epoch)
2452    }
2453
2454    fn insert_state_hash_for_epoch(
2455        &self,
2456        epoch: EpochId,
2457        checkpoint_seq_num: &CheckpointSequenceNumber,
2458        acc: &GlobalStateHash,
2459    ) -> SuiResult {
2460        self.store
2461            .insert_state_hash_for_epoch(epoch, checkpoint_seq_num, acc)
2462    }
2463
2464    fn iter_live_object_set(
2465        &self,
2466        include_wrapped_tombstone: bool,
2467    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
2468        // The only time it is safe to iterate the live object set is at an epoch boundary,
2469        // at which point the db is consistent and the dirty cache is empty. So this does
2470        // read the cache
2471        assert!(
2472            self.dirty.is_empty(),
2473            "cannot iterate live object set with dirty data"
2474        );
2475        self.store.iter_live_object_set(include_wrapped_tombstone)
2476    }
2477
2478    // A version of iter_live_object_set that reads the cache. Only use for testing. If used
2479    // on a live validator, can cause the server to block for as long as it takes to iterate
2480    // the entire live object set.
2481    fn iter_cached_live_object_set_for_testing(
2482        &self,
2483        include_wrapped_tombstone: bool,
2484    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
2485        // hold iter until we are finished to prevent any concurrent inserts/deletes
2486        let iter = self.dirty.objects.iter();
2487        let mut dirty_objects = BTreeMap::new();
2488
2489        // add everything from the store
2490        for obj in self.store.iter_live_object_set(include_wrapped_tombstone) {
2491            dirty_objects.insert(obj.object_id(), obj);
2492        }
2493
2494        // add everything from the cache, but also remove deletions
2495        for entry in iter {
2496            let id = *entry.key();
2497            let value = entry.value();
2498            match value.get_highest().unwrap() {
2499                (_, ObjectEntry::Object(object)) => {
2500                    dirty_objects.insert(id, LiveObject::Normal(object.clone()));
2501                }
2502                (version, ObjectEntry::Wrapped) => {
2503                    if include_wrapped_tombstone {
2504                        dirty_objects.insert(id, LiveObject::Wrapped(ObjectKey(id, *version)));
2505                    } else {
2506                        dirty_objects.remove(&id);
2507                    }
2508                }
2509                (_, ObjectEntry::Deleted) => {
2510                    dirty_objects.remove(&id);
2511                }
2512            }
2513        }
2514
2515        Box::new(dirty_objects.into_values())
2516    }
2517}
2518
2519// TODO: For correctness, we must at least invalidate the cache when items are written through this
2520// trait (since they could be negatively cached as absent). But it may or may not be optimal to
2521// actually insert them into the cache. For instance if state sync is running ahead of execution,
2522// they might evict other items that are about to be read. This could be an area for tuning in the
2523// future.
2524impl StateSyncAPI for WritebackCache {
2525    fn insert_transaction_and_effects(
2526        &self,
2527        transaction: &VerifiedTransaction,
2528        transaction_effects: &TransactionEffects,
2529    ) {
2530        self.store
2531            .insert_transaction_and_effects(transaction, transaction_effects)
2532            .expect("db error");
2533        self.cached
2534            .transactions
2535            .insert(
2536                transaction.digest(),
2537                PointCacheItem::Some(Arc::new(transaction.clone())),
2538                Ticket::Write,
2539            )
2540            .ok();
2541        self.cached
2542            .transaction_effects
2543            .insert(
2544                &transaction_effects.digest(),
2545                PointCacheItem::Some(Arc::new(transaction_effects.clone())),
2546                Ticket::Write,
2547            )
2548            .ok();
2549    }
2550
2551    fn multi_insert_transaction_and_effects(
2552        &self,
2553        transactions_and_effects: &[VerifiedExecutionData],
2554    ) {
2555        self.store
2556            .multi_insert_transaction_and_effects(transactions_and_effects.iter())
2557            .expect("db error");
2558        for VerifiedExecutionData {
2559            transaction,
2560            effects,
2561        } in transactions_and_effects
2562        {
2563            self.cached
2564                .transactions
2565                .insert(
2566                    transaction.digest(),
2567                    PointCacheItem::Some(Arc::new(transaction.clone())),
2568                    Ticket::Write,
2569                )
2570                .ok();
2571            self.cached
2572                .transaction_effects
2573                .insert(
2574                    &effects.digest(),
2575                    PointCacheItem::Some(Arc::new(effects.clone())),
2576                    Ticket::Write,
2577                )
2578                .ok();
2579        }
2580    }
2581}