diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 33f348af1ea958..ae430c12246d37 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2688,6 +2688,7 @@ impl Bank { } /// Forget all signatures. Useful for benchmarking. + #[cfg(feature = "dev-context-only-utils")] pub fn clear_signatures(&self) { self.status_cache.write().unwrap().clear(); } diff --git a/runtime/src/status_cache.rs b/runtime/src/status_cache.rs index a0a7c708187d6f..f9c049f8b14748 100644 --- a/runtime/src/status_cache.rs +++ b/runtime/src/status_cache.rs @@ -15,21 +15,30 @@ use { std::sync::{Arc, Mutex}, }; +// The maximum number of entries to store in the cache. This is the same as the number of recent +// blockhashes because we automatically reject txs that use older blockhashes so we don't need to +// track those explicitly. pub const MAX_CACHE_ENTRIES: usize = MAX_RECENT_BLOCKHASHES; + +// Only store 20 bytes of the tx keys processed to save some memory. const CACHED_KEY_SIZE: usize = 20; -// Store forks in a single chunk of memory to avoid another lookup. +// Store forks in a single chunk of memory to avoid another hash lookup. pub type ForkStatus = Vec<(Slot, T)>; + +// The type of the key used in the cache. pub(crate) type KeySlice = [u8; CACHED_KEY_SIZE]; + type KeyMap = HashMap>; + // Map of Hash and status pub type Status = Arc)>>>; + // A Map of hash + the highest fork it's been observed on along with // the key offset and a Map of the key slice + Fork status for that key type KeyStatusMap = HashMap)>; -// A map of keys recorded in each fork; used to serialize for snapshots easily. -// Doesn't store a `SlotDelta` in it because the bool `root` is usually set much later +// The type used for StatusCache::slot_deltas. See the field definition for more details. type SlotDeltaMap = HashMap>; // The statuses added during a slot, can be used to build on top of a status cache or to @@ -39,9 +48,12 @@ pub type SlotDelta = (Slot, bool, Status); #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Clone, Debug)] pub struct StatusCache { + // cache[blockhash][tx_key] => [(fork1_slot, tx_result), (fork2_slot, tx_result), ...] used to + // check if a tx_key was seen on a fork and for rpc to retrieve the tx_result cache: KeyStatusMap, roots: HashSet, - /// all keys seen during a fork/slot + // slot_deltas[slot][blockhash] => [(tx_key, tx_result), ...] used to serialize for snapshots + // and to rebuild cache[blockhash][tx_key] from a snapshot slot_deltas: SlotDeltaMap, } @@ -56,33 +68,15 @@ impl Default for StatusCache { } } -impl PartialEq for StatusCache { - fn eq(&self, other: &Self) -> bool { - self.roots == other.roots - && self - .cache - .iter() - .all(|(hash, (slot, key_index, hash_map))| { - if let Some((other_slot, other_key_index, other_hash_map)) = - other.cache.get(hash) - { - if slot == other_slot && key_index == other_key_index { - return hash_map.iter().all(|(slice, fork_map)| { - if let Some(other_fork_map) = other_hash_map.get(slice) { - // all this work just to compare the highest forks in the fork map - // per entry - return fork_map.last() == other_fork_map.last(); - } - false - }); - } - } - false - }) - } -} - impl StatusCache { + /// Clear all entries for a slot. + /// + /// This is used when a slot is purged from the cache, see + /// ReplayStage::purge_unconfirmed_duplicate_slot(). + /// + /// When this is called, it's guaranteed that there are no threads inserting new entries for + /// this slot. root_slot_deltas() also never accesses slots that are being cleared because roots + /// are never purged. pub fn clear_slot_entries(&mut self, slot: Slot) { let slot_deltas = self.slot_deltas.remove(&slot); if let Some(slot_deltas) = slot_deltas { @@ -146,9 +140,9 @@ impl StatusCache { None } - /// Search for a key with any blockhash - /// Prefer get_status for performance reasons, it doesn't need - /// to search all blockhashes. + /// Search for a key with any blockhash. + /// + /// Prefer get_status for performance reasons, it doesn't need to search all blockhashes. pub fn get_status_any_blockhash>( &self, key: K, @@ -166,8 +160,10 @@ impl StatusCache { None } - /// Add a known root fork. Roots are always valid ancestors. - /// After MAX_CACHE_ENTRIES, roots are removed, and any old keys are cleared. + /// Add a known root fork. + /// + /// Roots are always valid ancestors. After MAX_CACHE_ENTRIES, roots are removed, and any old + /// keys are cleared. pub fn add_root(&mut self, fork: Slot) { self.roots.insert(fork); self.purge_roots(); @@ -177,7 +173,7 @@ impl StatusCache { &self.roots } - /// Insert a new key for a specific slot. + /// Insert a new key using the given blockhash at the given slot. pub fn insert>( &mut self, transaction_blockhash: &Hash, @@ -224,7 +220,7 @@ impl StatusCache { } } - /// Clear for testing + #[cfg(feature = "dev-context-only-utils")] pub fn clear(&mut self) { for v in self.cache.values_mut() { v.2 = HashMap::new(); @@ -235,7 +231,13 @@ impl StatusCache { .for_each(|(_, status)| status.lock().unwrap().clear()); } - /// Get the statuses for all the root slots + /// Get the statuses for all the root slots. + /// + /// This is never called concurrently with add_root(), and for a slot to be a root there must be + /// no new entries for that slot, so there are no races. + /// + /// See ReplayStage::handle_new_root() => BankForks::set_root() => + /// BankForks::do_set_root_return_metrics() => root_slot_deltas() pub fn root_slot_deltas(&self) -> Vec> { self.roots() .iter() @@ -249,7 +251,10 @@ impl StatusCache { .collect() } - // replay deltas into a status_cache allows "appending" data + /// Populate the cache with the slot deltas from a snapshot. + /// + /// Really badly named method. See load_bank_forks() => ... => + /// rebuild_bank_from_snapshot() => [load slot deltas from snapshot] => append() pub fn append(&mut self, slot_deltas: &[SlotDelta]) { for (slot, is_root, statuses) in slot_deltas { statuses @@ -267,13 +272,6 @@ impl StatusCache { } } - pub fn from_slot_deltas(slot_deltas: &[SlotDelta]) -> Self { - // play all deltas back into the status cache - let mut me = Self::default(); - me.append(slot_deltas); - me - } - fn insert_with_slice( &mut self, transaction_blockhash: &Hash, @@ -294,8 +292,7 @@ impl StatusCache { self.add_to_slot_delta(transaction_blockhash, slot, key_index, key_slice, res); } - // Add this key slice to the list of key slices for this slot and blockhash - // combo. + // Add this key slice to the list of key slices for this slot and blockhash combo. fn add_to_slot_delta( &mut self, transaction_blockhash: &Hash, @@ -318,6 +315,40 @@ mod tests { type BankStatusCache = StatusCache<()>; + impl StatusCache { + fn from_slot_deltas(slot_deltas: &[SlotDelta]) -> Self { + let mut cache = Self::default(); + cache.append(slot_deltas); + cache + } + } + + impl PartialEq for StatusCache { + fn eq(&self, other: &Self) -> bool { + self.roots == other.roots + && self + .cache + .iter() + .all(|(hash, (slot, key_index, hash_map))| { + if let Some((other_slot, other_key_index, other_hash_map)) = + other.cache.get(hash) + { + if slot == other_slot && key_index == other_key_index { + return hash_map.iter().all(|(slice, fork_map)| { + if let Some(other_fork_map) = other_hash_map.get(slice) { + // all this work just to compare the highest forks in the fork map + // per entry + return fork_map.last() == other_fork_map.last(); + } + false + }); + } + } + false + }) + } + } + #[test] fn test_empty_has_no_sigs() { let sig = Signature::default();