Skip to main content

iota_core/execution_cache/
writeback_cache.rs

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