TieredStorage struct (3/N) -- new_readonly and reader structs (#32579)
#### Summary of Changes This PR implements TieredStorage::new_readonly() and introduces TieredStorageReader and HotStorageReader. #### Test Plan Updated the existing unit test.
This commit is contained in:
parent
533f42dae6
commit
a5bde0a79c
|
@ -19,6 +19,8 @@ use {
|
|||
error::TieredStorageError,
|
||||
footer::{AccountBlockFormat, AccountMetaFormat, OwnersBlockFormat},
|
||||
index::AccountIndexFormat,
|
||||
once_cell::sync::OnceCell,
|
||||
readable::TieredStorageReader,
|
||||
solana_sdk::{account::ReadableAccount, hash::Hash},
|
||||
std::{
|
||||
borrow::Borrow,
|
||||
|
@ -43,6 +45,7 @@ pub struct TieredStorageFormat {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct TieredStorage {
|
||||
reader: OnceCell<TieredStorageReader>,
|
||||
format: Option<TieredStorageFormat>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
@ -55,17 +58,34 @@ impl TieredStorage {
|
|||
/// is called.
|
||||
pub fn new_writable(path: impl Into<PathBuf>, format: TieredStorageFormat) -> Self {
|
||||
Self {
|
||||
reader: OnceCell::<TieredStorageReader>::new(),
|
||||
format: Some(format),
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new read-only instance of TieredStorage from the
|
||||
/// specified path.
|
||||
pub fn new_readonly(path: impl Into<PathBuf>) -> TieredStorageResult<Self> {
|
||||
let path = path.into();
|
||||
Ok(Self {
|
||||
reader: OnceCell::with_value(TieredStorageReader::new_from_path(&path)?),
|
||||
format: None,
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the path to this TieredStorage.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
|
||||
/// Writes the specified accounts into this TieredStorage.
|
||||
///
|
||||
/// Note that this function can only be called once per a TieredStorage
|
||||
/// instance. TieredStorageError::AttemptToUpdateReadOnly will be returned
|
||||
/// if this function is invoked more than once on the same TieredStorage
|
||||
/// instance.
|
||||
pub fn write_accounts<
|
||||
'a,
|
||||
'b,
|
||||
|
@ -77,12 +97,39 @@ impl TieredStorage {
|
|||
accounts: &StorableAccountsWithHashesAndWriteVersions<'a, 'b, T, U, V>,
|
||||
skip: usize,
|
||||
) -> TieredStorageResult<Vec<StoredAccountInfo>> {
|
||||
// self.format must be Some as write_accounts can only be called on a
|
||||
// TieredStorage instance created via new_writable() where it format
|
||||
// field is required.
|
||||
assert!(self.format.is_some());
|
||||
let writer = TieredStorageWriter::new(&self.path, self.format.as_ref().unwrap())?;
|
||||
writer.write_accounts(accounts, skip)
|
||||
if self.is_read_only() {
|
||||
return Err(TieredStorageError::AttemptToUpdateReadOnly(
|
||||
self.path.to_path_buf(),
|
||||
));
|
||||
}
|
||||
|
||||
let result = {
|
||||
// self.format must be Some as write_accounts can only be called on a
|
||||
// TieredStorage instance created via new_writable() where its format
|
||||
// field is required.
|
||||
let writer = TieredStorageWriter::new(&self.path, self.format.as_ref().unwrap())?;
|
||||
writer.write_accounts(accounts, skip)
|
||||
};
|
||||
|
||||
// panic here if self.reader.get() is not None as self.reader can only be
|
||||
// None since we have passed `is_read_only()` check previously, indicating
|
||||
// self.reader is not yet set.
|
||||
self.reader
|
||||
.set(TieredStorageReader::new_from_path(&self.path)?)
|
||||
.unwrap();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the underlying reader of the TieredStorage. None will be
|
||||
/// returned if it's is_read_only() returns false.
|
||||
pub fn reader(&self) -> Option<&TieredStorageReader> {
|
||||
self.reader.get()
|
||||
}
|
||||
|
||||
/// Returns true if the TieredStorage instance is read-only.
|
||||
pub fn is_read_only(&self) -> bool {
|
||||
self.reader.get().is_some()
|
||||
}
|
||||
|
||||
/// Returns the size of the underlying accounts file.
|
||||
|
@ -104,17 +151,21 @@ mod tests {
|
|||
footer::{TieredStorageFooter, TieredStorageMagicNumber},
|
||||
hot::HOT_FORMAT,
|
||||
solana_sdk::{account::AccountSharedData, clock::Slot, pubkey::Pubkey},
|
||||
tempfile::NamedTempFile,
|
||||
tempfile::tempdir,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_new_footer_only() {
|
||||
let path = NamedTempFile::new().unwrap().path().to_path_buf();
|
||||
|
||||
let tiered_storage = TieredStorage::new_writable(path.clone(), HOT_FORMAT.clone());
|
||||
assert_eq!(tiered_storage.path(), path);
|
||||
assert_eq!(tiered_storage.file_size().unwrap(), 0);
|
||||
impl TieredStorage {
|
||||
fn footer(&self) -> Option<&TieredStorageFooter> {
|
||||
self.reader.get().map(|r| r.footer())
|
||||
}
|
||||
}
|
||||
|
||||
/// Simply invoke write_accounts with empty vector to allow the tiered storage
|
||||
/// to persist non-account blocks such as footer, index block, etc.
|
||||
fn write_zero_accounts(
|
||||
tiered_storage: &TieredStorage,
|
||||
expected_result: TieredStorageResult<Vec<StoredAccountInfo>>,
|
||||
) {
|
||||
let slot_ignored = Slot::MAX;
|
||||
let account_refs = Vec::<(&Pubkey, &AccountSharedData)>::new();
|
||||
let account_data = (slot_ignored, account_refs.as_slice());
|
||||
|
@ -126,14 +177,79 @@ mod tests {
|
|||
);
|
||||
|
||||
let result = tiered_storage.write_accounts(&storable_accounts, 0);
|
||||
// Expect the result to be TieredStorageError::Unsupported as the feature
|
||||
// is not yet fully supported, but we can still check its partial results
|
||||
// in the test.
|
||||
assert!(result.is_err());
|
||||
|
||||
match (&result, &expected_result) {
|
||||
(
|
||||
Err(TieredStorageError::AttemptToUpdateReadOnly(_)),
|
||||
Err(TieredStorageError::AttemptToUpdateReadOnly(_)),
|
||||
) => {}
|
||||
(Err(TieredStorageError::Unsupported()), Err(TieredStorageError::Unsupported())) => {}
|
||||
// we don't expect error type mis-match or other error types here
|
||||
_ => {
|
||||
panic!("actual: {result:?}, expected: {expected_result:?}");
|
||||
}
|
||||
};
|
||||
|
||||
assert!(tiered_storage.is_read_only());
|
||||
assert_eq!(
|
||||
tiered_storage.file_size().unwrap() as usize,
|
||||
std::mem::size_of::<TieredStorageFooter>()
|
||||
+ std::mem::size_of::<TieredStorageMagicNumber>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_meta_file_only() {
|
||||
// Generate a new temp path that is guaranteed to NOT already have a file.
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let tiered_storage_path = temp_dir.path().join("test_new_meta_file_only");
|
||||
|
||||
{
|
||||
let tiered_storage =
|
||||
TieredStorage::new_writable(&tiered_storage_path, HOT_FORMAT.clone());
|
||||
assert!(!tiered_storage.is_read_only());
|
||||
assert_eq!(tiered_storage.path(), tiered_storage_path);
|
||||
assert_eq!(tiered_storage.file_size().unwrap(), 0);
|
||||
|
||||
// Expect the result to be TieredStorageError::Unsupported as the feature
|
||||
// is not yet fully supported, but we can still check its partial results
|
||||
// in the test.
|
||||
write_zero_accounts(&tiered_storage, Err(TieredStorageError::Unsupported()));
|
||||
}
|
||||
|
||||
let tiered_storage_readonly = TieredStorage::new_readonly(&tiered_storage_path).unwrap();
|
||||
let footer = tiered_storage_readonly.footer().unwrap();
|
||||
assert!(tiered_storage_readonly.is_read_only());
|
||||
assert_eq!(tiered_storage_readonly.reader().unwrap().num_accounts(), 0);
|
||||
assert_eq!(footer.account_meta_format, HOT_FORMAT.account_meta_format);
|
||||
assert_eq!(footer.owners_block_format, HOT_FORMAT.owners_block_format);
|
||||
assert_eq!(footer.account_index_format, HOT_FORMAT.account_index_format);
|
||||
assert_eq!(footer.account_block_format, HOT_FORMAT.account_block_format);
|
||||
assert_eq!(
|
||||
tiered_storage_readonly.file_size().unwrap() as usize,
|
||||
std::mem::size_of::<TieredStorageFooter>()
|
||||
+ std::mem::size_of::<TieredStorageMagicNumber>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_accounts_twice() {
|
||||
// Generate a new temp path that is guaranteed to NOT already have a file.
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let tiered_storage_path = temp_dir.path().join("test_write_accounts_twice");
|
||||
|
||||
let tiered_storage = TieredStorage::new_writable(&tiered_storage_path, HOT_FORMAT.clone());
|
||||
// Expect the result to be TieredStorageError::Unsupported as the feature
|
||||
// is not yet fully supported, but we can still check its partial results
|
||||
// in the test.
|
||||
write_zero_accounts(&tiered_storage, Err(TieredStorageError::Unsupported()));
|
||||
// Expect AttemptToUpdateReadOnly error as write_accounts can only
|
||||
// be invoked once.
|
||||
write_zero_accounts(
|
||||
&tiered_storage,
|
||||
Err(TieredStorageError::AttemptToUpdateReadOnly(
|
||||
tiered_storage_path,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ pub enum TieredStorageError {
|
|||
#[error("MagicNumberMismatch: expected {0}, found {1}")]
|
||||
MagicNumberMismatch(u64, u64),
|
||||
|
||||
#[error("ReadOnlyFileUpdateError: attempted to update read-only file {0}")]
|
||||
ReadOnlyFileUpdateError(PathBuf),
|
||||
#[error("AttemptToUpdateReadOnly: attempted to update read-only file {0}")]
|
||||
AttemptToUpdateReadOnly(PathBuf),
|
||||
|
||||
#[error("UnknownFormat: the tiered storage format is unavailable for file {0}")]
|
||||
UnknownFormat(PathBuf),
|
||||
|
|
|
@ -47,7 +47,8 @@ impl Default for TieredStorageMagicNumber {
|
|||
pub enum AccountMetaFormat {
|
||||
#[default]
|
||||
Hot = 0,
|
||||
Cold = 1,
|
||||
// Temporarily comment out to avoid unimplemented!() block
|
||||
// Cold = 1,
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
|
|
|
@ -6,15 +6,18 @@ use {
|
|||
account_storage::meta::StoredMetaWriteVersion,
|
||||
tiered_storage::{
|
||||
byte_block,
|
||||
footer::{AccountBlockFormat, AccountMetaFormat, OwnersBlockFormat},
|
||||
footer::{
|
||||
AccountBlockFormat, AccountMetaFormat, OwnersBlockFormat, TieredStorageFooter,
|
||||
},
|
||||
index::AccountIndexFormat,
|
||||
meta::{AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta},
|
||||
TieredStorageFormat,
|
||||
TieredStorageFormat, TieredStorageResult,
|
||||
},
|
||||
},
|
||||
memmap2::{Mmap, MmapOptions},
|
||||
modular_bitfield::prelude::*,
|
||||
solana_sdk::{hash::Hash, stake_history::Epoch},
|
||||
std::option::Option,
|
||||
std::{fs::OpenOptions, option::Option, path::Path},
|
||||
};
|
||||
|
||||
pub const HOT_FORMAT: TieredStorageFormat = TieredStorageFormat {
|
||||
|
@ -199,6 +202,39 @@ impl TieredAccountMeta for HotAccountMeta {
|
|||
}
|
||||
}
|
||||
|
||||
/// The reader to a hot accounts file.
|
||||
#[derive(Debug)]
|
||||
pub struct HotStorageReader {
|
||||
mmap: Mmap,
|
||||
footer: TieredStorageFooter,
|
||||
}
|
||||
|
||||
impl HotStorageReader {
|
||||
/// Constructs a HotStorageReader from the specified path.
|
||||
pub fn new_from_path(path: impl AsRef<Path>) -> TieredStorageResult<Self> {
|
||||
let file = OpenOptions::new().read(true).open(path)?;
|
||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||
// Here we are cloning the footer as accessing any data in a
|
||||
// TieredStorage instance requires accessing its Footer.
|
||||
// This can help improve cache locality and reduce the overhead
|
||||
// of indirection associated with memory-mapped accesses.
|
||||
let footer = TieredStorageFooter::new_from_mmap(&mmap)?.clone();
|
||||
|
||||
Ok(Self { mmap, footer })
|
||||
}
|
||||
|
||||
/// Returns the footer of the underlying tiered-storage accounts file.
|
||||
pub fn footer(&self) -> &TieredStorageFooter {
|
||||
&self.footer
|
||||
}
|
||||
|
||||
/// Returns the number of files inside the underlying tiered-storage
|
||||
/// accounts file.
|
||||
pub fn num_accounts(&self) -> usize {
|
||||
self.footer.account_entry_count as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use {
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
use {
|
||||
crate::{
|
||||
account_storage::meta::StoredMetaWriteVersion, tiered_storage::meta::TieredAccountMeta,
|
||||
account_storage::meta::StoredMetaWriteVersion,
|
||||
tiered_storage::{
|
||||
footer::{AccountMetaFormat, TieredStorageFooter},
|
||||
hot::HotStorageReader,
|
||||
meta::TieredAccountMeta,
|
||||
TieredStorageResult,
|
||||
},
|
||||
},
|
||||
solana_sdk::{account::ReadableAccount, hash::Hash, pubkey::Pubkey, stake_history::Epoch},
|
||||
std::path::Path,
|
||||
};
|
||||
|
||||
/// The struct that offers read APIs for accessing a TieredAccount.
|
||||
|
@ -83,3 +90,33 @@ impl<'accounts_file, M: TieredAccountMeta> ReadableAccount
|
|||
self.data()
|
||||
}
|
||||
}
|
||||
|
||||
/// The reader of a tiered storage instance.
|
||||
#[derive(Debug)]
|
||||
pub enum TieredStorageReader {
|
||||
Hot(HotStorageReader),
|
||||
}
|
||||
|
||||
impl TieredStorageReader {
|
||||
/// Creates a reader for the specified tiered storage accounts file.
|
||||
pub fn new_from_path(path: impl AsRef<Path>) -> TieredStorageResult<Self> {
|
||||
let footer = TieredStorageFooter::new_from_path(&path)?;
|
||||
match footer.account_meta_format {
|
||||
AccountMetaFormat::Hot => Ok(Self::Hot(HotStorageReader::new_from_path(path)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the footer of the associated HotAccountsFile.
|
||||
pub fn footer(&self) -> &TieredStorageFooter {
|
||||
match self {
|
||||
Self::Hot(hot) => hot.footer(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total number of accounts.
|
||||
pub fn num_accounts(&self) -> usize {
|
||||
match self {
|
||||
Self::Hot(hot) => hot.num_accounts(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue