Skip to content

Commit 3fef41f

Browse files
authored
Cleanup and refactor accountsdb index (#419)
Main changes: 1. Use a single environment for all the accountsdb indexes 2. Introduce table abstraction to make the code more clean 3. Disable read ahead in the lmdb env configuration, which was unnecessary due to the mostly random access nature of operations. Due to the use of a single environment instead of previously used 3, this is a breaking change, which requires running a migration script to fix up the index directory. <!-- greptile_comment --> ## Greptile Summary Major refactoring of the AccountsDB index system to improve performance and code organization by consolidating database environments and optimizing LMDB operations. - Consolidated three separate LMDB environments into a single environment for all indices in `magicblock-accounts-db/src/index.rs` - Added new `Table` abstraction in `magicblock-accounts-db/src/index/table.rs` to encapsulate database operations cleanly - Disabled LMDB read-ahead for better performance with random access patterns - Changed program account retrieval to return empty vector instead of NotFound error when no accounts exist - **Breaking Change**: Requires migration script to fix index directory structure due to environment consolidation <!-- /greptile_comment -->
1 parent f3c906b commit 3fef41f

File tree

8 files changed

+260
-368
lines changed

8 files changed

+260
-368
lines changed

magicblock-accounts-db/src/index.rs

Lines changed: 117 additions & 155 deletions
Large diffs are not rendered by default.

magicblock-accounts-db/src/index/iterator.rs

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,59 @@
1-
use lmdb::{Cursor, Database, RoCursor, RoTransaction, Transaction};
1+
use lmdb::{Cursor, RoCursor, RoTransaction};
22
use log::error;
33
use solana_pubkey::Pubkey;
44

5-
use super::lmdb_utils::MDB_GET_CURRENT_OP;
5+
use super::{table::Table, MDB_SET_OP};
66
use crate::AdbResult;
77

88
/// Iterator over pubkeys and offsets, where accounts
99
/// for those pubkeys can be found in database
1010
///
1111
/// S: Starting position operation, determines where to place cursor initially
1212
/// N: Next position operation, determines where to move cursor next
13-
pub(crate) struct OffsetPubkeyIter<'env, const S: u32, const N: u32> {
14-
cursor: RoCursor<'env>,
15-
terminated: bool,
13+
pub(crate) struct OffsetPubkeyIter<'env> {
14+
iter: lmdb::Iter<'env>,
15+
_cursor: RoCursor<'env>,
1616
_txn: RoTransaction<'env>,
1717
}
1818

