use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use bytesize::ByteSize;
use heed::{CompactionOption, Env, EnvOpenOptions, WithoutTls};
use super::lmdb_error::LmdbLayerError;
pub type LmdbEnv = Env<WithoutTls>;
const DEFAULT_MAX_READERS: u32 = 1024;
#[allow(dead_code)]
const LMDB_DATA_FILE: &str = "data.mdb";
#[derive(Debug, Clone)]
pub struct LmdbEnvConfig {
pub map_size: ByteSize,
pub max_dbs: u32,
pub max_readers: u32,
}
impl LmdbEnvConfig {
pub fn new(max_dbs: u32, map_size: ByteSize) -> Self {
LmdbEnvConfig {
map_size,
max_dbs,
max_readers: DEFAULT_MAX_READERS,
}
}
}
fn build_options(config: &LmdbEnvConfig) -> Result<EnvOpenOptions<WithoutTls>, LmdbLayerError> {
let map_size = usize::try_from(config.map_size.as_u64())
.map_err(|_| LmdbLayerError::MapSizeUnrepresentable(config.map_size))?;
let aligned = map_size
.checked_next_multiple_of(page_size::get())
.ok_or(LmdbLayerError::MapSizeUnrepresentable(config.map_size))?;
let mut options = EnvOpenOptions::new().read_txn_without_tls();
options.map_size(aligned);
options.max_dbs(config.max_dbs);
options.max_readers(config.max_readers);
Ok(options)
}
pub(in crate::lmdb) fn open_lmdb_env(
dir: &Path,
config: &LmdbEnvConfig,
) -> Result<LmdbEnv, LmdbLayerError> {
create_dir_all(dir).map_err(|source| LmdbLayerError::CreateDir {
path: dir.to_path_buf(),
source,
})?;
let options = build_options(config)?;
let lmdb_env = unsafe { options.open(dir) }.map_err(|source| LmdbLayerError::Open {
path: dir.to_path_buf(),
source,
})?;
Ok(lmdb_env)
}
#[allow(dead_code)]
pub(crate) fn copy_lmdb_env_to_dir(
lmdb_env: &LmdbEnv,
dst_dir: &Path,
) -> Result<PathBuf, LmdbLayerError> {
create_dir_all(dst_dir).map_err(|source| LmdbLayerError::CreateDir {
path: dst_dir.to_path_buf(),
source,
})?;
let dst = dst_dir.join(LMDB_DATA_FILE);
lmdb_env
.copy_to_path(&dst, CompactionOption::Disabled)
.map_err(|source| LmdbLayerError::Copy {
dst: dst.clone(),
source,
})?;
Ok(dst)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lmdb::lmdb_db::LmdbDb;
use crate::lmdb::txn::with_write_txn;
#[test]
fn open_lmdb_env_rounds_unaligned_map_size_up_to_page_size() {
let dir = tempfile::tempdir().expect("create temp dir");
let config = LmdbEnvConfig::new(1, ByteSize::kb(100));
open_lmdb_env(dir.path(), &config).expect("open env with unaligned map size");
}
#[test]
fn exhausting_the_fixed_map_is_detectable_as_map_full() {
let dir = tempfile::tempdir().expect("create temp dir");
let config = LmdbEnvConfig::new(1, ByteSize::kib(256));
let lmdb_env = open_lmdb_env(dir.path(), &config).expect("open env");
let db =
with_write_txn(&lmdb_env, |txn| LmdbDb::open(&lmdb_env, txn, "data")).expect("open db");
let result: Result<(), LmdbLayerError> = with_write_txn(&lmdb_env, |txn| {
let value = vec![0_u8; 32 * 1024];
for i in 0..10_000_u32 {
db.put(txn, &i.to_be_bytes(), &value)?;
}
Ok(())
});
let err = result.expect_err("a 256 KiB map should fill within a few 32 KiB writes");
assert!(err.is_map_full(), "expected map-full, got: {err}");
let translated = LmdbLayerError::MapFull {
capacity: config.map_size,
};
assert!(
matches!(translated, LmdbLayerError::MapFull { capacity } if capacity == config.map_size)
);
}
}