Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions accounts-db/src/accounts_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,13 @@ impl AccountStorageEntry {
self.alive_bytes.load(Ordering::Acquire)
}

/// Returns the accounts that were marked obsolete as of the passed in slot
/// or earlier. Returned data includes the slots that the accounts were marked
/// obsolete at
pub fn obsolete_accounts_for_snapshots(&self, slot: Slot) -> ObsoleteAccounts {
self.obsolete_accounts_read_lock()
.obsolete_accounts_for_snapshots(slot)
}
/// Locks obsolete accounts with a read lock and returns the the accounts with the guard
pub(crate) fn obsolete_accounts_read_lock(&self) -> RwLockReadGuard<ObsoleteAccounts> {
self.obsolete_accounts.read().unwrap()
Expand Down
2 changes: 1 addition & 1 deletion accounts-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub mod waitable_condvar;

pub use {
buffered_reader::large_file_buf_reader, file_io::validate_memlock_limit_for_disk_io,
obsolete_accounts::ObsoleteAccounts,
obsolete_accounts::{ObsoleteAccounts, ObsoleteAccountItem},
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to make this public for access in storage.rs

};

#[macro_use]
Expand Down
61 changes: 56 additions & 5 deletions accounts-db/src/obsolete_accounts.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use {crate::account_info::Offset, solana_clock::Slot};

#[derive(Debug, Clone, PartialEq)]
struct ObsoleteAccountItem {
pub struct ObsoleteAccountItem {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, public for access in storage.rs

/// Offset of the account in the account storage entry
offset: Offset,
pub offset: Offset,
/// Length of the account data
data_len: usize,
pub data_len: usize,
/// Slot when the account was marked obsolete
slot: Slot,
pub slot: Slot,
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct ObsoleteAccounts {
accounts: Vec<ObsoleteAccountItem>,
pub accounts: Vec<ObsoleteAccountItem>,
}

impl ObsoleteAccounts {
Expand Down Expand Up @@ -45,6 +45,22 @@ impl ObsoleteAccounts {
.filter(move |obsolete_account| slot.is_none_or(|s| obsolete_account.slot <= s))
.map(|obsolete_account| (obsolete_account.offset, obsolete_account.data_len))
}

/// Returns the accounts that were marked obsolete as of the passed in slot
/// or earlier. Returned data includes the slots that the accounts were marked
/// obsolete at
pub fn obsolete_accounts_for_snapshots(&self, slot: Slot) -> ObsoleteAccounts {
let filtered_accounts = self
.accounts
.iter()
.filter(|account| account.slot <= slot)
.cloned()
.collect();

ObsoleteAccounts {
accounts: filtered_accounts,
}
}
}
#[cfg(test)]
mod tests {
Expand Down Expand Up @@ -99,4 +115,39 @@ mod tests {

assert_eq!(filtered_accounts, vec![(10, 100), (20, 200), (30, 300)]);
}

#[test]
fn test_obsolete_accounts_for_snapshots() {
let mut obsolete_accounts = ObsoleteAccounts::default();
let new_accounts = vec![(10, 100, 40), (20, 200, 42), (30, 300, 44)]
.into_iter()
.map(|(offset, data_len, slot)| ObsoleteAccountItem {
offset,
data_len,
slot,
})
.collect::<Vec<_>>();

// Mark accounts obsolete with different slots
new_accounts.iter().for_each(|item| {
obsolete_accounts
.mark_accounts_obsolete([(item.offset, item.data_len)].into_iter(), item.slot)
});

// Filter accounts obsolete as of slot 42
let obsolete_accounts_for_snapshots = obsolete_accounts.obsolete_accounts_for_snapshots(42);

let expected_accounts: Vec<_> = new_accounts
.iter()
.filter(|account| account.slot <= 42)
.cloned()
.collect();

assert_eq!(obsolete_accounts_for_snapshots.accounts, expected_accounts);

// Filter accounts obsolete passing in no slot (i.e., all obsolete accounts)
let obsolete_accounts_for_snapshots = obsolete_accounts.obsolete_accounts_for_snapshots(100);

assert_eq!(obsolete_accounts_for_snapshots.accounts, new_accounts);
}
}
46 changes: 43 additions & 3 deletions runtime/src/serde_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use {
stakes::{serialize_stake_accounts_to_delegation_format, Stakes},
},
bincode::{self, config::Options, Error},
dashmap::DashMap,
log::*,
serde::{de::DeserializeOwned, Deserialize, Serialize},
solana_accounts_db::{
Expand All @@ -24,7 +25,7 @@ use {
accounts_update_notifier_interface::AccountsUpdateNotifier,
ancestors::AncestorsForSerialization,
blockhash_queue::BlockhashQueue,
ObsoleteAccounts,
ObsoleteAccountItem, ObsoleteAccounts,
},
solana_clock::{Epoch, Slot, UnixTimestamp},
solana_epoch_schedule::EpochSchedule,
Expand Down Expand Up @@ -58,7 +59,7 @@ mod status_cache;
mod storage;
mod tests;
mod types;
mod utils;
pub mod utils;

pub(crate) use {
status_cache::{deserialize_status_cache, serialize_status_cache},
Expand Down Expand Up @@ -649,6 +650,34 @@ where
(serializable_bank, serializable_accounts_db, extra_fields).serialize(serializer)
}

pub(crate) fn serialize_obsolete_accounts<W>(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really clear where the right place for this to live is, the ser/des functions are spread all over the place. Any feedback?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... it's not great... We could create a new file runtime/src/serde_snapshot/obsolete_accounts.rs and put everything in there. The Serde struct, the serialize/deserialize functions, and the tests.

I kind of like that. Wdyt?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, makes sense.

stream: &mut BufWriter<W>,
obsolete_accounts_map: &HashMap<Slot, SerdeObsoleteAccounts>,
) -> Result<(), Error>
where
W: Write,
{
let bincode = bincode::DefaultOptions::new().with_fixint_encoding();
bincode.serialize_into(
stream,
&utils::serialize_iter_as_tuple(obsolete_accounts_map.iter()),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to serialize as tuple

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked at the serialize_status_cache(), and I think we can pass obsoelte_accounts_map to serialize_into() and it'll just work?

Suggested change
&utils::serialize_iter_as_tuple(obsolete_accounts_map.iter()),
obsolete_accounts_map,

Minimally clippy is happy.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If clippy is happy, it probably works!

Copy link
Author

@roryharr roryharr Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it out, it doesn't work:
It is because it is being serialized as a hashmap and deserialized as a dashmap.
I could use a dashmap on the serialization side, but dashmaps do not suppot serialize/deseralize, so it would need something like serialize_iter_as_tuple anyways in that situation.

Dashmap is definitely needed on the deserialize size as performance testing found. It could be deseralized as a hashmap and then converted to a dashmap, but that seems less efficient.

Thoughts?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a serde feature for DashMap. I haven't used it before. Maybe it'll work here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But otherwise, yeah I agree with you. Probably don't want to do extra allocations/conversions unnecessarily.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passes testing. I will check performance in a bit

)
}

pub(crate) fn deserialize_obsolete_accounts<R>(
stream: &mut BufReader<R>,
) -> Result<DashMap<Slot, SerdeObsoleteAccounts>, Error>
where
R: Read,
{
let obsolete_accounts = DashMap::default();
while let Ok((slot, accounts)) = { deserialize_from(&mut *stream) } {
obsolete_accounts.insert(slot, accounts);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this will swallow any real errors too?


Ok(obsolete_accounts)
}

#[cfg(test)]
struct SerializableBankAndStorage<'a> {
bank: &'a Bank,
Expand Down Expand Up @@ -869,8 +898,19 @@ pub(crate) fn reconstruct_single_storage(
obsolete_accounts.id,
));
}
let accounts = ObsoleteAccounts {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt

Suggested change
let accounts = ObsoleteAccounts {
let obsolete_accounts = ObsoleteAccounts {

accounts: obsolete_accounts
.accounts
.into_iter()
.map(|(offset, data_len, slot)| ObsoleteAccountItem {
offset,
data_len,
slot,
})
.collect(),
};

(updated_len, obsolete_accounts.accounts)
(updated_len, accounts)
} else {
(current_len, ObsoleteAccounts::default())
};
Expand Down
26 changes: 23 additions & 3 deletions runtime/src/serde_snapshot/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
serde::{Deserialize, Serialize},
solana_accounts_db::{accounts_db::AccountStorageEntry, ObsoleteAccounts},
solana_accounts_db::{account_info::Offset, accounts_db::AccountStorageEntry},
solana_clock::Slot,
};

Expand Down Expand Up @@ -51,7 +51,7 @@ impl SerializableStorage for SerializableAccountStorageEntry {
impl solana_frozen_abi::abi_example::TransparentAsHelper for SerializableAccountStorageEntry {}

/// This structure handles the load/store of obsolete accounts during snapshot restoration.
#[derive(Debug, Default)]
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct SerdeObsoleteAccounts {
/// The ID of the associated account file. Used for verification to ensure the restored
/// obsolete accounts correspond to the correct account file
Expand All @@ -61,5 +61,25 @@ pub(crate) struct SerdeObsoleteAccounts {
/// is used to validate the size when creating the accounts file.
pub bytes: u64,
/// A list of accounts that are obsolete in the storage being restored.
pub accounts: ObsoleteAccounts,
pub accounts: Vec<(Offset, usize, Slot)>,
}

impl SerdeObsoleteAccounts {
pub fn new_from_storage_entry_at_slot(
storage: &AccountStorageEntry,
snapshot_slot: Slot,
) -> Self {
let accounts: Vec<(usize, usize, u64)> = storage
.obsolete_accounts_for_snapshots(snapshot_slot)
.accounts
.into_iter()
.map(|item| (item.offset, item.data_len, item.slot))
.collect();

SerdeObsoleteAccounts {
id: storage.id() as SerializedAccountsFileId,
bytes: storage.get_obsolete_bytes(Some(snapshot_slot)) as u64,
accounts,
}
}
}
53 changes: 50 additions & 3 deletions runtime/src/serde_snapshot/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ mod serde_snapshot_tests {
crate::{
bank::BankHashStats,
serde_snapshot::{
deserialize_accounts_db_fields, reconstruct_accountsdb_from_fields,
remap_append_vec_file, SerializableAccountsDb, SnapshotAccountsDbFields,
deserialize_accounts_db_fields, deserialize_obsolete_accounts,
reconstruct_accountsdb_from_fields, remap_append_vec_file,
serialize_obsolete_accounts, SerdeObsoleteAccounts, SerializableAccountsDb,
SnapshotAccountsDbFields,
},
snapshot_utils::{get_storages_to_serialize, StorageAndNextAccountsFileId},
},
Expand All @@ -30,8 +32,9 @@ mod serde_snapshot_tests {
solana_nohash_hasher::BuildNoHashHasher,
solana_pubkey::Pubkey,
std::{
collections::HashMap,
fs::File,
io::{self, BufReader, Cursor, Read, Write},
io::{self, BufReader, BufWriter, Cursor, Read, Write},
ops::RangeFull,
path::{Path, PathBuf},
sync::{
Expand Down Expand Up @@ -937,4 +940,48 @@ mod serde_snapshot_tests {
)
.unwrap();
}

#[test_case(0, 0)]
#[test_case(1, 0)]
#[test_case(10, 15)]
fn test_serialize_and_deserialize_obsolete_accounts(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to test in the latest review.

num_storages: u64,
num_accounts_per_storage: usize,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt

Suggested change
num_accounts_per_storage: usize,
num_obsolete_accounts_per_storage: usize,

) {
// Create a sample obsolete accounts map
let mut obsolete_accounts_map = HashMap::new();
for slot in 1..=num_storages {
let obsolete_accounts = (0..num_accounts_per_storage)
.map(|j| (j, j * 10, slot + 1))
.collect();

obsolete_accounts_map.insert(
slot,
SerdeObsoleteAccounts {
bytes: num_accounts_per_storage as u64 * 1000,
id: slot as usize,
accounts: obsolete_accounts,
},
);
}

// Serialize the obsolete accounts
let mut buf = Vec::new();
let cursor = Cursor::new(&mut buf);
let mut writer = BufWriter::new(cursor);
serialize_obsolete_accounts(&mut writer, &obsolete_accounts_map).unwrap();
drop(writer);

// Deserialize the obsolete accounts
let cursor = Cursor::new(buf.as_slice());
let mut reader = BufReader::new(cursor);
let deserialized_accounts = deserialize_obsolete_accounts(&mut reader).unwrap();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt

Suggested change
let deserialized_accounts = deserialize_obsolete_accounts(&mut reader).unwrap();
let deserialized_obsolete_accounts = deserialize_obsolete_accounts(&mut reader).unwrap();


// Verify the deserialized data matches the original
assert_eq!(deserialized_accounts.len(), obsolete_accounts_map.len());
for (slot, accounts) in obsolete_accounts_map {
let deserialized_accounts = deserialized_accounts.get(&slot).unwrap();
assert_eq!(accounts.accounts, deserialized_accounts.accounts);
}
}
}
Loading
Loading