19-
impl<'a, const S: u32, const N: u32> OffsetPubkeyIter<'a, S, N> {
19+
impl<'env> OffsetPubkeyIter<'env> {
2020
pub(super) fn new(
21-
db: Database,
22-
txn: RoTransaction<'a>,
21+
table: &Table,
22+
txn: RoTransaction<'env>,
2323
pubkey: Option<&Pubkey>,
2424
) -> AdbResult<Self> {
25-
let cursor = txn.open_ro_cursor(db)?;
25+
let cursor = table.cursor_ro(&txn)?;
2626
// SAFETY:
2727
// nasty/neat trick for lifetime erasure, but we are upholding
28-
// the rust's ownership contracts by keeping txn around as well
29-
let cursor: RoCursor = unsafe { std::mem::transmute(cursor) };
30-
// jump to the first entry, key might be ignored depending on OP
31-
cursor.get(pubkey.map(AsRef::as_ref), None, S)?;
28+
// the rust's ownership contracts by keeping txn around as well
29+
let mut cursor: RoCursor = unsafe { std::mem::transmute(cursor) };
30+
let iter = if let Some(pubkey) = pubkey {
31+
// NOTE: there's a bug in the LMDB, which ignores NotFound error when
32+
// iterating on DUPSORT databases, where it just jumps to the next key,
33+
// here we check for the error explicitly to prevent this behavior
34+
if let Err(lmdb::Error::NotFound) =
35+
cursor.get(Some(pubkey.as_ref()), None, MDB_SET_OP)
36+
{
37+
lmdb::Iter::Err(lmdb::Error::NotFound)
38+
} else {
39+
cursor.iter_dup_of(pubkey)
40+
}
41+
} else {
42+
cursor.iter_start()
43+
};
3244
Ok(Self {
3345
_txn: txn,
34-
cursor,
35-
terminated: false,
46+
_cursor: cursor,
47+
iter,
3648
})
3749
}
3850
}
3951

40-
impl<const S: u32, const N: u32> Iterator for OffsetPubkeyIter<'_, S, N> {
52+
impl Iterator for OffsetPubkeyIter<'_> {
4153
type Item = (u32, Pubkey);
4254
fn next(&mut self) -> Option<Self::Item> {
43-
if self.terminated {
44-
return None;
45-
}
46-
47-
match self.cursor.get(None, None, MDB_GET_CURRENT_OP) {
48-
Ok(entry) => {
49-
// advance the cursor,
50-
let advance = self.cursor.get(None, None, N);
51-
// if we move past the iterable range, NotFound will be
52-
// triggered by OP, and we can terminate the iteration
53-
if let Err(lmdb::Error::NotFound) = advance {
54-
self.terminated = true;
55-
}
56-
Some(bytes!(#unpack, entry.1, u32, Pubkey))
57-
}
55+
match self.iter.next()? {
56+
Ok(entry) => Some(bytes!(#unpack, entry.1, u32, Pubkey)),
5857
Err(error) => {
5958
error!("error advancing offset iterator cursor: {error}");
6059
None

magicblock-accounts-db/src/index/standalone.rs

Lines changed: 0 additions & 138 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use lmdb::{
2+
Database, DatabaseFlags, Environment, RoCursor, RwCursor, RwTransaction,
3+
Transaction, WriteFlags,
4+
};
5+
6+
use super::WEMPTY;
7+
8+
pub(super) struct Table {
9+
db: Database,
10+
}
11+
12+
impl Table {
13+
pub(super) fn new(
14+
env: &Environment,
15+
name: &str,
16+
flags: DatabaseFlags,
17+
) -> lmdb::Result<Self> {
18+
let db = env.create_db(Some(name), flags)?;
19+
Ok(Self { db })
20+
}
21+
22+
#[inline]
23+
pub(super) fn get<'txn, T: Transaction, K: AsRef<[u8]>>(
24+
&self,
25+
txn: &'txn T,
26+
key: K,
27+
) -> lmdb::Result<Option<&'txn [u8]>> {
28+
match txn.get(self.db, &key) {
29+
Ok(bytes) => Ok(Some(bytes)),
30+
Err(lmdb::Error::NotFound) => Ok(None),
31+
Err(e) => Err(e),
32+
}
33+
}
34+
35+
#[inline]
36+
pub(super) fn put<K: AsRef<[u8]>, V: AsRef<[u8]>>(
37+
&self,
38+
txn: &mut RwTransaction,
39+
key: K,
40+
value: V,
41+
) -> lmdb::Result<()> {
42+
txn.put(self.db, &key, &value, WEMPTY)
43+
}
44+
45+
#[inline]
46+
pub(super) fn del<K: AsRef<[u8]>>(
47+
&self,
48+
txn: &mut RwTransaction,
49+
key: K,
50+
value: Option<&[u8]>,
51+
) -> lmdb::Result<()> {
52+
match txn.del(self.db, &key, value) {
53+
Ok(_) | Err(lmdb::Error::NotFound) => Ok(()),
54+
Err(e) => Err(e),
55+
}
56+
}
57+
58+
#[inline]
59+
pub(super) fn put_if_not_exists<K: AsRef<[u8]>, V: AsRef<[u8]>>(
60+
&self,
61+
txn: &mut RwTransaction,
62+
key: K,
63+
value: V,
64+
) -> lmdb::Result<bool> {
65+
let result = txn.put(self.db, &key, &value, WriteFlags::NO_OVERWRITE);
66+
match result {
67+
Ok(_) => Ok(true),
68+
Err(lmdb::Error::KeyExist) => Ok(false),
69+
Err(err) => Err(err),
70+
}
71+
}
72+
73+
#[inline]
74+
pub(super) fn cursor_ro<'txn, T: Transaction>(
75+
&self,
76+
txn: &'txn T,
77+
) -> lmdb::Result<RoCursor<'txn>> {
78+
txn.open_ro_cursor(self.db)
79+
}
80+
81+
#[inline]
82+
pub(super) fn cursor_rw<'txn>(
83+
&self,
84+
txn: &'txn mut RwTransaction,
85+
) -> lmdb::Result<RwCursor<'txn>> {
86+
txn.open_rw_cursor(self.db)
87+
}
88+
89+
pub(super) fn entries<T: Transaction>(&self, txn: &T) -> usize {
90+
txn.stat(self.db).map(|s| s.entries()).unwrap_or_default()
91+
}
92+
}

magicblock-accounts-db/src/index/tests.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ fn test_ensure_correct_owner() {
171171
);
172172
let result = tenv.get_program_accounts_iter(&owner);
173173
assert!(
174-
matches!(result, Err(AccountsDbError::NotFound)),
174+
matches!(result.map(|i| i.count()), Ok(0)),
175175
"programs index still has record of account after owner change"
176176
);
177177

@@ -201,14 +201,18 @@ fn test_program_index_cleanup() {
201201
.env
202202
.begin_rw_txn()
203203
.expect("failed to start new RW transaction");
204-
let result =
205-
tenv.remove_programs_index_entry(&pubkey, &mut txn, allocation.offset);
204+
let result = tenv.remove_programs_index_entry(
205+
&pubkey,
206+
None,
207+
&mut txn,
208+
allocation.offset,
209+
);
206210
assert!(result.is_ok(), "failed to remove entry from programs index");
207211
txn.commit().expect("failed to commit transaction");
208212

209213
let result = tenv.get_program_accounts_iter(&owner);
210214
assert!(
211-
matches!(result, Err(AccountsDbError::NotFound)),
215+
matches!(result.map(|i| i.count()), Ok(0)),
212216
"programs index still has record of account after cleanup"
213217
);
214218
}
@@ -339,7 +343,7 @@ fn setup() -> IndexTestEnv {
339343
let directory = tempfile::tempdir()
340344
.expect("failed to create temp directory for index tests")
341345
.keep();
342-
let index = AccountsDbIndex::new(&config, &directory)
346+
let index = AccountsDbIndex::new(config.index_map_size, &directory)
343347
.expect("failed to create accountsdb index in temp dir");
344348
IndexTestEnv { index, directory }
345349
}

0 commit comments

Comments
 (0)