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