Skip to content

Commit c84900d

Browse files
shrink/ancient pack purge zero lamport accounts (#2312)
* shrink/ancient pack purge zero lamport accounts * pr feedback * add = * comment * fix comment typo
1 parent beb3f58 commit c84900d

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

accounts-db/src/accounts_db.rs

+233
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ pub(crate) struct ShrinkCollect<'a, T: ShrinkCollectRefs<'a>> {
475475
pub(crate) slot: Slot,
476476
pub(crate) capacity: u64,
477477
pub(crate) unrefed_pubkeys: Vec<&'a Pubkey>,
478+
pub(crate) zero_lamport_single_ref_pubkeys: Vec<&'a Pubkey>,
478479
pub(crate) alive_accounts: T,
479480
/// total size in storage of all alive accounts
480481
pub(crate) alive_total_bytes: usize,
@@ -524,6 +525,8 @@ struct LoadAccountsIndexForShrink<'a, T: ShrinkCollectRefs<'a>> {
524525
alive_accounts: T,
525526
/// pubkeys that were unref'd in the accounts index because they were dead
526527
unrefed_pubkeys: Vec<&'a Pubkey>,
528+
/// pubkeys that are the last remaining zero lamport instance of an account
529+
zero_lamport_single_ref_pubkeys: Vec<&'a Pubkey>,
527530
/// true if all alive accounts are zero lamport accounts
528531
all_are_zero_lamports: bool,
529532
/// index entries we need to hold onto to keep them from getting flushed
@@ -2011,6 +2014,7 @@ pub struct ShrinkStats {
20112014
dead_accounts: AtomicU64,
20122015
alive_accounts: AtomicU64,
20132016
accounts_loaded: AtomicU64,
2017+
purged_zero_lamports: AtomicU64,
20142018
}
20152019

20162020
impl ShrinkStats {
@@ -2109,6 +2113,11 @@ impl ShrinkStats {
21092113
self.accounts_loaded.swap(0, Ordering::Relaxed) as i64,
21102114
i64
21112115
),
2116+
(
2117+
"purged_zero_lamports_count",
2118+
self.purged_zero_lamports.swap(0, Ordering::Relaxed),
2119+
i64
2120+
),
21122121
);
21132122
}
21142123
}
@@ -2309,6 +2318,13 @@ impl ShrinkAncientStats {
23092318
self.many_refs_old_alive.swap(0, Ordering::Relaxed),
23102319
i64
23112320
),
2321+
(
2322+
"purged_zero_lamports_count",
2323+
self.shrink_stats
2324+
.purged_zero_lamports
2325+
.swap(0, Ordering::Relaxed),
2326+
i64
2327+
),
23122328
);
23132329
}
23142330
}
@@ -3793,12 +3809,14 @@ impl AccountsDb {
37933809
let count = accounts.len();
37943810
let mut alive_accounts = T::with_capacity(count, slot_to_shrink);
37953811
let mut unrefed_pubkeys = Vec::with_capacity(count);
3812+
let mut zero_lamport_single_ref_pubkeys = Vec::with_capacity(count);
37963813

37973814
let mut alive = 0;
37983815
let mut dead = 0;
37993816
let mut index = 0;
38003817
let mut all_are_zero_lamports = true;
38013818
let mut index_entries_being_shrunk = Vec::with_capacity(accounts.len());
3819+
let latest_full_snapshot_slot = self.latest_full_snapshot_slot();
38023820
self.accounts_index.scan(
38033821
accounts.iter().map(|account| account.pubkey()),
38043822
|pubkey, slots_refs, entry| {
@@ -3817,6 +3835,21 @@ impl AccountsDb {
38173835
unrefed_pubkeys.push(pubkey);
38183836
result = AccountsIndexScanResult::Unref;
38193837
dead += 1;
3838+
} else if stored_account.is_zero_lamport()
3839+
&& ref_count == 1
3840+
&& latest_full_snapshot_slot
3841+
.map(|latest_full_snapshot_slot| {
3842+
latest_full_snapshot_slot >= slot_to_shrink
3843+
})
3844+
.unwrap_or(true)
3845+
{
3846+
// only do this if our slot is prior to the latest full snapshot
3847+
// we found a zero lamport account that is the only instance of this account. We can delete it completely.
3848+
zero_lamport_single_ref_pubkeys.push(pubkey);
3849+
self.add_uncleaned_pubkeys_after_shrink(
3850+
slot_to_shrink,
3851+
[*pubkey].into_iter(),
3852+
);
38203853
} else {
38213854
// Hold onto the index entry arc so that it cannot be flushed.
38223855
// Since we are shrinking these entries, we need to disambiguate storage ids during this period and those only exist in the in-memory accounts index.
@@ -3839,6 +3872,7 @@ impl AccountsDb {
38393872
LoadAccountsIndexForShrink {
38403873
alive_accounts,
38413874
unrefed_pubkeys,
3875+
zero_lamport_single_ref_pubkeys,
38423876
all_are_zero_lamports,
38433877
index_entries_being_shrunk,
38443878
}
@@ -3939,6 +3973,7 @@ impl AccountsDb {
39393973
let len = stored_accounts.len();
39403974
let alive_accounts_collect = Mutex::new(T::with_capacity(len, slot));
39413975
let unrefed_pubkeys_collect = Mutex::new(Vec::with_capacity(len));
3976+
let zero_lamport_single_ref_pubkeys_collect = Mutex::new(Vec::with_capacity(len));
39423977
stats
39433978
.accounts_loaded
39443979
.fetch_add(len as u64, Ordering::Relaxed);
@@ -3955,6 +3990,7 @@ impl AccountsDb {
39553990
alive_accounts,
39563991
mut unrefed_pubkeys,
39573992
all_are_zero_lamports,
3993+
mut zero_lamport_single_ref_pubkeys,
39583994
mut index_entries_being_shrunk,
39593995
} = self.load_accounts_index_for_shrink(stored_accounts, stats, slot);
39603996

@@ -3967,6 +4003,10 @@ impl AccountsDb {
39674003
.lock()
39684004
.unwrap()
39694005
.append(&mut unrefed_pubkeys);
4006+
zero_lamport_single_ref_pubkeys_collect
4007+
.lock()
4008+
.unwrap()
4009+
.append(&mut zero_lamport_single_ref_pubkeys);
39704010
index_entries_being_shrunk_outer
39714011
.lock()
39724012
.unwrap()
@@ -3979,6 +4019,9 @@ impl AccountsDb {
39794019

39804020
let alive_accounts = alive_accounts_collect.into_inner().unwrap();
39814021
let unrefed_pubkeys = unrefed_pubkeys_collect.into_inner().unwrap();
4022+
let zero_lamport_single_ref_pubkeys = zero_lamport_single_ref_pubkeys_collect
4023+
.into_inner()
4024+
.unwrap();
39824025

39834026
index_read_elapsed.stop();
39844027
stats
@@ -4002,6 +4045,7 @@ impl AccountsDb {
40024045
slot,
40034046
capacity: *capacity,
40044047
unrefed_pubkeys,
4048+
zero_lamport_single_ref_pubkeys,
40054049
alive_accounts,
40064050
alive_total_bytes,
40074051
total_starting_accounts: len,
@@ -4010,6 +4054,41 @@ impl AccountsDb {
40104054
}
40114055
}
40124056

4057+
/// These accounts were found during shrink of `slot` to be slot_list=[slot] and ref_count == 1 and lamports = 0.
4058+
/// This means this slot contained the only account data for this pubkey and it is zero lamport.
4059+
/// Thus, we did NOT treat this as an alive account, so we did NOT copy the zero lamport account to the new
4060+
/// storage. So, the account will no longer be alive or exist at `slot`.
4061+
/// So, first, remove the ref count since this newly shrunk storage will no longer access it.
4062+
/// Second, remove `slot` from the index entry's slot list. If the slot list is now empty, then the
4063+
/// pubkey can be removed completely from the index.
4064+
/// In parallel with this code (which is running in the bg), the same pubkey could be revived and written to
4065+
/// as part of tx processing. In that case, the slot list will contain a slot in the write cache and the
4066+
/// index entry will NOT be deleted.
4067+
fn remove_zero_lamport_single_ref_accounts_after_shrink(
4068+
&self,
4069+
zero_lamport_single_ref_pubkeys: &[&Pubkey],
4070+
slot: Slot,
4071+
stats: &ShrinkStats,
4072+
) {
4073+
stats.purged_zero_lamports.fetch_add(
4074+
zero_lamport_single_ref_pubkeys.len() as u64,
4075+
Ordering::Relaxed,
4076+
);
4077+
4078+
// we have to unref before we `purge_keys_exact`. Otherwise, we could race with the foreground with tx processing
4079+
// reviving this index entry and then we'd unref the revived version, which is a refcount bug.
4080+
self.accounts_index.scan(
4081+
zero_lamport_single_ref_pubkeys.iter().cloned(),
4082+
|_pubkey, _slots_refs, _entry| AccountsIndexScanResult::Unref,
4083+
Some(AccountsIndexScanResult::Unref),
4084+
false,
4085+
);
4086+
4087+
zero_lamport_single_ref_pubkeys.iter().for_each(|k| {
4088+
_ = self.purge_keys_exact([&(**k, slot)].into_iter());
4089+
});
4090+
}
4091+
40134092
/// common code from shrink and combine_ancient_slots
40144093
/// get rid of all original store_ids in the slot
40154094
pub(crate) fn remove_old_stores_shrink<'a, T: ShrinkCollectRefs<'a>>(
@@ -4020,7 +4099,19 @@ impl AccountsDb {
40204099
shrink_can_be_active: bool,
40214100
) {
40224101
let mut time = Measure::start("remove_old_stores_shrink");
4102+
4103+
// handle the zero lamport alive accounts before calling clean
4104+
// We have to update the index entries for these zero lamport pubkeys before we remove the storage in `mark_dirty_dead_stores`
4105+
// that contained the accounts.
4106+
self.remove_zero_lamport_single_ref_accounts_after_shrink(
4107+
&shrink_collect.zero_lamport_single_ref_pubkeys,
4108+
shrink_collect.slot,
4109+
stats,
4110+
);
4111+
40234112
// Purge old, overwritten storage entries
4113+
// This has the side effect of dropping `shrink_in_progress`, which removes the old storage completely. The
4114+
// index has to be correct before we drop the old storage.
40244115
let dead_storages = self.mark_dirty_dead_stores(
40254116
shrink_collect.slot,
40264117
// If all accounts are zero lamports, then we want to mark the entire OLD append vec as dirty.
@@ -10884,6 +10975,146 @@ pub mod tests {
1088410975
assert_eq!(accounts.alive_account_count_in_slot(1), 0);
1088510976
}
1088610977

10978+
#[test]
10979+
fn test_remove_zero_lamport_single_ref_accounts_after_shrink() {
10980+
for pass in 0..3 {
10981+
let accounts = AccountsDb::new_single_for_tests();
10982+
let pubkey_zero = Pubkey::from([1; 32]);
10983+
let pubkey2 = Pubkey::from([2; 32]);
10984+
let account = AccountSharedData::new(1, 0, AccountSharedData::default().owner());
10985+
let zero_lamport_account =
10986+
AccountSharedData::new(0, 0, AccountSharedData::default().owner());
10987+
let slot = 1;
10988+
10989+
accounts.store_for_tests(
10990+
slot,
10991+
&[(&pubkey_zero, &zero_lamport_account), (&pubkey2, &account)],
10992+
);
10993+
10994+
// Simulate rooting the zero-lamport account, writes it to storage
10995+
accounts.calculate_accounts_delta_hash(slot);
10996+
accounts.add_root_and_flush_write_cache(slot);
10997+
10998+
if pass > 0 {
10999+
// store in write cache
11000+
accounts.store_for_tests(slot + 1, &[(&pubkey_zero, &zero_lamport_account)]);
11001+
if pass == 2 {
11002+
// move to a storage (causing ref count to increase)
11003+
accounts.calculate_accounts_delta_hash(slot + 1);
11004+
accounts.add_root_and_flush_write_cache(slot + 1);
11005+
}
11006+
}
11007+
11008+
accounts.accounts_index.get_and_then(&pubkey_zero, |entry| {
11009+
let expected_ref_count = if pass < 2 { 1 } else { 2 };
11010+
assert_eq!(entry.unwrap().ref_count(), expected_ref_count, "{pass}");
11011+
let expected_slot_list = if pass < 1 { 1 } else { 2 };
11012+
assert_eq!(
11013+
entry.unwrap().slot_list.read().unwrap().len(),
11014+
expected_slot_list
11015+
);
11016+
(false, ())
11017+
});
11018+
accounts.accounts_index.get_and_then(&pubkey2, |entry| {
11019+
assert!(entry.is_some());
11020+
(false, ())
11021+
});
11022+
11023+
let zero_lamport_single_ref_pubkeys = [&pubkey_zero];
11024+
accounts.remove_zero_lamport_single_ref_accounts_after_shrink(
11025+
&zero_lamport_single_ref_pubkeys,
11026+
slot,
11027+
&ShrinkStats::default(),
11028+
);
11029+
11030+
accounts.accounts_index.get_and_then(&pubkey_zero, |entry| {
11031+
if pass == 0 {
11032+
// should not exist in index at all
11033+
assert!(entry.is_none(), "{pass}");
11034+
} else {
11035+
// alive only in slot + 1
11036+
assert_eq!(entry.unwrap().slot_list.read().unwrap().len(), 1);
11037+
assert_eq!(
11038+
entry
11039+
.unwrap()
11040+
.slot_list
11041+
.read()
11042+
.unwrap()
11043+
.first()
11044+
.map(|(s, _)| s)
11045+
.cloned()
11046+
.unwrap(),
11047+
slot + 1
11048+
);
11049+
// refcount = 1 if we flushed the write cache for slot + 1
11050+
let expected_ref_count = if pass < 2 { 0 } else { 1 };
11051+
assert_eq!(
11052+
entry.map(|e| e.ref_count()),
11053+
Some(expected_ref_count),
11054+
"{pass}"
11055+
);
11056+
}
11057+
(false, ())
11058+
});
11059+
11060+
accounts.accounts_index.get_and_then(&pubkey2, |entry| {
11061+
assert!(entry.is_some(), "{pass}");
11062+
(false, ())
11063+
});
11064+
}
11065+
}
11066+
11067+
#[test]
11068+
fn test_shrink_zero_lamport_single_ref_account() {
11069+
solana_logger::setup();
11070+
11071+
// store a zero and non-zero lamport account
11072+
// make sure clean marks the ref_count=1, zero lamport account dead and removes pubkey from index completely
11073+
let accounts = AccountsDb::new_single_for_tests();
11074+
let pubkey_zero = Pubkey::from([1; 32]);
11075+
let pubkey2 = Pubkey::from([2; 32]);
11076+
let account = AccountSharedData::new(1, 0, AccountSharedData::default().owner());
11077+
let zero_lamport_account =
11078+
AccountSharedData::new(0, 0, AccountSharedData::default().owner());
11079+
11080+
// Store a zero-lamport account and a non-zero lamport account
11081+
accounts.store_for_tests(
11082+
1,
11083+
&[(&pubkey_zero, &zero_lamport_account), (&pubkey2, &account)],
11084+
);
11085+
11086+
// Simulate rooting the zero-lamport account, should be a
11087+
// candidate for cleaning
11088+
accounts.calculate_accounts_delta_hash(1);
11089+
accounts.add_root_and_flush_write_cache(1);
11090+
11091+
// for testing, we need to cause shrink to think this will be productive.
11092+
// The zero lamport account isn't dead, but it can become dead inside shrink.
11093+
accounts
11094+
.storage
11095+
.get_slot_storage_entry(1)
11096+
.unwrap()
11097+
.alive_bytes
11098+
.fetch_sub(aligned_stored_size(0), Ordering::Relaxed);
11099+
11100+
// Slot 1 should be cleaned, but
11101+
// zero-lamport account should not be cleaned since last full snapshot root is before slot 1
11102+
accounts.shrink_slot_forced(1);
11103+
11104+
assert!(accounts.storage.get_slot_storage_entry(1).is_some());
11105+
11106+
// the zero lamport account should be marked as dead
11107+
assert_eq!(accounts.alive_account_count_in_slot(1), 1);
11108+
11109+
// zero lamport account should be dead in the index
11110+
assert!(!accounts
11111+
.accounts_index
11112+
.contains_with(&pubkey_zero, None, None));
11113+
// other account should still be alive
11114+
assert!(accounts.accounts_index.contains_with(&pubkey2, None, None));
11115+
assert!(accounts.storage.get_slot_storage_entry(1).is_some());
11116+
}
11117+
1088711118
#[test]
1088811119
fn test_clean_multiple_zero_lamport_decrements_index_ref_count() {
1088911120
solana_logger::setup();
@@ -15910,6 +16141,8 @@ pub mod tests {
1591016141
debug!("space: {space}, lamports: {lamports}, alive: {alive}, account_count: {account_count}, append_opposite_alive_account: {append_opposite_alive_account}, append_opposite_zero_lamport_account: {append_opposite_zero_lamport_account}, normal_account_count: {normal_account_count}");
1591116142
let db = AccountsDb::new_single_for_tests();
1591216143
let slot5 = 5;
16144+
// don't do special zero lamport account handling
16145+
db.set_latest_full_snapshot_slot(0);
1591316146
let mut account = AccountSharedData::new(
1591416147
lamports,
1591516148
space,

accounts-db/src/ancient_append_vecs.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3836,6 +3836,7 @@ pub mod tests {
38363836
unrefed_pubkeys: unrefed_pubkeys.iter().collect(),
38373837

38383838
// irrelevant fields
3839+
zero_lamport_single_ref_pubkeys: Vec::default(),
38393840
slot: 0,
38403841
capacity: 0,
38413842
alive_accounts: ShrinkCollectAliveSeparatedByRefs {

0 commit comments

Comments
 (0)