Co-authored-by: Carl Lin <carl@solana.com>
This commit is contained in:
@ -100,8 +100,23 @@ impl SnapshotRequestHandler {
|
|||||||
|
|
||||||
let mut flush_accounts_cache_time = Measure::start("flush_accounts_cache_time");
|
let mut flush_accounts_cache_time = Measure::start("flush_accounts_cache_time");
|
||||||
if accounts_db_caching_enabled {
|
if accounts_db_caching_enabled {
|
||||||
// Force flush all the roots from the cache so that the snapshot can be taken.
|
// Forced cache flushing MUST flush all roots <= snapshot_root_bank.slot().
|
||||||
|
// That's because `snapshot_root_bank.slot()` must be root at this point,
|
||||||
|
// and contains relevant updates because each bank has at least 1 account update due
|
||||||
|
// to sysvar maintenance. Otherwise, this would cause missing storages in the snapshot
|
||||||
snapshot_root_bank.force_flush_accounts_cache();
|
snapshot_root_bank.force_flush_accounts_cache();
|
||||||
|
// Ensure all roots <= `self.slot()` have been flushed.
|
||||||
|
// Note `max_flush_root` could be larger than self.slot() if there are
|
||||||
|
// `> MAX_CACHE_SLOT` cached and rooted slots which triggered earlier flushes.
|
||||||
|
assert!(
|
||||||
|
snapshot_root_bank.slot()
|
||||||
|
<= snapshot_root_bank
|
||||||
|
.rc
|
||||||
|
.accounts
|
||||||
|
.accounts_db
|
||||||
|
.accounts_cache
|
||||||
|
.fetch_max_flush_root()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
flush_accounts_cache_time.stop();
|
flush_accounts_cache_time.stop();
|
||||||
|
|
||||||
|
@ -2996,27 +2996,31 @@ impl AccountsDB {
|
|||||||
self.accounts_cache.report_size();
|
self.accounts_cache.report_size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// `force_flush` flushes all the cached roots `<= max_clean_root`. It also then
|
// `force_flush` flushes all the cached roots `<= requested_flush_root`. It also then
|
||||||
// flushes:
|
// flushes:
|
||||||
// 1) Any remaining roots if there are > MAX_CACHE_SLOTS remaining slots in the cache,
|
// 1) Any remaining roots if there are > MAX_CACHE_SLOTS remaining slots in the cache,
|
||||||
// 2) It there are still > MAX_CACHE_SLOTS remaining slots in the cache, the excess
|
// 2) It there are still > MAX_CACHE_SLOTS remaining slots in the cache, the excess
|
||||||
// unrooted slots
|
// unrooted slots
|
||||||
pub fn flush_accounts_cache(&self, force_flush: bool, max_clean_root: Option<Slot>) {
|
pub fn flush_accounts_cache(&self, force_flush: bool, requested_flush_root: Option<Slot>) {
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
assert!(max_clean_root.is_some());
|
assert!(requested_flush_root.is_some());
|
||||||
|
|
||||||
if !force_flush && self.accounts_cache.num_slots() <= MAX_CACHE_SLOTS {
|
if !force_flush && self.accounts_cache.num_slots() <= MAX_CACHE_SLOTS {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush only the roots <= max_clean_root, so that snapshotting has all
|
// Flush only the roots <= requested_flush_root, so that snapshotting has all
|
||||||
// the relevant roots in storage.
|
// the relevant roots in storage.
|
||||||
let mut flush_roots_elapsed = Measure::start("flush_roots_elapsed");
|
let mut flush_roots_elapsed = Measure::start("flush_roots_elapsed");
|
||||||
let mut account_bytes_saved = 0;
|
let mut account_bytes_saved = 0;
|
||||||
let mut num_accounts_saved = 0;
|
let mut num_accounts_saved = 0;
|
||||||
|
|
||||||
|
// Note even if force_flush is false, we will still flush all roots <= the
|
||||||
|
// given `requested_flush_root`, even if some of the later roots cannot be used for
|
||||||
|
// cleaning due to an ongoing scan
|
||||||
let (total_new_cleaned_roots, num_cleaned_roots_flushed) = self
|
let (total_new_cleaned_roots, num_cleaned_roots_flushed) = self
|
||||||
.flush_rooted_accounts_cache(
|
.flush_rooted_accounts_cache(
|
||||||
max_clean_root,
|
requested_flush_root,
|
||||||
Some((&mut account_bytes_saved, &mut num_accounts_saved)),
|
Some((&mut account_bytes_saved, &mut num_accounts_saved)),
|
||||||
);
|
);
|
||||||
flush_roots_elapsed.stop();
|
flush_roots_elapsed.stop();
|
||||||
@ -3030,7 +3034,7 @@ impl AccountsDB {
|
|||||||
if self.accounts_cache.num_slots() > MAX_CACHE_SLOTS {
|
if self.accounts_cache.num_slots() > MAX_CACHE_SLOTS {
|
||||||
// Start by flushing the roots
|
// Start by flushing the roots
|
||||||
//
|
//
|
||||||
// Cannot do any cleaning on roots past `max_clean_root` because future
|
// Cannot do any cleaning on roots past `requested_flush_root` because future
|
||||||
// snapshots may need updates from those later slots, hence we pass `None`
|
// snapshots may need updates from those later slots, hence we pass `None`
|
||||||
// for `should_clean`.
|
// for `should_clean`.
|
||||||
self.flush_rooted_accounts_cache(None, None)
|
self.flush_rooted_accounts_cache(None, None)
|
||||||
@ -3093,16 +3097,14 @@ impl AccountsDB {
|
|||||||
|
|
||||||
fn flush_rooted_accounts_cache(
|
fn flush_rooted_accounts_cache(
|
||||||
&self,
|
&self,
|
||||||
mut max_flush_root: Option<Slot>,
|
requested_flush_root: Option<Slot>,
|
||||||
should_clean: Option<(&mut usize, &mut usize)>,
|
should_clean: Option<(&mut usize, &mut usize)>,
|
||||||
) -> (usize, usize) {
|
) -> (usize, usize) {
|
||||||
if should_clean.is_some() {
|
let max_clean_root = should_clean.as_ref().and_then(|_| {
|
||||||
max_flush_root = self.max_clean_root(max_flush_root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a long running scan going on, this could prevent any cleaning
|
// If there is a long running scan going on, this could prevent any cleaning
|
||||||
// past `max_flush_root`.
|
// based on updates from slots > `max_clean_root`.
|
||||||
let cached_roots: BTreeSet<Slot> = self.accounts_cache.clear_roots(max_flush_root);
|
self.max_clean_root(requested_flush_root)
|
||||||
|
});
|
||||||
|
|
||||||
// Use HashMap because HashSet doesn't provide Entry api
|
// Use HashMap because HashSet doesn't provide Entry api
|
||||||
let mut written_accounts = HashMap::new();
|
let mut written_accounts = HashMap::new();
|
||||||
@ -3112,7 +3114,6 @@ impl AccountsDB {
|
|||||||
let mut should_flush_f = should_clean.map(|(account_bytes_saved, num_accounts_saved)| {
|
let mut should_flush_f = should_clean.map(|(account_bytes_saved, num_accounts_saved)| {
|
||||||
move |&pubkey: &Pubkey, account: &Account| {
|
move |&pubkey: &Pubkey, account: &Account| {
|
||||||
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
||||||
|
|
||||||
let should_flush = match written_accounts.entry(pubkey) {
|
let should_flush = match written_accounts.entry(pubkey) {
|
||||||
Vacant(vacant_entry) => {
|
Vacant(vacant_entry) => {
|
||||||
vacant_entry.insert(());
|
vacant_entry.insert(());
|
||||||
@ -3130,11 +3131,27 @@ impl AccountsDB {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Always flush up to `requested_flush_root`, which is necessary for things like snapshotting.
|
||||||
|
let cached_roots: BTreeSet<Slot> = self.accounts_cache.clear_roots(requested_flush_root);
|
||||||
|
|
||||||
// Iterate from highest to lowest so that we don't need to flush earlier
|
// Iterate from highest to lowest so that we don't need to flush earlier
|
||||||
// outdated updates in earlier roots
|
// outdated updates in earlier roots
|
||||||
let mut num_roots_flushed = 0;
|
let mut num_roots_flushed = 0;
|
||||||
for &root in cached_roots.iter().rev() {
|
for &root in cached_roots.iter().rev() {
|
||||||
if self.flush_slot_cache(root, should_flush_f.as_mut()) {
|
let should_flush_f = if let Some(max_clean_root) = max_clean_root {
|
||||||
|
if root > max_clean_root {
|
||||||
|
// Only if the root is greater than the `max_clean_root` do we
|
||||||
|
// have to prevent cleaning, otherwise, just default to `should_flush_f`
|
||||||
|
// for any slots <= `max_clean_root`
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
should_flush_f.as_mut()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
should_flush_f.as_mut()
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.flush_slot_cache(root, should_flush_f) {
|
||||||
num_roots_flushed += 1;
|
num_roots_flushed += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4390,7 +4407,7 @@ pub mod tests {
|
|||||||
use std::{
|
use std::{
|
||||||
iter::FromIterator,
|
iter::FromIterator,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
thread::{sleep, Builder},
|
thread::{self, sleep, Builder, JoinHandle},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -7431,54 +7448,37 @@ pub mod tests {
|
|||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
struct ScanTracker {
|
||||||
fn test_scan_flush_accounts_cache_then_clean_drop() {
|
t_scan: JoinHandle<()>,
|
||||||
let caching_enabled = true;
|
exit: Arc<AtomicBool>,
|
||||||
let db = Arc::new(AccountsDB::new_with_config(
|
}
|
||||||
Vec::new(),
|
|
||||||
&ClusterType::Development,
|
impl ScanTracker {
|
||||||
HashSet::new(),
|
fn exit(self) -> thread::Result<()> {
|
||||||
caching_enabled,
|
self.exit.store(true, Ordering::Relaxed);
|
||||||
));
|
self.t_scan.join()
|
||||||
let db_ = db.clone();
|
}
|
||||||
let account_key = Pubkey::new_unique();
|
}
|
||||||
let account_key2 = Pubkey::new_unique();
|
|
||||||
let zero_lamport_account = Account::new(0, 0, &Account::default().owner);
|
fn setup_scan(
|
||||||
let slot1_account = Account::new(1, 1, &Account::default().owner);
|
db: Arc<AccountsDB>,
|
||||||
let slot2_account = Account::new(2, 1, &Account::default().owner);
|
scan_ancestors: Arc<Ancestors>,
|
||||||
|
stall_key: Pubkey,
|
||||||
|
) -> ScanTracker {
|
||||||
let exit = Arc::new(AtomicBool::new(false));
|
let exit = Arc::new(AtomicBool::new(false));
|
||||||
let exit_ = exit.clone();
|
let exit_ = exit.clone();
|
||||||
let ready = Arc::new(AtomicBool::new(false));
|
let ready = Arc::new(AtomicBool::new(false));
|
||||||
let ready_ = ready.clone();
|
let ready_ = ready.clone();
|
||||||
|
|
||||||
/*
|
|
||||||
Store zero lamport account into slots 0, 1, 2 where
|
|
||||||
root slots are 0, 2, and slot 1 is unrooted.
|
|
||||||
0 (root)
|
|
||||||
/ \
|
|
||||||
1 2 (root)
|
|
||||||
*/
|
|
||||||
db.store_cached(0, &[(&account_key, &zero_lamport_account)]);
|
|
||||||
db.store_cached(1, &[(&account_key, &slot1_account)]);
|
|
||||||
db.store_cached(2, &[(&account_key, &slot2_account)]);
|
|
||||||
// Fodder for the scan so that the lock on `account_key` is not held
|
|
||||||
db.store_cached(2, &[(&account_key2, &slot2_account)]);
|
|
||||||
db.get_accounts_delta_hash(0);
|
|
||||||
db.add_root(0);
|
|
||||||
let max_scan_root = 0;
|
|
||||||
let scan_ancestors: Arc<Ancestors> = Arc::new(vec![(0, 1), (1, 1)].into_iter().collect());
|
|
||||||
let scan_ancestors_ = scan_ancestors.clone();
|
|
||||||
let t_scan = Builder::new()
|
let t_scan = Builder::new()
|
||||||
.name("scan".to_string())
|
.name("scan".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
db_.scan_accounts(
|
db.scan_accounts(
|
||||||
&scan_ancestors_,
|
&scan_ancestors,
|
||||||
|_collector: &mut Vec<(Pubkey, Account)>, maybe_account| {
|
|_collector: &mut Vec<(Pubkey, Account)>, maybe_account| {
|
||||||
ready_.store(true, Ordering::Relaxed);
|
ready_.store(true, Ordering::Relaxed);
|
||||||
if let Some((pubkey, _, _)) = maybe_account {
|
if let Some((pubkey, _, _)) = maybe_account {
|
||||||
// Do the wait on account_key2, because clean is happening
|
if *pubkey == stall_key {
|
||||||
// on account_key1's index and we don't want to block the clean.
|
|
||||||
if *pubkey == account_key2 {
|
|
||||||
loop {
|
loop {
|
||||||
if exit_.load(Ordering::Relaxed) {
|
if exit_.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
@ -7498,15 +7498,68 @@ pub mod tests {
|
|||||||
sleep(Duration::from_millis(10));
|
sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new root 2
|
ScanTracker { t_scan, exit }
|
||||||
db.get_accounts_delta_hash(2);
|
}
|
||||||
db.add_root(2);
|
|
||||||
|
|
||||||
// Flush the cache, slot 1 should remain in the cache, everything else should be flushed
|
#[test]
|
||||||
db.flush_accounts_cache(true, None);
|
fn test_scan_flush_accounts_cache_then_clean_drop() {
|
||||||
|
let caching_enabled = true;
|
||||||
|
let db = Arc::new(AccountsDB::new_with_config(
|
||||||
|
Vec::new(),
|
||||||
|
&ClusterType::Development,
|
||||||
|
HashSet::new(),
|
||||||
|
caching_enabled,
|
||||||
|
));
|
||||||
|
let account_key = Pubkey::new_unique();
|
||||||
|
let account_key2 = Pubkey::new_unique();
|
||||||
|
let zero_lamport_account = Account::new(0, 0, &Account::default().owner);
|
||||||
|
let slot1_account = Account::new(1, 1, &Account::default().owner);
|
||||||
|
let slot2_account = Account::new(2, 1, &Account::default().owner);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Store zero lamport account into slots 0, 1, 2 where
|
||||||
|
root slots are 0, 2, and slot 1 is unrooted.
|
||||||
|
0 (root)
|
||||||
|
/ \
|
||||||
|
1 2 (root)
|
||||||
|
*/
|
||||||
|
db.store_cached(0, &[(&account_key, &zero_lamport_account)]);
|
||||||
|
db.store_cached(1, &[(&account_key, &slot1_account)]);
|
||||||
|
// Fodder for the scan so that the lock on `account_key` is not held
|
||||||
|
db.store_cached(1, &[(&account_key2, &slot1_account)]);
|
||||||
|
db.store_cached(2, &[(&account_key, &slot2_account)]);
|
||||||
|
db.get_accounts_delta_hash(0);
|
||||||
|
|
||||||
|
let max_scan_root = 0;
|
||||||
|
db.add_root(max_scan_root);
|
||||||
|
let scan_ancestors: Arc<Ancestors> = Arc::new(vec![(0, 1), (1, 1)].into_iter().collect());
|
||||||
|
let scan_tracker = setup_scan(db.clone(), scan_ancestors.clone(), account_key2);
|
||||||
|
|
||||||
|
// Add a new root 2
|
||||||
|
let new_root = 2;
|
||||||
|
db.get_accounts_delta_hash(new_root);
|
||||||
|
db.add_root(new_root);
|
||||||
|
|
||||||
|
// Check that the scan is properly set up
|
||||||
|
assert_eq!(
|
||||||
|
db.accounts_index.min_ongoing_scan_root().unwrap(),
|
||||||
|
max_scan_root
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we specify a requested_flush_root == 2, then `slot 2 <= max_flush_slot` will
|
||||||
|
// be flushed even though `slot 2 > max_scan_root`. The unrooted slot 1 should
|
||||||
|
// remain in the cache
|
||||||
|
db.flush_accounts_cache(true, Some(new_root));
|
||||||
assert_eq!(db.accounts_cache.num_slots(), 1);
|
assert_eq!(db.accounts_cache.num_slots(), 1);
|
||||||
assert!(db.accounts_cache.slot_cache(1).is_some());
|
assert!(db.accounts_cache.slot_cache(1).is_some());
|
||||||
|
|
||||||
|
// Intra cache cleaning should not clean the entry for `account_key` from slot 0,
|
||||||
|
// even though it was updated in slot `2` because of the ongoing scan
|
||||||
|
let account = db
|
||||||
|
.do_load(&Ancestors::default(), &account_key, Some(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(account.0.lamports, zero_lamport_account.lamports);
|
||||||
|
|
||||||
// Run clean, unrooted slot 1 should not be purged, and still readable from the cache,
|
// Run clean, unrooted slot 1 should not be purged, and still readable from the cache,
|
||||||
// because we're still doing a scan on it.
|
// because we're still doing a scan on it.
|
||||||
db.clean_accounts(None);
|
db.clean_accounts(None);
|
||||||
@ -7517,8 +7570,7 @@ pub mod tests {
|
|||||||
|
|
||||||
// When the scan is over, clean should not panic and should not purge something
|
// When the scan is over, clean should not panic and should not purge something
|
||||||
// still in the cache.
|
// still in the cache.
|
||||||
exit.store(true, Ordering::Relaxed);
|
scan_tracker.exit().unwrap();
|
||||||
t_scan.join().unwrap();
|
|
||||||
db.clean_accounts(None);
|
db.clean_accounts(None);
|
||||||
let account = db
|
let account = db
|
||||||
.do_load(&scan_ancestors, &account_key, Some(max_scan_root))
|
.do_load(&scan_ancestors, &account_key, Some(max_scan_root))
|
||||||
@ -7585,26 +7637,49 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_accounts_db_cache_clean(num_slots: usize) -> (AccountsDB, Vec<Pubkey>, Vec<Slot>) {
|
fn setup_accounts_db_cache_clean(
|
||||||
|
num_slots: usize,
|
||||||
|
scan_slot: Option<Slot>,
|
||||||
|
) -> (Arc<AccountsDB>, Vec<Pubkey>, Vec<Slot>, Option<ScanTracker>) {
|
||||||
let caching_enabled = true;
|
let caching_enabled = true;
|
||||||
let accounts_db = AccountsDB::new_with_config(
|
let accounts_db = Arc::new(AccountsDB::new_with_config(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
&ClusterType::Development,
|
&ClusterType::Development,
|
||||||
HashSet::new(),
|
HashSet::new(),
|
||||||
caching_enabled,
|
caching_enabled,
|
||||||
);
|
));
|
||||||
let slots: Vec<_> = (0..num_slots as Slot).into_iter().collect();
|
let slots: Vec<_> = (0..num_slots as Slot).into_iter().collect();
|
||||||
|
let stall_slot = num_slots as Slot;
|
||||||
|
let scan_stall_key = Pubkey::new_unique();
|
||||||
let keys: Vec<Pubkey> = std::iter::repeat_with(Pubkey::new_unique)
|
let keys: Vec<Pubkey> = std::iter::repeat_with(Pubkey::new_unique)
|
||||||
.take(num_slots)
|
.take(num_slots)
|
||||||
.collect();
|
.collect();
|
||||||
|
if scan_slot.is_some() {
|
||||||
|
accounts_db.store_cached(
|
||||||
|
// Store it in a slot that isn't returned in `slots`
|
||||||
|
stall_slot,
|
||||||
|
&[(&scan_stall_key, &Account::new(1, 0, &Pubkey::default()))],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Store some subset of the keys in slots 0..num_slots
|
// Store some subset of the keys in slots 0..num_slots
|
||||||
|
let mut scan_tracker = None;
|
||||||
for slot in &slots {
|
for slot in &slots {
|
||||||
for key in &keys[*slot as usize..] {
|
for key in &keys[*slot as usize..] {
|
||||||
accounts_db.store_cached(*slot, &[(key, &Account::new(1, 0, &Pubkey::default()))]);
|
accounts_db.store_cached(*slot, &[(key, &Account::new(1, 0, &Pubkey::default()))]);
|
||||||
}
|
}
|
||||||
accounts_db.add_root(*slot as Slot);
|
accounts_db.add_root(*slot as Slot);
|
||||||
|
if Some(*slot) == scan_slot {
|
||||||
|
let ancestors = Arc::new(vec![(stall_slot, 1), (*slot, 1)].into_iter().collect());
|
||||||
|
scan_tracker = Some(setup_scan(accounts_db.clone(), ancestors, scan_stall_key));
|
||||||
|
assert_eq!(
|
||||||
|
accounts_db.accounts_index.min_ongoing_scan_root().unwrap(),
|
||||||
|
*slot
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts_db.accounts_cache.remove_slot(stall_slot);
|
||||||
|
|
||||||
// If there's <= MAX_CACHE_SLOTS, no slots should be flushed
|
// If there's <= MAX_CACHE_SLOTS, no slots should be flushed
|
||||||
if accounts_db.accounts_cache.num_slots() <= MAX_CACHE_SLOTS {
|
if accounts_db.accounts_cache.num_slots() <= MAX_CACHE_SLOTS {
|
||||||
@ -7612,13 +7687,13 @@ pub mod tests {
|
|||||||
assert_eq!(accounts_db.accounts_cache.num_slots(), num_slots);
|
assert_eq!(accounts_db.accounts_cache.num_slots(), num_slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
(accounts_db, keys, slots)
|
(accounts_db, keys, slots, scan_tracker)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_accounts_db_cache_clean_dead_slots() {
|
fn test_accounts_db_cache_clean_dead_slots() {
|
||||||
let num_slots = 10;
|
let num_slots = 10;
|
||||||
let (accounts_db, keys, mut slots) = setup_accounts_db_cache_clean(num_slots);
|
let (accounts_db, keys, mut slots, _) = setup_accounts_db_cache_clean(num_slots, None);
|
||||||
let last_dead_slot = (num_slots - 1) as Slot;
|
let last_dead_slot = (num_slots - 1) as Slot;
|
||||||
assert_eq!(*slots.last().unwrap(), last_dead_slot);
|
assert_eq!(*slots.last().unwrap(), last_dead_slot);
|
||||||
let alive_slot = last_dead_slot as Slot + 1;
|
let alive_slot = last_dead_slot as Slot + 1;
|
||||||
@ -7685,7 +7760,7 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_accounts_db_cache_clean() {
|
fn test_accounts_db_cache_clean() {
|
||||||
let (accounts_db, keys, slots) = setup_accounts_db_cache_clean(10);
|
let (accounts_db, keys, slots, _) = setup_accounts_db_cache_clean(10, None);
|
||||||
|
|
||||||
// If no `max_clean_root` is specified, cleaning should purge all flushed slots
|
// If no `max_clean_root` is specified, cleaning should purge all flushed slots
|
||||||
accounts_db.flush_accounts_cache(true, None);
|
accounts_db.flush_accounts_cache(true, None);
|
||||||
@ -7719,21 +7794,27 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_test_accounts_db_cache_clean_max_root(num_slots: usize, max_clean_root: Slot) {
|
fn run_test_accounts_db_cache_clean_max_root(
|
||||||
assert!(max_clean_root < (num_slots as Slot));
|
num_slots: usize,
|
||||||
let (accounts_db, keys, slots) = setup_accounts_db_cache_clean(num_slots);
|
requested_flush_root: Slot,
|
||||||
let is_cache_at_limit = num_slots - max_clean_root as usize - 1 > MAX_CACHE_SLOTS;
|
scan_root: Option<Slot>,
|
||||||
|
) {
|
||||||
|
assert!(requested_flush_root < (num_slots as Slot));
|
||||||
|
let (accounts_db, keys, slots, scan_tracker) =
|
||||||
|
setup_accounts_db_cache_clean(num_slots, scan_root);
|
||||||
|
let is_cache_at_limit = num_slots - requested_flush_root as usize - 1 > MAX_CACHE_SLOTS;
|
||||||
|
|
||||||
// If:
|
// If:
|
||||||
// 1) `max_clean_root` is specified,
|
// 1) `requested_flush_root` is specified,
|
||||||
// 2) not at the cache limit, i.e. `is_cache_at_limit == false`, then
|
// 2) not at the cache limit, i.e. `is_cache_at_limit == false`, then
|
||||||
// `flush_accounts_cache()` should clean and flushed only slots < max_clean_root,
|
// `flush_accounts_cache()` should clean and flush only slots <= requested_flush_root,
|
||||||
accounts_db.flush_accounts_cache(true, Some(max_clean_root));
|
accounts_db.flush_accounts_cache(true, Some(requested_flush_root));
|
||||||
|
|
||||||
if !is_cache_at_limit {
|
if !is_cache_at_limit {
|
||||||
// Should flush all slots between 0..=max_clean_root
|
// Should flush all slots between 0..=requested_flush_root
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
accounts_db.accounts_cache.num_slots(),
|
accounts_db.accounts_cache.num_slots(),
|
||||||
slots.len() - max_clean_root as usize - 1
|
slots.len() - requested_flush_root as usize - 1
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, if we are at the cache limit, all roots will be flushed
|
// Otherwise, if we are at the cache limit, all roots will be flushed
|
||||||
@ -7747,9 +7828,9 @@ pub mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
uncleaned_roots.sort_unstable();
|
uncleaned_roots.sort_unstable();
|
||||||
|
|
||||||
let expected_max_clean_root = if !is_cache_at_limit {
|
let expected_max_flushed_root = if !is_cache_at_limit {
|
||||||
// Should flush all slots between 0..=max_clean_root
|
// Should flush all slots between 0..=requested_flush_root
|
||||||
max_clean_root
|
requested_flush_root
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, if we are at the cache limit, all roots will be flushed
|
// Otherwise, if we are at the cache limit, all roots will be flushed
|
||||||
num_slots as Slot - 1
|
num_slots as Slot - 1
|
||||||
@ -7757,14 +7838,13 @@ pub mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
uncleaned_roots,
|
uncleaned_roots,
|
||||||
slots[0..=expected_max_clean_root as usize].to_vec()
|
slots[0..=expected_max_flushed_root as usize].to_vec()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
accounts_db.accounts_cache.fetch_max_flush_root(),
|
accounts_db.accounts_cache.fetch_max_flush_root(),
|
||||||
expected_max_clean_root,
|
expected_max_flushed_root,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Updates from slots > max_clean_root should still be flushed to storage
|
|
||||||
for slot in &slots {
|
for slot in &slots {
|
||||||
let slot_accounts = accounts_db.scan_account_storage(
|
let slot_accounts = accounts_db.scan_account_storage(
|
||||||
*slot as Slot,
|
*slot as Slot,
|
||||||
@ -7774,7 +7854,9 @@ pub mod tests {
|
|||||||
"When cache is at limit, all roots should have been flushed to storage"
|
"When cache is at limit, all roots should have been flushed to storage"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert!(*slot > max_clean_root);
|
// All slots <= requested_flush_root should have been flushed, regardless
|
||||||
|
// of ongoing scans
|
||||||
|
assert!(*slot > requested_flush_root);
|
||||||
Some(*loaded_account.pubkey())
|
Some(*loaded_account.pubkey())
|
||||||
},
|
},
|
||||||
|slot_accounts: &DashSet<Pubkey>, loaded_account: LoadedAccount| {
|
|slot_accounts: &DashSet<Pubkey>, loaded_account: LoadedAccount| {
|
||||||
@ -7782,7 +7864,7 @@ pub mod tests {
|
|||||||
if !is_cache_at_limit {
|
if !is_cache_at_limit {
|
||||||
// Only true when the limit hasn't been reached and there are still
|
// Only true when the limit hasn't been reached and there are still
|
||||||
// slots left in the cache
|
// slots left in the cache
|
||||||
assert!(*slot <= max_clean_root);
|
assert!(*slot <= requested_flush_root);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -7795,56 +7877,93 @@ pub mod tests {
|
|||||||
slot_accounts.into_iter().collect::<HashSet<Pubkey>>()
|
slot_accounts.into_iter().collect::<HashSet<Pubkey>>()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if *slot >= max_clean_root {
|
|
||||||
// 1) If slot > `max_clean_root`, then either:
|
let expected_accounts =
|
||||||
// a) If `is_cache_at_limit == true`, still in the cache
|
if *slot >= requested_flush_root || *slot >= scan_root.unwrap_or(Slot::MAX) {
|
||||||
// b) if `is_cache_at_limit == false`, were not cleaned before being flushed to storage.
|
// 1) If slot > `requested_flush_root`, then either:
|
||||||
|
// a) If `is_cache_at_limit == false`, still in the cache
|
||||||
|
// b) if `is_cache_at_limit == true`, were not cleaned before being flushed to storage.
|
||||||
//
|
//
|
||||||
// In both cases all the *original* updates at index `slot` were uncleaned and thus
|
// In both cases all the *original* updates at index `slot` were uncleaned and thus
|
||||||
// should be discoverable by this scan.
|
// should be discoverable by this scan.
|
||||||
//
|
//
|
||||||
// 2) If slot == `max_clean_root`, the slot was not cleaned before being flushed to storage,
|
// 2) If slot == `requested_flush_root`, the slot was not cleaned before being flushed to storage,
|
||||||
// so it also contains all the original updates.
|
// so it also contains all the original updates.
|
||||||
assert_eq!(
|
//
|
||||||
slot_accounts,
|
// 3) If *slot >= scan_root, then we should not clean it either
|
||||||
keys[*slot as usize..]
|
keys[*slot as usize..]
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<HashSet<Pubkey>>()
|
.collect::<HashSet<Pubkey>>()
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Slots less than `max_clean_root` were cleaned in the cache before being flushed
|
// Slots less than `requested_flush_root` and `scan_root` were cleaned in the cache before being flushed
|
||||||
// to storage, should only contain one account
|
// to storage, should only contain one account
|
||||||
assert_eq!(
|
|
||||||
slot_accounts,
|
|
||||||
std::iter::once(keys[*slot as usize])
|
std::iter::once(keys[*slot as usize])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<HashSet<Pubkey>>()
|
.collect::<HashSet<Pubkey>>()
|
||||||
);
|
};
|
||||||
|
|
||||||
|
assert_eq!(slot_accounts, expected_accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(scan_tracker) = scan_tracker {
|
||||||
|
scan_tracker.exit().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_accounts_db_cache_clean_max_root() {
|
fn test_accounts_db_cache_clean_max_root() {
|
||||||
let max_clean_root = 5;
|
let requested_flush_root = 5;
|
||||||
run_test_accounts_db_cache_clean_max_root(10, max_clean_root);
|
run_test_accounts_db_cache_clean_max_root(10, requested_flush_root, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_accounts_db_cache_clean_max_root_with_scan() {
|
||||||
|
let requested_flush_root = 5;
|
||||||
|
run_test_accounts_db_cache_clean_max_root(
|
||||||
|
10,
|
||||||
|
requested_flush_root,
|
||||||
|
Some(requested_flush_root - 1),
|
||||||
|
);
|
||||||
|
run_test_accounts_db_cache_clean_max_root(
|
||||||
|
10,
|
||||||
|
requested_flush_root,
|
||||||
|
Some(requested_flush_root + 1),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_accounts_db_cache_clean_max_root_with_cache_limit_hit() {
|
fn test_accounts_db_cache_clean_max_root_with_cache_limit_hit() {
|
||||||
let max_clean_root = 5;
|
let requested_flush_root = 5;
|
||||||
// Test that if there are > MAX_CACHE_SLOTS in the cache after flush, then more roots
|
// Test that if there are > MAX_CACHE_SLOTS in the cache after flush, then more roots
|
||||||
// will be flushed
|
// will be flushed
|
||||||
run_test_accounts_db_cache_clean_max_root(
|
run_test_accounts_db_cache_clean_max_root(
|
||||||
MAX_CACHE_SLOTS + max_clean_root as usize + 2,
|
MAX_CACHE_SLOTS + requested_flush_root as usize + 2,
|
||||||
max_clean_root,
|
requested_flush_root,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_accounts_db_cache_clean_max_root_with_cache_limit_hit_and_scan() {
|
||||||
|
let requested_flush_root = 5;
|
||||||
|
// Test that if there are > MAX_CACHE_SLOTS in the cache after flush, then more roots
|
||||||
|
// will be flushed
|
||||||
|
run_test_accounts_db_cache_clean_max_root(
|
||||||
|
MAX_CACHE_SLOTS + requested_flush_root as usize + 2,
|
||||||
|
requested_flush_root,
|
||||||
|
Some(requested_flush_root - 1),
|
||||||
|
);
|
||||||
|
run_test_accounts_db_cache_clean_max_root(
|
||||||
|
MAX_CACHE_SLOTS + requested_flush_root as usize + 2,
|
||||||
|
requested_flush_root,
|
||||||
|
Some(requested_flush_root + 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_flush_rooted_accounts_cache(should_clean: bool) {
|
fn run_flush_rooted_accounts_cache(should_clean: bool) {
|
||||||
let num_slots = 10;
|
let num_slots = 10;
|
||||||
let (accounts_db, keys, slots) = setup_accounts_db_cache_clean(num_slots);
|
let (accounts_db, keys, slots, _) = setup_accounts_db_cache_clean(num_slots, None);
|
||||||
let mut cleaned_bytes = 0;
|
let mut cleaned_bytes = 0;
|
||||||
let mut cleaned_accounts = 0;
|
let mut cleaned_accounts = 0;
|
||||||
let should_clean_tracker = if should_clean {
|
let should_clean_tracker = if should_clean {
|
||||||
|
Reference in New Issue
Block a user