diff --git a/magicblock-accounts-db/src/index.rs b/magicblock-accounts-db/src/index.rs index 413b8dd67..edc71ad05 100644 --- a/magicblock-accounts-db/src/index.rs +++ b/magicblock-accounts-db/src/index.rs @@ -2,27 +2,26 @@ use std::path::Path; use iterator::OffsetPubkeyIter; use lmdb::{ - Cursor, Database, DatabaseFlags, Environment, RwTransaction, Transaction, - WriteFlags, + Cursor, DatabaseFlags, Environment, RwTransaction, Transaction, WriteFlags, }; -use lmdb_utils::*; use log::warn; use solana_pubkey::Pubkey; -use standalone::StandaloneIndex; +use table::Table; +use utils::*; use crate::{ + error::AccountsDbError, log_err, storage::{Allocation, ExistingAllocation}, - AccountsDbConfig, AdbResult, + AdbResult, }; const WEMPTY: WriteFlags = WriteFlags::empty(); -const ACCOUNTS_PATH: &str = "accounts"; -const ACCOUNTS_INDEX: Option<&str> = Some("accounts-idx"); -const PROGRAMS_INDEX: Option<&str> = Some("programs-idx"); -const DEALLOCATIONS_INDEX_PATH: &str = "deallocations"; -const OWNERS_INDEX_PATH: &str = "owners"; +const ACCOUNTS_INDEX: &str = "accounts-idx"; +const PROGRAMS_INDEX: &str = "programs-idx"; +const DEALLOCATIONS_INDEX: &str = "deallocations-idx"; +const OWNERS_INDEX: &str = "owners-idx"; /// LMDB Index manager pub(crate) struct AccountsDbIndex { @@ -32,7 +31,7 @@ pub(crate) struct AccountsDbIndex { /// the value is a concatenation of: /// 1. offset in the storage (4 bytes) /// 2. number of allocated blocks (4 bytes) - accounts: Database, + accounts: Table, /// Programs Index, used to keep track of owner->accounts /// mapping, significantly speeds up program accounts retrieval /// @@ -40,24 +39,24 @@ pub(crate) struct AccountsDbIndex { /// the value is a concatenation of: /// 1. offset in the storage (4 bytes) /// 2. account pubkey (32 bytes) - programs: Database, + programs: Table, /// Deallocation Index, used to keep track of allocation size of deallocated /// accounts, this is further utilized when defragmentation is required, by - /// matching new accounts' size and already present "holes" in database + /// matching new accounts' size and already present "holes" in the database /// /// the key is the allocation size in blocks (4 bytes) /// the value is a concatenation of: /// 1. offset in the storage (4 bytes) /// 2. number of allocated blocks (4 bytes) - deallocations: StandaloneIndex, + deallocations: Table, /// Index map from accounts' pubkeys to their current owners, the index is /// used primarily for cleanup purposes when owner change occures and we need /// to cleanup programs index, so that old owner -> account mapping doesn't dangle /// /// the key is the account's pubkey (32 bytes) /// the value is owner's pubkey (32 bytes) - owners: StandaloneIndex, - /// Common envorinment for accounts and programs databases + owners: Table, + /// Common envorinment for all of the tables env: Environment, } @@ -90,40 +89,35 @@ macro_rules! bytes { impl AccountsDbIndex { /// Creates new index manager for AccountsDB, by /// opening/creating necessary lmdb environments - pub(crate) fn new( - config: &AccountsDbConfig, - directory: &Path, - ) -> AdbResult { - // create an environment for 2 databases: accounts and programs index - let env = lmdb_env(ACCOUNTS_PATH, directory, config.index_map_size, 2) - .inspect_err(log_err!( - "main index env creation at {}", - directory.display() - ))?; - let accounts = env.create_db(ACCOUNTS_INDEX, DatabaseFlags::empty())?; - let programs = env.create_db( + pub(crate) fn new(size: usize, directory: &Path) -> AdbResult { + // create an environment for all the tables + let env = lmdb_env(directory, size).inspect_err(log_err!( + "main index env creation at {}", + directory.display() + ))?; + let accounts = + Table::new(&env, ACCOUNTS_INDEX, DatabaseFlags::empty())?; + let programs = Table::new( + &env, PROGRAMS_INDEX, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED, )?; - let deallocations = StandaloneIndex::new( - DEALLOCATIONS_INDEX_PATH, - directory, - config.index_map_size, - DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED, + let deallocations = Table::new( + &env, + DEALLOCATIONS_INDEX, + DatabaseFlags::DUP_SORT + | DatabaseFlags::DUP_FIXED + | DatabaseFlags::REVERSE_KEY + | DatabaseFlags::INTEGER_KEY, )?; - let owners = StandaloneIndex::new( - OWNERS_INDEX_PATH, - directory, - config.index_map_size, - DatabaseFlags::empty(), - )?; + let owners = Table::new(&env, OWNERS_INDEX, DatabaseFlags::empty())?; Ok(Self { accounts, programs, deallocations, - env, owners, + env, }) } @@ -131,7 +125,9 @@ impl AccountsDbIndex { #[inline(always)] pub(crate) fn get_account_offset(&self, pubkey: &Pubkey) -> AdbResult { let txn = self.env.begin_ro_txn()?; - let offset = txn.get(self.accounts, pubkey)?; + let Some(offset) = self.accounts.get(&txn, pubkey)? else { + return Err(AccountsDbError::NotFound); + }; let offset = // SAFETY: // The accounts index stores two u32 values (offset and blocks) @@ -146,12 +142,14 @@ impl AccountsDbIndex { } /// Retrieve the offset and the size (number of blocks) given account occupies - fn get_allocation( + fn get_allocation( &self, - txn: &RwTransaction, + txn: &T, pubkey: &Pubkey, ) -> AdbResult { - let slice = txn.get(self.accounts, pubkey)?; + let Some(slice) = self.accounts.get(txn, pubkey)? else { + return Err(AccountsDbError::NotFound); + }; let (offset, blocks) = bytes!(#unpack, slice, u32, u32); Ok(ExistingAllocation { offset, blocks }) } @@ -175,29 +173,22 @@ impl AccountsDbIndex { let offset_and_pubkey = bytes!(#pack, offset, u32, *pubkey, Pubkey); // optimisitically try to insert account to index, assuming that it doesn't exist - let result = txn.put( - self.accounts, - pubkey, - &index_value, - WriteFlags::NO_OVERWRITE, - ); + let inserted = + self.accounts + .put_if_not_exists(&mut txn, pubkey, index_value)?; // if the account does exist, then it already occupies space in main storage - match result { - Ok(_) => {} - // in which case we just move the account to new allocation - // adjusting all offset and cleaning up older ones - Err(lmdb::Error::KeyExist) => { - let previous = - self.reallocate_account(pubkey, &mut txn, &index_value)?; - dealloc.replace(previous); - } - Err(err) => return Err(err.into()), + if !inserted { + // in which case we just move the account to a new allocation + // adjusting all of the offsets and cleaning up the older ones + let previous = + self.reallocate_account(pubkey, &mut txn, &index_value)?; + dealloc.replace(previous); }; // track the account via programs' index as well - txn.put(self.programs, owner, &offset_and_pubkey, WEMPTY)?; + self.programs.put(&mut txn, owner, offset_and_pubkey)?; // track the reverse relation between account and its owner - self.owners.put(pubkey, owner)?; + self.owners.put(&mut txn, pubkey, owner)?; txn.commit()?; Ok(dealloc) @@ -213,26 +204,24 @@ impl AccountsDbIndex { // retrieve the size and offset for allocation let allocation = self.get_allocation(txn, pubkey)?; // and put it into deallocation index, so the space can be recycled later - self.deallocations.put( - BigEndianU32::new(allocation.blocks), - bytes!(#pack, allocation.offset, u32, allocation.blocks, u32), - )?; + let key = allocation.blocks.to_le_bytes(); + let value = + bytes!(#pack, allocation.offset, u32, allocation.blocks, u32); + self.deallocations.put(txn, key, value)?; // now we can overwrite the index record - txn.put(self.accounts, pubkey, &index_value, WEMPTY)?; + self.accounts.put(txn, pubkey, index_value)?; // we also need to delete old entry from `programs` index - match self.remove_programs_index_entry(pubkey, txn, allocation.offset) { - Ok(()) | Err(lmdb::Error::NotFound) => Ok(allocation), - Err(err) => Err(err.into()), - } + self.remove_programs_index_entry(pubkey, None, txn, allocation.offset)?; + Ok(allocation) } - /// Removes account from database and marks its backing storage for recycling - /// this method also performs various cleanup operations on secondary indexes + /// Removes account from the database and marks its backing storage for recycling + /// this method also performs various cleanup operations on the secondary indexes pub(crate) fn remove_account(&self, pubkey: &Pubkey) -> AdbResult<()> { let mut txn = self.env.begin_rw_txn()?; - let mut cursor = txn.open_rw_cursor(self.accounts)?; + let mut cursor = self.accounts.cursor_rw(&mut txn)?; // locate the account entry let result = cursor @@ -250,17 +239,14 @@ impl AccountsDbIndex { // mark the allocation for future recycling self.deallocations.put( - BigEndianU32::new(blocks), + &mut txn, + blocks.to_le_bytes(), bytes!(#pack, offset, u32, blocks, u32), )?; // we also need to cleanup `programs` index - match self.remove_programs_index_entry(pubkey, &mut txn, offset) { - Ok(()) | Err(lmdb::Error::NotFound) => { - txn.commit()?; - } - Err(err) => return Err(err.into()), - } + self.remove_programs_index_entry(pubkey, None, &mut txn, offset)?; + txn.commit()?; Ok(()) } @@ -272,28 +258,31 @@ impl AccountsDbIndex { pubkey: &Pubkey, owner: &Pubkey, ) -> AdbResult<()> { - match self.owners.getter()?.get(pubkey) { + let txn = self.env.begin_ro_txn()?; + let old_owner = match self.owners.get(&txn, pubkey)? { // if current owner matches with that stored in index, then we are all set - Ok(val) if owner.as_ref() == val => { - return Ok(()); - } - Err(lmdb::Error::NotFound) => { + Some(val) if owner.as_ref() == val => { return Ok(()); } - // if they don't match, well then we have to remove old entries and create new ones - Ok(_) => (), - Err(err) => Err(err)?, + None => return Ok(()), + // if they don't match, then we have to remove old entries and create new ones + Some(val) => Pubkey::try_from(val).ok(), }; - let mut txn = self.env.begin_rw_txn()?; let allocation = self.get_allocation(&txn, pubkey)?; + let mut txn = self.env.begin_rw_txn()?; // cleanup `programs` and `owners` index - self.remove_programs_index_entry(pubkey, &mut txn, allocation.offset)?; + self.remove_programs_index_entry( + pubkey, + old_owner, + &mut txn, + allocation.offset, + )?; // track new owner of the account via programs' index let offset_and_pubkey = bytes!(#pack, allocation.offset, u32, *pubkey, Pubkey); - txn.put(self.programs, owner, &offset_and_pubkey, WEMPTY)?; + self.programs.put(&mut txn, owner, offset_and_pubkey)?; // track the reverse relation between account and its owner - self.owners.put(pubkey, owner)?; + self.owners.put(&mut txn, pubkey, owner)?; txn.commit().map_err(Into::into) } @@ -301,13 +290,19 @@ impl AccountsDbIndex { fn remove_programs_index_entry( &self, pubkey: &Pubkey, + old_owner: Option, txn: &mut RwTransaction, offset: u32, ) -> lmdb::Result<()> { - // in order to delete old entry from `programs` index, we consult - // `owners` index to fetch previous owner of the account - let owner = match self.owners.getter()?.get(pubkey) { - Ok(val) => { + let val = bytes!(#pack, offset, u32, *pubkey, Pubkey); + if let Some(owner) = old_owner { + return self.programs.del(txn, owner, Some(&val)); + } + // in order to delete the old entry from `programs` index, we consult + // the `owners` index to fetch the previous owner of the account + let mut owners = self.owners.cursor_rw(txn)?; + let owner = match owners.get(Some(pubkey.as_ref()), None, MDB_SET_OP) { + Ok((_, val)) => { let pk = Pubkey::try_from(val).inspect_err(log_err!( "owners index contained invalid value for pubkey of len {}", val.len() @@ -321,24 +316,14 @@ impl AccountsDbIndex { warn!("account {pubkey} didn't have owners index entry"); return Ok(()); } - Err(err) => Err(err)?, + Err(e) => { + return Err(e); + } }; + owners.del(WEMPTY)?; + drop(owners); - let mut cursor = txn.open_rw_cursor(self.programs)?; - - let key = Some(owner.as_ref()); - let val = bytes!(#pack, offset, u32, *pubkey, Pubkey); - // locate the entry matching the owner and offset/pubkey combo - let found = cursor.get(key, Some(&val), MDB_GET_BOTH_OP).is_ok(); - if found { - // delete the entry only if it was located successfully - cursor.del(WEMPTY)?; - } else { - // NOTE: this should never happend in consistent database - warn!("account {pubkey} with owner {owner} didn't have programs index entry"); - } - // and cleanup `owners` index as well - self.owners.del(pubkey)?; + self.programs.del(txn, owner, Some(&val))?; Ok(()) } @@ -347,23 +332,25 @@ impl AccountsDbIndex { pub(crate) fn get_program_accounts_iter( &self, program: &Pubkey, - ) -> AdbResult> { + ) -> AdbResult> { let txn = self.env.begin_ro_txn()?; - OffsetPubkeyIter::new(self.programs, txn, Some(program)) + OffsetPubkeyIter::new(&self.programs, txn, Some(program)) } /// Returns an iterator over offsets and pubkeys of all accounts in database /// offsets can be used further to retrieve the account from storage - pub(crate) fn get_all_accounts( - &self, - ) -> AdbResult> { + pub(crate) fn get_all_accounts(&self) -> AdbResult> { let txn = self.env.begin_ro_txn()?; - OffsetPubkeyIter::new(self.programs, txn, None) + OffsetPubkeyIter::new(&self.programs, txn, None) } - /// Returns the number of accounts in database + /// Returns the number of accounts in the database pub(crate) fn get_accounts_count(&self) -> usize { - self.owners.len() + let Ok(txn) = self.env.begin_ro_txn() else { + warn!("failed to start transaction for stats retrieval"); + return 0; + }; + self.owners.entries(&txn) } /// Check whether allocation of given size (in blocks) exists. @@ -373,20 +360,21 @@ impl AccountsDbIndex { &self, space: u32, ) -> AdbResult { - let mut cursor = self.deallocations.cursor()?; + let mut txn = self.env.begin_rw_txn()?; + let mut cursor = self.deallocations.cursor_rw(&mut txn)?; // this is a neat lmdb trick where we can search for entry with matching // or greater key since we are interested in any allocation of at least // `blocks` size or greater, this works perfectly well for this case - let key = BigEndianU32::new(space); let (_, val) = - cursor.get(Some(key.as_ref()), None, MDB_SET_RANGE_OP)?; + cursor.get(Some(&space.to_le_bytes()), None, MDB_SET_RANGE_OP)?; let (offset, blocks) = bytes!(#unpack, val, u32, u32); // delete the allocation record from recycleable list cursor.del(WEMPTY)?; - cursor.commit()?; + drop(cursor); + txn.commit()?; Ok(ExistingAllocation { offset, blocks }) } @@ -399,8 +387,6 @@ impl AccountsDbIndex { .env .sync(true) .inspect_err(log_err!("main index flushing")); - self.deallocations.sync(); - self.owners.sync(); } /// Reopen the index databases from a different directory at provided path @@ -410,38 +396,14 @@ impl AccountsDbIndex { // set it to default lmdb map size, it will be // ignored if smaller than currently occupied const DEFAULT_SIZE: usize = 1024 * 1024; - let env = - lmdb_env(ACCOUNTS_PATH, dbpath, DEFAULT_SIZE, 2).inspect_err( - log_err!("main index env creation at {}", dbpath.display()), - )?; - let accounts = env.create_db(ACCOUNTS_INDEX, DatabaseFlags::empty())?; - let programs = env.create_db( - PROGRAMS_INDEX, - DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED, - )?; - let deallocations = StandaloneIndex::new( - DEALLOCATIONS_INDEX_PATH, - dbpath, - DEFAULT_SIZE, - DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED, - )?; - let owners = StandaloneIndex::new( - OWNERS_INDEX_PATH, - dbpath, - DEFAULT_SIZE, - DatabaseFlags::empty(), - )?; - self.env = env; - self.accounts = accounts; - self.programs = programs; - self.deallocations = deallocations; - self.owners = owners; + *self = Self::new(DEFAULT_SIZE, dbpath)?; Ok(()) } } pub(crate) mod iterator; -mod lmdb_utils; -mod standalone; +mod utils; +//mod standalone; +mod table; #[cfg(test)] mod tests; diff --git a/magicblock-accounts-db/src/index/iterator.rs b/magicblock-accounts-db/src/index/iterator.rs index b16947399..437b7e35f 100644 --- a/magicblock-accounts-db/src/index/iterator.rs +++ b/magicblock-accounts-db/src/index/iterator.rs @@ -1,8 +1,8 @@ -use lmdb::{Cursor, Database, RoCursor, RoTransaction, Transaction}; +use lmdb::{Cursor, RoCursor, RoTransaction}; use log::error; use solana_pubkey::Pubkey; -use super::lmdb_utils::MDB_GET_CURRENT_OP; +use super::{table::Table, MDB_SET_OP}; use crate::AdbResult; /// Iterator over pubkeys and offsets, where accounts @@ -10,51 +10,50 @@ use crate::AdbResult; /// /// S: Starting position operation, determines where to place cursor initially /// N: Next position operation, determines where to move cursor next -pub(crate) struct OffsetPubkeyIter<'env, const S: u32, const N: u32> { - cursor: RoCursor<'env>, - terminated: bool, +pub(crate) struct OffsetPubkeyIter<'env> { + iter: lmdb::Iter<'env>, + _cursor: RoCursor<'env>, _txn: RoTransaction<'env>, } -impl<'a, const S: u32, const N: u32> OffsetPubkeyIter<'a, S, N> { +impl<'env> OffsetPubkeyIter<'env> { pub(super) fn new( - db: Database, - txn: RoTransaction<'a>, + table: &Table, + txn: RoTransaction<'env>, pubkey: Option<&Pubkey>, ) -> AdbResult { - let cursor = txn.open_ro_cursor(db)?; + let cursor = table.cursor_ro(&txn)?; // SAFETY: // nasty/neat trick for lifetime erasure, but we are upholding - // the rust's ownership contracts by keeping txn around as well - let cursor: RoCursor = unsafe { std::mem::transmute(cursor) }; - // jump to the first entry, key might be ignored depending on OP - cursor.get(pubkey.map(AsRef::as_ref), None, S)?; + // the rust's ownership contracts by keeping txn around as well + let mut cursor: RoCursor = unsafe { std::mem::transmute(cursor) }; + let iter = if let Some(pubkey) = pubkey { + // NOTE: there's a bug in the LMDB, which ignores NotFound error when + // iterating on DUPSORT databases, where it just jumps to the next key, + // here we check for the error explicitly to prevent this behavior + if let Err(lmdb::Error::NotFound) = + cursor.get(Some(pubkey.as_ref()), None, MDB_SET_OP) + { + lmdb::Iter::Err(lmdb::Error::NotFound) + } else { + cursor.iter_dup_of(pubkey) + } + } else { + cursor.iter_start() + }; Ok(Self { _txn: txn, - cursor, - terminated: false, + _cursor: cursor, + iter, }) } } -impl Iterator for OffsetPubkeyIter<'_, S, N> { +impl Iterator for OffsetPubkeyIter<'_> { type Item = (u32, Pubkey); fn next(&mut self) -> Option { - if self.terminated { - return None; - } - - match self.cursor.get(None, None, MDB_GET_CURRENT_OP) { - Ok(entry) => { - // advance the cursor, - let advance = self.cursor.get(None, None, N); - // if we move past the iterable range, NotFound will be - // triggered by OP, and we can terminate the iteration - if let Err(lmdb::Error::NotFound) = advance { - self.terminated = true; - } - Some(bytes!(#unpack, entry.1, u32, Pubkey)) - } + match self.iter.next()? { + Ok(entry) => Some(bytes!(#unpack, entry.1, u32, Pubkey)), Err(error) => { error!("error advancing offset iterator cursor: {error}"); None diff --git a/magicblock-accounts-db/src/index/standalone.rs b/magicblock-accounts-db/src/index/standalone.rs deleted file mode 100644 index 0e4b1c1d3..000000000 --- a/magicblock-accounts-db/src/index/standalone.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - path::Path, -}; - -use lmdb::{ - Cursor, Database, DatabaseFlags, Environment, RoTransaction, RwCursor, - RwTransaction, Transaction, -}; - -use super::{ - lmdb_utils::{lmdb_env, MDB_SET_OP}, - WEMPTY, -}; -use crate::{log_err, AdbResult}; - -pub(super) struct StandaloneIndex { - db: Database, - env: Environment, -} - -impl StandaloneIndex { - pub(super) fn new( - name: &str, - dbpath: &Path, - size: usize, - flags: DatabaseFlags, - ) -> AdbResult { - let env = lmdb_env(name, dbpath, size, 1).inspect_err(log_err!( - "deallocation index creation at {}", - dbpath.display() - ))?; - let db = env.create_db(None, flags)?; - Ok(Self { env, db }) - } - - pub(super) fn put( - &self, - key: impl AsRef<[u8]>, - val: impl AsRef<[u8]>, - ) -> lmdb::Result<()> { - let mut txn = self.rwtxn()?; - txn.put(self.db, &key, &val, WEMPTY)?; - txn.commit() - } - - pub(super) fn getter(&self) -> lmdb::Result { - self.rotxn() - .map(|txn| StandaloneIndexGetter { txn, db: self.db }) - } - - pub(super) fn del(&self, key: impl AsRef<[u8]>) -> lmdb::Result<()> { - let mut txn = self.rwtxn()?; - let mut cursor = txn.open_rw_cursor(self.db)?; - match cursor.get(Some(key.as_ref()), None, MDB_SET_OP) { - Ok(_) => (), - Err(lmdb::Error::NotFound) => return Ok(()), - Err(err) => Err(err)?, - } - cursor.del(WEMPTY)?; - drop(cursor); - txn.commit() - } - - pub(super) fn cursor(&self) -> lmdb::Result> { - let mut txn = self.rwtxn()?; - let inner = txn.open_rw_cursor(self.db)?; - // SAFETY: - // We erase the lifetime of cursor which is bound to _txn since we keep - // txn bundled with cursor (inner) it's safe to perform the transmutation - let inner = unsafe { - std::mem::transmute::, lmdb::RwCursor<'_>>(inner) - }; - - Ok(StandaloneIndexCursor { inner, txn }) - } - - pub(super) fn sync(&self) { - // it's ok to ignore error, as it will only happen if something utterly terrible - // happened at OS level, in which case we most likely won't even reach this code - let _ = self - .env - .sync(true) - .inspect_err(log_err!("secondary index flushing")); - } - - pub(super) fn len(&self) -> usize { - self.env - .stat() - .inspect_err(log_err!("secondary index stat retrieval")) - .map(|stat| stat.entries()) - .unwrap_or_default() - } - - fn rotxn(&self) -> lmdb::Result { - self.env.begin_ro_txn() - } - - fn rwtxn(&self) -> lmdb::Result { - self.env.begin_rw_txn() - } -} - -pub(super) struct StandaloneIndexGetter<'a> { - txn: RoTransaction<'a>, - db: Database, -} - -impl StandaloneIndexGetter<'_> { - pub(super) fn get(&self, key: impl AsRef<[u8]>) -> lmdb::Result<&[u8]> { - self.txn.get(self.db, &key) - } -} - -pub(super) struct StandaloneIndexCursor<'a> { - inner: RwCursor<'a>, - txn: RwTransaction<'a>, -} - -impl<'a> Deref for StandaloneIndexCursor<'a> { - type Target = RwCursor<'a>; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for StandaloneIndexCursor<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -impl StandaloneIndexCursor<'_> { - pub(super) fn commit(self) -> lmdb::Result<()> { - drop(self.inner); - self.txn.commit() - } -} diff --git a/magicblock-accounts-db/src/index/table.rs b/magicblock-accounts-db/src/index/table.rs new file mode 100644 index 000000000..2935aa9da --- /dev/null +++ b/magicblock-accounts-db/src/index/table.rs @@ -0,0 +1,92 @@ +use lmdb::{ + Database, DatabaseFlags, Environment, RoCursor, RwCursor, RwTransaction, + Transaction, WriteFlags, +}; + +use super::WEMPTY; + +pub(super) struct Table { + db: Database, +} + +impl Table { + pub(super) fn new( + env: &Environment, + name: &str, + flags: DatabaseFlags, + ) -> lmdb::Result { + let db = env.create_db(Some(name), flags)?; + Ok(Self { db }) + } + + #[inline] + pub(super) fn get<'txn, T: Transaction, K: AsRef<[u8]>>( + &self, + txn: &'txn T, + key: K, + ) -> lmdb::Result> { + match txn.get(self.db, &key) { + Ok(bytes) => Ok(Some(bytes)), + Err(lmdb::Error::NotFound) => Ok(None), + Err(e) => Err(e), + } + } + + #[inline] + pub(super) fn put, V: AsRef<[u8]>>( + &self, + txn: &mut RwTransaction, + key: K, + value: V, + ) -> lmdb::Result<()> { + txn.put(self.db, &key, &value, WEMPTY) + } + + #[inline] + pub(super) fn del>( + &self, + txn: &mut RwTransaction, + key: K, + value: Option<&[u8]>, + ) -> lmdb::Result<()> { + match txn.del(self.db, &key, value) { + Ok(_) | Err(lmdb::Error::NotFound) => Ok(()), + Err(e) => Err(e), + } + } + + #[inline] + pub(super) fn put_if_not_exists, V: AsRef<[u8]>>( + &self, + txn: &mut RwTransaction, + key: K, + value: V, + ) -> lmdb::Result { + let result = txn.put(self.db, &key, &value, WriteFlags::NO_OVERWRITE); + match result { + Ok(_) => Ok(true), + Err(lmdb::Error::KeyExist) => Ok(false), + Err(err) => Err(err), + } + } + + #[inline] + pub(super) fn cursor_ro<'txn, T: Transaction>( + &self, + txn: &'txn T, + ) -> lmdb::Result> { + txn.open_ro_cursor(self.db) + } + + #[inline] + pub(super) fn cursor_rw<'txn>( + &self, + txn: &'txn mut RwTransaction, + ) -> lmdb::Result> { + txn.open_rw_cursor(self.db) + } + + pub(super) fn entries(&self, txn: &T) -> usize { + txn.stat(self.db).map(|s| s.entries()).unwrap_or_default() + } +} diff --git a/magicblock-accounts-db/src/index/tests.rs b/magicblock-accounts-db/src/index/tests.rs index 15307bea6..055ed0658 100644 --- a/magicblock-accounts-db/src/index/tests.rs +++ b/magicblock-accounts-db/src/index/tests.rs @@ -171,7 +171,7 @@ fn test_ensure_correct_owner() { ); let result = tenv.get_program_accounts_iter(&owner); assert!( - matches!(result, Err(AccountsDbError::NotFound)), + matches!(result.map(|i| i.count()), Ok(0)), "programs index still has record of account after owner change" ); @@ -201,14 +201,18 @@ fn test_program_index_cleanup() { .env .begin_rw_txn() .expect("failed to start new RW transaction"); - let result = - tenv.remove_programs_index_entry(&pubkey, &mut txn, allocation.offset); + let result = tenv.remove_programs_index_entry( + &pubkey, + None, + &mut txn, + allocation.offset, + ); assert!(result.is_ok(), "failed to remove entry from programs index"); txn.commit().expect("failed to commit transaction"); let result = tenv.get_program_accounts_iter(&owner); assert!( - matches!(result, Err(AccountsDbError::NotFound)), + matches!(result.map(|i| i.count()), Ok(0)), "programs index still has record of account after cleanup" ); } @@ -339,7 +343,7 @@ fn setup() -> IndexTestEnv { let directory = tempfile::tempdir() .expect("failed to create temp directory for index tests") .keep(); - let index = AccountsDbIndex::new(&config, &directory) + let index = AccountsDbIndex::new(config.index_map_size, &directory) .expect("failed to create accountsdb index in temp dir"); IndexTestEnv { index, directory } } diff --git a/magicblock-accounts-db/src/index/lmdb_utils.rs b/magicblock-accounts-db/src/index/utils.rs similarity index 50% rename from magicblock-accounts-db/src/index/lmdb_utils.rs rename to magicblock-accounts-db/src/index/utils.rs index b057abde9..f69c33007 100644 --- a/magicblock-accounts-db/src/index/lmdb_utils.rs +++ b/magicblock-accounts-db/src/index/utils.rs @@ -10,23 +10,10 @@ use lmdb::{Environment, EnvironmentFlags}; pub(super) const MDB_SET_RANGE_OP: u32 = 17; #[doc = "Position at specified key"] pub(super) const MDB_SET_OP: u32 = 15; -#[doc = "Position at first key/data item"] -pub(super) const MDB_FIRST_OP: u32 = 0; -#[doc = "Position at next data item"] -pub(super) const MDB_NEXT_OP: u32 = 8; -#[doc = "Position at next data item of current key. Only for #MDB_DUPSORT"] -pub(super) const MDB_NEXT_DUP_OP: u32 = 9; -#[doc = "Return key/data at current cursor position"] -pub(super) const MDB_GET_CURRENT_OP: u32 = 4; -#[doc = "Position at key/data pair. Only for #MDB_DUPSORT"] -pub(super) const MDB_GET_BOTH_OP: u32 = 2; -pub(super) fn lmdb_env( - name: &str, - dir: &Path, - size: usize, - maxdb: u32, -) -> lmdb::Result { +const TABLES_COUNT: u32 = 4; + +pub(super) fn lmdb_env(dir: &Path, size: usize) -> lmdb::Result { let lmdb_env_flags: EnvironmentFlags = // allows to manually trigger flush syncs, but OS initiated flushes are somewhat beyond our control EnvironmentFlags::NO_SYNC @@ -34,29 +21,15 @@ pub(super) fn lmdb_env( // directly, saves CPU cycles and memory access | EnvironmentFlags::WRITE_MAP // we never read uninit memory, so there's no point in paying for meminit - | EnvironmentFlags::NO_MEM_INIT; + | EnvironmentFlags::NO_MEM_INIT + // accounts' access is pretty much random, so read ahead might be doing unecessary work + | EnvironmentFlags::NO_READAHEAD; - let path = dir.join(name); + let path = dir.join("index"); let _ = fs::create_dir_all(&path); Environment::new() .set_map_size(size) - .set_max_dbs(maxdb) + .set_max_dbs(TABLES_COUNT) .set_flags(lmdb_env_flags) .open_with_permissions(&path, 0o644) } - -/// Utility type to enforce big endian representation of u32. This is useful when u32 -/// is used as a key in lmdb and we need an ascending ordering on byte representation -pub(super) struct BigEndianU32([u8; 4]); - -impl BigEndianU32 { - pub(super) fn new(val: u32) -> Self { - Self(val.to_be_bytes()) - } -} - -impl AsRef<[u8]> for BigEndianU32 { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} diff --git a/magicblock-accounts-db/src/lib.rs b/magicblock-accounts-db/src/lib.rs index 7fa963daa..7ad635c8c 100644 --- a/magicblock-accounts-db/src/lib.rs +++ b/magicblock-accounts-db/src/lib.rs @@ -48,7 +48,7 @@ impl AccountsDb { ))?; let storage = AccountsStorage::new(config, &directory) .inspect_err(log_err!("storage creation"))?; - let index = AccountsDbIndex::new(config, &directory) + let index = AccountsDbIndex::new(config.index_map_size, &directory) .inspect_err(log_err!("index creation"))?; let snapshot_engine = SnapshotEngine::new(directory, config.max_snapshots as usize) @@ -127,9 +127,9 @@ impl AccountsDb { // https://github.com/magicblock-labs/magicblock-validator/issues/327 let allocation = match self.index.try_recycle_allocation(blocks) { - // if we could recycle some "hole" in database, use it + // if we could recycle some "hole" in the database, use it Ok(recycled) => { - // bookkeeping for deallocated(free hole) space + // bookkeeping for the deallocated (free hole) space self.storage.decrement_deallocations(recycled.blocks); self.storage.recycle(recycled) } diff --git a/magicblock-accounts-db/src/tests.rs b/magicblock-accounts-db/src/tests.rs index 2ab7bf6c1..ed40da814 100644 --- a/magicblock-accounts-db/src/tests.rs +++ b/magicblock-accounts-db/src/tests.rs @@ -424,7 +424,7 @@ fn test_owner_change() { let result = tenv.account_matches_owners(&acc.pubkey, &[OWNER]); assert!(matches!(result, Err(AccountsDbError::NotFound))); let result = tenv.get_program_accounts(&OWNER, |_| true); - assert!(matches!(result, Err(AccountsDbError::NotFound))); + assert!(result.map(|pks| pks.is_empty()).unwrap_or_default()); let result = tenv.account_matches_owners(&acc.pubkey, &[OWNER, new_owner]); assert!(matches!(result, Ok(1)));