solana_accounts_db/
tiered_storage.rs

1#![allow(dead_code)]
2
3pub mod byte_block;
4pub mod error;
5pub mod file;
6pub mod footer;
7pub mod hot;
8pub mod index;
9pub mod meta;
10pub mod mmap_utils;
11pub mod owners;
12pub mod readable;
13mod test_utils;
14
15use {
16    crate::{accounts_file::StoredAccountsInfo, storable_accounts::StorableAccounts},
17    error::TieredStorageError,
18    footer::{AccountBlockFormat, AccountMetaFormat},
19    hot::{HotStorageWriter, HOT_FORMAT},
20    index::IndexBlockFormat,
21    owners::OwnersBlockFormat,
22    readable::TieredStorageReader,
23    std::{
24        fs, io,
25        path::{Path, PathBuf},
26        sync::{
27            atomic::{AtomicBool, Ordering},
28            OnceLock,
29        },
30    },
31};
32
33pub type TieredStorageResult<T> = Result<T, TieredStorageError>;
34
35const MAX_TIERED_STORAGE_FILE_SIZE: u64 = 16 * 1024 * 1024 * 1024; // 16 GiB;
36
37/// The struct that defines the formats of all building blocks of a
38/// TieredStorage.
39#[derive(Clone, Debug, PartialEq)]
40pub struct TieredStorageFormat {
41    pub meta_entry_size: usize,
42    pub account_meta_format: AccountMetaFormat,
43    pub owners_block_format: OwnersBlockFormat,
44    pub index_block_format: IndexBlockFormat,
45    pub account_block_format: AccountBlockFormat,
46}
47
48/// The implementation of AccountsFile for tiered-storage.
49#[derive(Debug)]
50pub struct TieredStorage {
51    /// The internal reader instance for its accounts file.
52    reader: OnceLock<TieredStorageReader>,
53    /// A status flag indicating whether its file has been already written.
54    already_written: AtomicBool,
55    /// The path to the file that stores accounts.
56    path: PathBuf,
57}
58
59impl Drop for TieredStorage {
60    fn drop(&mut self) {
61        if let Err(err) = fs::remove_file(&self.path) {
62            // Here we bypass NotFound error as the focus of the panic is to
63            // detect any leakage of storage resource.
64            if err.kind() != io::ErrorKind::NotFound {
65                panic!(
66                    "TieredStorage failed to remove backing storage file '{}': {err}",
67                    self.path.display(),
68                );
69            }
70        }
71    }
72}
73
74impl TieredStorage {
75    /// Creates a new writable instance of TieredStorage based on the
76    /// specified path and TieredStorageFormat.
77    ///
78    /// Note that the actual file will not be created until write_accounts
79    /// is called.
80    pub fn new_writable(path: impl Into<PathBuf>) -> Self {
81        Self {
82            reader: OnceLock::<TieredStorageReader>::new(),
83            already_written: false.into(),
84            path: path.into(),
85        }
86    }
87
88    /// Creates a new read-only instance of TieredStorage from the
89    /// specified path.
90    pub fn new_readonly(path: impl Into<PathBuf>) -> TieredStorageResult<Self> {
91        let path = path.into();
92        Ok(Self {
93            reader: TieredStorageReader::new_from_path(&path).map(OnceLock::from)?,
94            already_written: true.into(),
95            path,
96        })
97    }
98
99    /// Returns the path to this TieredStorage.
100    pub fn path(&self) -> &Path {
101        self.path.as_path()
102    }
103
104    /// Writes the specified accounts into this TieredStorage.
105    ///
106    /// Note that this function can only be called once per a TieredStorage
107    /// instance.  Otherwise, it will trigger panic.
108    pub fn write_accounts<'a>(
109        &self,
110        accounts: &impl StorableAccounts<'a>,
111        skip: usize,
112        format: &TieredStorageFormat,
113    ) -> TieredStorageResult<StoredAccountsInfo> {
114        let was_written = self.already_written.swap(true, Ordering::AcqRel);
115
116        if was_written {
117            panic!("cannot write same tiered storage file more than once");
118        }
119
120        if format == &HOT_FORMAT {
121            let stored_accounts_info = {
122                let mut writer = HotStorageWriter::new(&self.path)?;
123                let stored_accounts_info = writer.write_accounts(accounts, skip)?;
124                writer.flush()?;
125                stored_accounts_info
126            };
127
128            // panic here if self.reader.get() is not None as self.reader can only be
129            // None since a false-value `was_written` indicates the accounts file has
130            // not been written previously, implying is_read_only() was also false.
131            debug_assert!(!self.is_read_only());
132            self.reader
133                .set(TieredStorageReader::new_from_path(&self.path)?)
134                .unwrap();
135
136            Ok(stored_accounts_info)
137        } else {
138            Err(TieredStorageError::UnknownFormat(self.path.to_path_buf()))
139        }
140    }
141
142    /// Returns the underlying reader of the TieredStorage.  None will be
143    /// returned if it's is_read_only() returns false.
144    pub fn reader(&self) -> Option<&TieredStorageReader> {
145        self.reader.get()
146    }
147
148    /// Returns true if the TieredStorage instance is read-only.
149    pub fn is_read_only(&self) -> bool {
150        self.reader.get().is_some()
151    }
152
153    /// Returns the size of the underlying accounts file.
154    pub fn len(&self) -> usize {
155        self.reader().map_or(0, |reader| reader.len())
156    }
157
158    /// Returns whether the underlying storage is empty.
159    pub fn is_empty(&self) -> bool {
160        self.len() == 0
161    }
162
163    pub fn capacity(&self) -> u64 {
164        self.reader()
165            .map_or(MAX_TIERED_STORAGE_FILE_SIZE, |reader| reader.capacity())
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use {
172        super::*,
173        file::TieredStorageMagicNumber,
174        footer::TieredStorageFooter,
175        hot::HOT_FORMAT,
176        solana_sdk::{
177            account::{AccountSharedData, ReadableAccount},
178            clock::Slot,
179            pubkey::Pubkey,
180            system_instruction::MAX_PERMITTED_DATA_LENGTH,
181        },
182        std::{
183            collections::{HashMap, HashSet},
184            mem::ManuallyDrop,
185        },
186        tempfile::tempdir,
187        test_utils::{create_test_account, verify_test_account_with_footer},
188    };
189
190    impl TieredStorage {
191        fn footer(&self) -> Option<&TieredStorageFooter> {
192            self.reader.get().map(|r| r.footer())
193        }
194    }
195
196    /// Simply invoke write_accounts with empty vector to allow the tiered storage
197    /// to persist non-account blocks such as footer, index block, etc.
198    fn write_zero_accounts(
199        tiered_storage: &TieredStorage,
200        expected_result: TieredStorageResult<StoredAccountsInfo>,
201    ) {
202        let slot_ignored = Slot::MAX;
203        let account_refs = Vec::<(&Pubkey, &AccountSharedData)>::new();
204        let storable_accounts = (slot_ignored, account_refs.as_slice());
205
206        let result = tiered_storage.write_accounts(&storable_accounts, 0, &HOT_FORMAT);
207
208        match (&result, &expected_result) {
209            (
210                Err(TieredStorageError::AttemptToUpdateReadOnly(_)),
211                Err(TieredStorageError::AttemptToUpdateReadOnly(_)),
212            ) => {}
213            (Err(TieredStorageError::Unsupported()), Err(TieredStorageError::Unsupported())) => {}
214            (Ok(_), Ok(_)) => {}
215            // we don't expect error type mis-match or other error types here
216            _ => {
217                panic!("actual: {result:?}, expected: {expected_result:?}");
218            }
219        };
220
221        assert!(tiered_storage.is_read_only());
222        assert_eq!(
223            tiered_storage.len(),
224            std::mem::size_of::<TieredStorageFooter>()
225                + std::mem::size_of::<TieredStorageMagicNumber>()
226        );
227    }
228
229    #[test]
230    fn test_new_meta_file_only() {
231        // Generate a new temp path that is guaranteed to NOT already have a file.
232        let temp_dir = tempdir().unwrap();
233        let tiered_storage_path = temp_dir.path().join("test_new_meta_file_only");
234
235        {
236            let tiered_storage =
237                ManuallyDrop::new(TieredStorage::new_writable(&tiered_storage_path));
238
239            assert!(!tiered_storage.is_read_only());
240            assert_eq!(tiered_storage.path(), tiered_storage_path);
241            assert_eq!(tiered_storage.len(), 0);
242
243            write_zero_accounts(
244                &tiered_storage,
245                Ok(StoredAccountsInfo {
246                    offsets: vec![],
247                    size: 0,
248                }),
249            );
250        }
251
252        let tiered_storage_readonly = TieredStorage::new_readonly(&tiered_storage_path).unwrap();
253        let footer = tiered_storage_readonly.footer().unwrap();
254        assert!(tiered_storage_readonly.is_read_only());
255        assert_eq!(tiered_storage_readonly.reader().unwrap().num_accounts(), 0);
256        assert_eq!(footer.account_meta_format, HOT_FORMAT.account_meta_format);
257        assert_eq!(footer.owners_block_format, HOT_FORMAT.owners_block_format);
258        assert_eq!(footer.index_block_format, HOT_FORMAT.index_block_format);
259        assert_eq!(footer.account_block_format, HOT_FORMAT.account_block_format);
260        assert_eq!(
261            tiered_storage_readonly.len(),
262            std::mem::size_of::<TieredStorageFooter>()
263                + std::mem::size_of::<TieredStorageMagicNumber>()
264        );
265    }
266
267    #[test]
268    #[should_panic(expected = "cannot write same tiered storage file more than once")]
269    fn test_write_accounts_twice() {
270        // Generate a new temp path that is guaranteed to NOT already have a file.
271        let temp_dir = tempdir().unwrap();
272        let tiered_storage_path = temp_dir.path().join("test_write_accounts_twice");
273
274        let tiered_storage = TieredStorage::new_writable(&tiered_storage_path);
275        write_zero_accounts(
276            &tiered_storage,
277            Ok(StoredAccountsInfo {
278                offsets: vec![],
279                size: 0,
280            }),
281        );
282        // Expect AttemptToUpdateReadOnly error as write_accounts can only
283        // be invoked once.
284        write_zero_accounts(
285            &tiered_storage,
286            Err(TieredStorageError::AttemptToUpdateReadOnly(
287                tiered_storage_path,
288            )),
289        );
290    }
291
292    #[test]
293    fn test_remove_on_drop() {
294        // Generate a new temp path that is guaranteed to NOT already have a file.
295        let temp_dir = tempdir().unwrap();
296        let tiered_storage_path = temp_dir.path().join("test_remove_on_drop");
297        {
298            let tiered_storage = TieredStorage::new_writable(&tiered_storage_path);
299            write_zero_accounts(
300                &tiered_storage,
301                Ok(StoredAccountsInfo {
302                    offsets: vec![],
303                    size: 0,
304                }),
305            );
306        }
307        // expect the file does not exists as it has been removed on drop
308        assert!(!tiered_storage_path.try_exists().unwrap());
309
310        {
311            let tiered_storage =
312                ManuallyDrop::new(TieredStorage::new_writable(&tiered_storage_path));
313            write_zero_accounts(
314                &tiered_storage,
315                Ok(StoredAccountsInfo {
316                    offsets: vec![],
317                    size: 0,
318                }),
319            );
320        }
321        // expect the file exists as we have ManuallyDrop this time.
322        assert!(tiered_storage_path.try_exists().unwrap());
323
324        {
325            // open again in read-only mode with ManuallyDrop.
326            _ = ManuallyDrop::new(TieredStorage::new_readonly(&tiered_storage_path).unwrap());
327        }
328        // again expect the file exists as we have ManuallyDrop.
329        assert!(tiered_storage_path.try_exists().unwrap());
330
331        {
332            // open again without ManuallyDrop in read-only mode
333            _ = TieredStorage::new_readonly(&tiered_storage_path).unwrap();
334        }
335        // expect the file does not exist as the file has been removed on drop
336        assert!(!tiered_storage_path.try_exists().unwrap());
337    }
338
339    /// The helper function for all write_accounts tests.
340    /// Currently only supports hot accounts.
341    fn do_test_write_accounts(
342        path_suffix: &str,
343        account_data_sizes: &[u64],
344        format: TieredStorageFormat,
345    ) {
346        let accounts: Vec<_> = account_data_sizes
347            .iter()
348            .map(|size| create_test_account(*size))
349            .collect();
350
351        let account_refs: Vec<_> = accounts
352            .iter()
353            .map(|account| (&account.0.pubkey, &account.1))
354            .collect();
355
356        // Slot information is not used here
357        let storable_accounts = (Slot::MAX, &account_refs[..]);
358
359        let temp_dir = tempdir().unwrap();
360        let tiered_storage_path = temp_dir.path().join(path_suffix);
361        let tiered_storage = TieredStorage::new_writable(tiered_storage_path);
362        _ = tiered_storage.write_accounts(&storable_accounts, 0, &format);
363
364        let reader = tiered_storage.reader().unwrap();
365        let num_accounts = storable_accounts.len();
366        assert_eq!(reader.num_accounts(), num_accounts);
367
368        let mut expected_accounts_map = HashMap::new();
369        for i in 0..num_accounts {
370            storable_accounts.account_default_if_zero_lamport(i, |account| {
371                expected_accounts_map.insert(*account.pubkey(), account.to_account_shared_data());
372            });
373        }
374
375        let mut verified_accounts = HashSet::new();
376        let footer = reader.footer();
377
378        const MIN_PUBKEY: Pubkey = Pubkey::new_from_array([0x00u8; 32]);
379        const MAX_PUBKEY: Pubkey = Pubkey::new_from_array([0xFFu8; 32]);
380        let mut min_pubkey = MAX_PUBKEY;
381        let mut max_pubkey = MIN_PUBKEY;
382
383        reader
384            .scan_accounts(|stored_account_meta| {
385                if let Some(account) = expected_accounts_map.get(stored_account_meta.pubkey()) {
386                    verify_test_account_with_footer(
387                        &stored_account_meta,
388                        account,
389                        stored_account_meta.pubkey(),
390                        footer,
391                    );
392                    verified_accounts.insert(*stored_account_meta.pubkey());
393                    if min_pubkey > *stored_account_meta.pubkey() {
394                        min_pubkey = *stored_account_meta.pubkey();
395                    }
396                    if max_pubkey < *stored_account_meta.pubkey() {
397                        max_pubkey = *stored_account_meta.pubkey();
398                    }
399                }
400            })
401            .unwrap();
402
403        assert_eq!(footer.min_account_address, min_pubkey);
404        assert_eq!(footer.max_account_address, max_pubkey);
405        assert!(!verified_accounts.is_empty());
406        assert_eq!(verified_accounts.len(), expected_accounts_map.len());
407    }
408
409    #[test]
410    fn test_write_accounts_small_accounts() {
411        do_test_write_accounts(
412            "test_write_accounts_small_accounts",
413            &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
414            HOT_FORMAT.clone(),
415        );
416    }
417
418    #[test]
419    fn test_write_accounts_one_max_len() {
420        do_test_write_accounts(
421            "test_write_accounts_one_max_len",
422            &[MAX_PERMITTED_DATA_LENGTH],
423            HOT_FORMAT.clone(),
424        );
425    }
426
427    #[test]
428    fn test_write_accounts_mixed_size() {
429        do_test_write_accounts(
430            "test_write_accounts_mixed_size",
431            &[
432                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1000, 2000, 3000, 4000, 9, 8, 7, 6, 5, 4, 3, 2, 1,
433            ],
434            HOT_FORMAT.clone(),
435        );
436    }
437}