use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use bytesize::ByteSize;
use heed::{RoTxn, RwTxn, WithoutTls};
use super::env_registry::open_shared_env;
use super::lmdb_db::{LmdbDb, open_db};
use super::lmdb_env::{LmdbEnv, copy_lmdb_env_to_dir};
use super::lmdb_error::LmdbLayerError;
use super::txn::{with_read_txn, with_write_txn};
pub(crate) struct LmdbSlot {
env_dir: PathBuf,
handles: OnceLock<LmdbHandles>,
}
struct LmdbHandles {
env: Arc<LmdbEnv>,
db: LmdbDb,
}
impl LmdbSlot {
pub(crate) fn new(env_dir: PathBuf) -> Self {
LmdbSlot {
env_dir,
handles: OnceLock::new(),
}
}
}
impl fmt::Debug for LmdbSlot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LmdbSlot")
.field("env_dir", &self.env_dir)
.finish_non_exhaustive()
}
}
fn opened<S: LmdbStore + ?Sized>(store: &S) -> Result<&LmdbHandles, LmdbLayerError> {
let slot = store.lmdb_slot();
if let Some(handles) = slot.handles.get() {
return Ok(handles);
}
let env = open_shared_env(&slot.env_dir, S::LMDB_MAP_SIZE)?;
let db = open_db(&env, S::LMDB_DB_NAME)?;
Ok(slot.handles.get_or_init(|| LmdbHandles { env, db }))
}
pub(crate) trait LmdbStore {
const LMDB_MAP_SIZE: ByteSize;
const LMDB_DB_NAME: &'static str;
fn lmdb_slot(&self) -> &LmdbSlot;
fn read<R, E>(
&self,
f: impl FnOnce(&LmdbDb, &RoTxn<'_, WithoutTls>) -> Result<R, E>,
) -> Result<R, E>
where
E: From<LmdbLayerError>,
{
let handles = opened(self)?;
with_read_txn(&handles.env, |txn| f(&handles.db, txn))
}
fn write<R, E>(&self, f: impl FnOnce(&LmdbDb, &mut RwTxn<'_>) -> Result<R, E>) -> Result<R, E>
where
E: From<LmdbLayerError>,
{
let handles = opened(self)?;
with_write_txn(&handles.env, |txn| f(&handles.db, txn))
}
fn snapshot_to(&self, dst_dir: &Path) -> Result<PathBuf, LmdbLayerError> {
let handles = opened(self)?;
copy_lmdb_env_to_dir(&handles.env, dst_dir)
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
use crate::lmdb::lmdb_env::open_lmdb_env;
const TEST_MAP_SIZE: ByteSize = ByteSize::mib(16);
struct TestStore {
lmdb: LmdbSlot,
}
impl LmdbStore for TestStore {
const LMDB_MAP_SIZE: ByteSize = TEST_MAP_SIZE;
const LMDB_DB_NAME: &'static str = "data";
fn lmdb_slot(&self) -> &LmdbSlot {
&self.lmdb
}
}
fn test_store() -> (TempDir, TestStore) {
let dir = tempfile::tempdir().expect("create temp dir");
let store = TestStore {
lmdb: LmdbSlot::new(dir.path().join("env")),
};
(dir, store)
}
#[test]
fn read_and_write_go_through_the_brackets() {
let (_dir, store) = test_store();
assert!(
!store.lmdb.env_dir.exists(),
"a store that has not been read or written opens no env"
);
store
.write(|db, txn| db.put(txn, b"key", b"value"))
.expect("write");
let value = store
.read(|db, txn| db.get(txn, b"key"))
.expect("read")
.expect("value present");
assert_eq!(value.as_ref(), b"value");
}
#[test]
fn snapshot_to_copies_committed_state() {
let (_dir, store) = test_store();
store
.write(|db, txn| db.put(txn, b"key", b"value"))
.expect("write");
let dst = tempfile::tempdir().expect("create temp dir");
let data_file = store.snapshot_to(dst.path()).expect("snapshot");
assert!(data_file.exists());
let copied = open_lmdb_env(dst.path(), TEST_MAP_SIZE).expect("open snapshot");
let copied_db =
with_write_txn(&copied, |txn| LmdbDb::open(&copied, txn, "data")).expect("open db");
let value = with_read_txn(&copied, |txn| copied_db.get(txn, b"key"))
.expect("read")
.expect("value present");
assert_eq!(value.as_ref(), b"value");
}
}