use std::{fmt, write};
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::common::{
ExportRecord, IncrError, KeyMeta, SearchPage, SearchQuery, VacuumStats,
};
#[derive(Debug)]
pub enum EngineError {
UnknownEngine(String),
Unsupported {
engine: &'static str,
op: &'static str,
},
PathRequired(&'static str),
Io(std::io::Error),
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EngineError::UnknownEngine(name) => write!(
f,
"unknown engine {name:?}; expected one of {}",
ENGINE_NAMES.join(" / ")
),
EngineError::Unsupported { engine, op } => {
write!(f, "engine {engine} does not support {op}")
}
EngineError::PathRequired(engine) => {
write!(f, "engine {engine} requires a database path")
}
EngineError::Io(e) => write!(f, "io error: {e}"),
}
}
}
impl std::error::Error for EngineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
EngineError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for EngineError {
fn from(e: std::io::Error) -> Self {
EngineError::Io(e)
}
}
pub type Result<T> = std::result::Result<T, EngineError>;
pub const ENGINE_NAMES: &[&str] = &["bitcask", "bplus_tree", "lsm_tree", "memory"];
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub order: usize,
pub max_cache_pages: usize,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
order: 64,
max_cache_pages: 1024,
}
}
}
pub trait KvTxn {
fn version(&self) -> u64;
fn set(&self, key: &[u8], value: Vec<u8>) -> bool;
fn set_with_ttl(&self, key: &[u8], value: Vec<u8>, ttl: Duration) -> bool;
fn delete(&self, key: &[u8]) -> bool;
fn get(&self, key: &[u8]) -> Option<Vec<u8>>;
fn exists(&self, key: &[u8]) -> bool;
fn get_meta(&self, key: &[u8]) -> Option<KeyMeta>;
fn batch_set(&self, items: &[(Vec<u8>, Vec<u8>)]) -> std::result::Result<(), Vec<u8>>;
fn batch_delete(&self, keys: &[Vec<u8>]) -> std::result::Result<(), Vec<u8>>;
fn multi_get(&self, keys: &[Vec<u8>]) -> Vec<Option<Vec<u8>>>;
fn incr(&self, key: &[u8], delta: i64) -> std::result::Result<i64, IncrError>;
fn decr(&self, key: &[u8], delta: i64) -> std::result::Result<i64, IncrError>;
fn scan(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Vec<ExportRecord>;
fn reverse_scan(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Vec<ExportRecord>;
fn prefix_scan(&self, prefix: &[u8]) -> Vec<ExportRecord>;
fn seek(&self, key: &[u8]) -> Option<ExportRecord>;
fn seek_prev(&self, key: &[u8]) -> Option<ExportRecord>;
fn search_keys(&self, query: &SearchQuery) -> SearchPage;
fn key_count(&self, prefix: &[u8]) -> usize;
fn get_ttl(&self, key: &[u8]) -> Option<Option<Duration>>;
fn refresh_ttl(&self, key: &[u8], ttl: Duration) -> bool;
fn persist_key(&self, key: &[u8]) -> bool;
fn purge_expired(&self) -> usize;
fn export_latest_visible(&self, include_deleted: bool) -> Vec<ExportRecord>;
fn commit(&self);
fn rollback(&self);
}
pub trait KvBulk {
fn put(&mut self, key: &[u8], value: Vec<u8>);
fn put_batch(&mut self, items: &[(Vec<u8>, Vec<u8>)]);
fn put_batch_owned(&mut self, items: Vec<(Vec<u8>, Vec<u8>)>);
fn delete(&mut self, key: &[u8]);
fn get(&self, key: &[u8]) -> Option<Vec<u8>>;
fn finish(self: Box<Self>);
}
pub trait KvEngine {
fn name(&self) -> &'static str;
fn path(&self) -> Option<&Path>;
fn begin(&self) -> Box<dyn KvTxn + '_>;
fn begin_bulk(&self) -> Box<dyn KvBulk + '_>;
fn export_latest_visible(&self, include_deleted: bool) -> Vec<ExportRecord>;
fn vacuum(&self) -> Result<VacuumStats>;
fn flush(&self) -> Result<()>;
fn checkpoint(&self) -> Result<()>;
fn merge(&self) -> Result<()> {
Err(EngineError::Unsupported {
engine: self.name(),
op: "merge",
})
}
}
pub fn open_engine(name: &str, path: Option<&Path>) -> Result<Box<dyn KvEngine>> {
open_engine_with(name, path, &EngineConfig::default())
}
pub fn open_engine_with(
name: &str,
path: Option<&Path>,
config: &EngineConfig,
) -> Result<Box<dyn KvEngine>> {
let canon = canonical_name(name)
.ok_or_else(|| EngineError::UnknownEngine(name.to_string()))?;
let disk_path = |engine: &'static str| -> Result<PathBuf> {
path.map(|p| p.to_path_buf())
.ok_or(EngineError::PathRequired(engine))
};
Ok(match canon {
"bitcask" => Box::new(BitcaskEngine {
inner: crate::bitcask::MVCC::try_open(disk_path("bitcask")?)?,
}),
"bplus_tree" => Box::new(BPlusTreeEngine {
inner: crate::bplus_tree::MVCC::try_open(
disk_path("bplus_tree")?,
config.order,
config.max_cache_pages,
)?,
}),
"lsm_tree" => Box::new(LsmTreeEngine {
inner: crate::lsm_tree::MVCC::try_open(disk_path("lsm_tree")?)?,
}),
"memory" => Box::new(MemoryEngine {
inner: crate::memory::MVCC::new(),
}),
_ => unreachable!("canonical_name 已校验"),
})
}
fn canonical_name(name: &str) -> Option<&'static str> {
match name.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"bitcask" => Some("bitcask"),
"bplus_tree" | "bplus" | "b_plus_tree" | "btree" | "b_tree" => Some("bplus_tree"),
"lsm_tree" | "lsm" => Some("lsm_tree"),
"memory" | "mem" | "in_memory" => Some("memory"),
_ => None,
}
}
macro_rules! impl_engine {
(
engine_struct: $engine:ident,
txn_struct: $txn:ident,
bulk_struct: $bulk:ident,
mvcc: $mvcc:ty,
txn_inner: $txn_inner:ty,
bulk_inner: $bulk_inner:ty,
name: $name:literal,
path: $path:tt,
vacuum: $vacuum:tt,
durable: $durable:tt,
merge: $merge:tt,
) => {
struct $engine {
inner: $mvcc,
}
struct $txn<'a> {
inner: $txn_inner,
_engine: std::marker::PhantomData<&'a ()>,
}
struct $bulk<'a> {
inner: $bulk_inner,
_engine: std::marker::PhantomData<&'a ()>,
}
impl KvEngine for $engine {
fn name(&self) -> &'static str {
$name
}
impl_engine!(@path self, $path);
fn begin(&self) -> Box<dyn KvTxn + '_> {
Box::new($txn {
inner: self.inner.begin_transaction(),
_engine: std::marker::PhantomData,
})
}
fn begin_bulk(&self) -> Box<dyn KvBulk + '_> {
Box::new($bulk {
inner: self.inner.begin_bulk(),
_engine: std::marker::PhantomData,
})
}
fn export_latest_visible(&self, include_deleted: bool) -> Vec<ExportRecord> {
self.inner.export_latest_visible(include_deleted)
}
impl_engine!(@vacuum $vacuum);
impl_engine!(@durable $durable);
impl_engine!(@merge $merge);
}
impl<'a> KvTxn for $txn<'a> {
fn version(&self) -> u64 {
self.inner.version()
}
fn set(&self, key: &[u8], value: Vec<u8>) -> bool {
self.inner.set(key, value)
}
fn set_with_ttl(&self, key: &[u8], value: Vec<u8>, ttl: Duration) -> bool {
self.inner.set_with_ttl(key, value, ttl)
}
fn delete(&self, key: &[u8]) -> bool {
self.inner.delete(key)
}
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
self.inner.get(key)
}
fn exists(&self, key: &[u8]) -> bool {
self.inner.exists(key)
}
fn get_meta(&self, key: &[u8]) -> Option<KeyMeta> {
self.inner.get_meta(key)
}
fn batch_set(
&self,
items: &[(Vec<u8>, Vec<u8>)],
) -> std::result::Result<(), Vec<u8>> {
self.inner.batch_set(items)
}
fn batch_delete(&self, keys: &[Vec<u8>]) -> std::result::Result<(), Vec<u8>> {
self.inner.batch_delete(keys)
}
fn multi_get(&self, keys: &[Vec<u8>]) -> Vec<Option<Vec<u8>>> {
self.inner.multi_get(keys)
}
fn incr(&self, key: &[u8], delta: i64) -> std::result::Result<i64, IncrError> {
self.inner.incr(key, delta)
}
fn decr(&self, key: &[u8], delta: i64) -> std::result::Result<i64, IncrError> {
self.inner.decr(key, delta)
}
fn scan(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Vec<ExportRecord> {
self.inner.scan(start, end)
}
fn reverse_scan(
&self,
start: Option<&[u8]>,
end: Option<&[u8]>,
) -> Vec<ExportRecord> {
self.inner.reverse_scan(start, end)
}
fn prefix_scan(&self, prefix: &[u8]) -> Vec<ExportRecord> {
self.inner.prefix_scan(prefix)
}
fn seek(&self, key: &[u8]) -> Option<ExportRecord> {
self.inner.seek(key)
}
fn seek_prev(&self, key: &[u8]) -> Option<ExportRecord> {
self.inner.seek_prev(key)
}
fn search_keys(&self, query: &SearchQuery) -> SearchPage {
self.inner.search_keys(query)
}
fn key_count(&self, prefix: &[u8]) -> usize {
self.inner.key_count(prefix)
}
fn get_ttl(&self, key: &[u8]) -> Option<Option<Duration>> {
self.inner.get_ttl(key)
}
fn refresh_ttl(&self, key: &[u8], ttl: Duration) -> bool {
self.inner.refresh_ttl(key, ttl)
}
fn persist_key(&self, key: &[u8]) -> bool {
self.inner.persist(key)
}
fn purge_expired(&self) -> usize {
self.inner.purge_expired()
}
fn export_latest_visible(&self, include_deleted: bool) -> Vec<ExportRecord> {
self.inner.export_latest_visible(include_deleted)
}
fn commit(&self) {
self.inner.commit()
}
fn rollback(&self) {
self.inner.rollback()
}
}
impl<'a> KvBulk for $bulk<'a> {
fn put(&mut self, key: &[u8], value: Vec<u8>) {
self.inner.put(key, value)
}
fn put_batch(&mut self, items: &[(Vec<u8>, Vec<u8>)]) {
self.inner.put_batch(items)
}
fn put_batch_owned(&mut self, items: Vec<(Vec<u8>, Vec<u8>)>) {
self.inner.put_batch_owned(items)
}
fn delete(&mut self, key: &[u8]) {
self.inner.delete(key)
}
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
self.inner.get(key)
}
fn finish(self: Box<Self>) {
(*self).inner.finish()
}
}
};
(@path $self:ident, none) => {
fn path(&self) -> Option<&Path> {
None
}
};
(@path $self:ident, $method:ident) => {
fn path(&self) -> Option<&Path> {
Some(self.inner.$method())
}
};
(@vacuum io) => {
fn vacuum(&self) -> Result<VacuumStats> {
Ok(self.inner.vacuum()?)
}
};
(@vacuum plain) => {
fn vacuum(&self) -> Result<VacuumStats> {
Ok(self.inner.vacuum())
}
};
(@durable yes) => {
fn flush(&self) -> Result<()> {
self.inner.flush();
Ok(())
}
fn checkpoint(&self) -> Result<()> {
self.inner.checkpoint();
Ok(())
}
};
(@durable no) => {
fn flush(&self) -> Result<()> {
Ok(())
}
fn checkpoint(&self) -> Result<()> {
Ok(())
}
};
(@merge yes) => {
fn merge(&self) -> Result<()> {
self.inner.merge()?;
Ok(())
}
};
(@merge no) => {};
}
impl_engine! {
engine_struct: BitcaskEngine,
txn_struct: BitcaskTxn,
bulk_struct: BitcaskBulk,
mvcc: crate::bitcask::bitcask_mvcc::MVCC,
txn_inner: crate::bitcask::bitcask_mvcc::Transaction,
bulk_inner: crate::bitcask::bitcask_mvcc::BulkLoader,
name: "bitcask",
path: db_dir,
vacuum: io,
durable: yes,
merge: yes,
}
impl_engine! {
engine_struct: BPlusTreeEngine,
txn_struct: BPlusTreeTxn,
bulk_struct: BPlusTreeBulk,
mvcc: crate::bplus_tree::bplus_tree_mvcc::MVCC,
txn_inner: crate::bplus_tree::bplus_tree_mvcc::Transaction,
bulk_inner: crate::bplus_tree::bplus_tree_mvcc::BulkLoader,
name: "bplus_tree",
path: db_path,
vacuum: io,
durable: yes,
merge: no,
}
impl_engine! {
engine_struct: LsmTreeEngine,
txn_struct: LsmTreeTxn,
bulk_struct: LsmTreeBulk,
mvcc: crate::lsm_tree::lsm_tree_mvcc::MVCC,
txn_inner: crate::lsm_tree::lsm_tree_mvcc::Transaction,
bulk_inner: crate::lsm_tree::lsm_tree_mvcc::BulkLoader,
name: "lsm_tree",
path: db_dir,
vacuum: io,
durable: yes,
merge: no,
}
impl_engine! {
engine_struct: MemoryEngine,
txn_struct: MemoryTxn,
bulk_struct: MemoryBulk,
mvcc: crate::memory::memory_mvcc::MVCC,
txn_inner: crate::memory::memory_mvcc::Transaction,
bulk_inner: crate::memory::memory_mvcc::BulkLoader,
name: "memory",
path: none,
vacuum: plain,
durable: no,
merge: no,
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_dir(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("se_engine_{tag}_{nanos}"));
let _ = std::fs::remove_dir_all(&dir);
dir
}
fn open_all() -> Vec<(&'static str, Box<dyn KvEngine>, Option<PathBuf>)> {
let mut out: Vec<(&'static str, Box<dyn KvEngine>, Option<PathBuf>)> = Vec::new();
for name in ["bitcask", "bplus_tree", "lsm_tree"] {
let dir = tmp_dir(name);
let db = open_engine(name, Some(&dir)).expect("open disk engine");
out.push((name, db, Some(dir)));
}
out.push(("memory", open_engine("memory", None).unwrap(), None));
out
}
fn cleanup(dirs: Vec<Option<PathBuf>>) {
for d in dirs.into_iter().flatten() {
let _ = std::fs::remove_dir_all(&d);
}
}
#[test]
fn canonical_name_accepts_aliases() {
assert_eq!(canonical_name("LSM"), Some("lsm_tree"));
assert_eq!(canonical_name("bplus-tree"), Some("bplus_tree"));
assert_eq!(canonical_name(" mem "), Some("memory"));
assert_eq!(canonical_name("btree"), Some("bplus_tree"));
assert_eq!(canonical_name("nope"), None);
}
fn expect_err(r: Result<Box<dyn KvEngine>>) -> EngineError {
match r {
Ok(_) => panic!("expected an error, got an engine"),
Err(e) => e,
}
}
#[test]
fn unknown_engine_is_reported() {
let err = expect_err(open_engine("redis", None));
assert!(matches!(err, EngineError::UnknownEngine(n) if n == "redis"));
}
#[test]
fn disk_engines_require_path() {
for name in ["bitcask", "bplus_tree", "lsm_tree"] {
let err = expect_err(open_engine(name, None));
assert!(
matches!(err, EngineError::PathRequired(_)),
"{name} should require a path"
);
}
}
#[test]
fn all_engines_roundtrip_set_get_delete() {
let engines = open_all();
let mut dirs = Vec::new();
for (name, db, dir) in engines {
assert_eq!(db.name(), name);
let tx = db.begin();
assert!(tx.set(b"a", b"1".to_vec()), "{name}: set");
assert_eq!(tx.get(b"a"), Some(b"1".to_vec()), "{name}: get");
assert!(tx.exists(b"a"), "{name}: exists");
assert!(tx.delete(b"a"), "{name}: delete");
assert_eq!(tx.get(b"a"), None, "{name}: get after delete");
tx.commit();
dirs.push(dir);
}
cleanup(dirs);
}
#[test]
fn all_engines_support_batch_and_scan() {
let engines = open_all();
let mut dirs = Vec::new();
for (name, db, dir) in engines {
let tx = db.begin();
tx.batch_set(&[
(b"k1".to_vec(), b"v1".to_vec()),
(b"k2".to_vec(), b"v2".to_vec()),
(b"k3".to_vec(), b"v3".to_vec()),
])
.expect("batch_set");
let got = tx.multi_get(&[b"k1".to_vec(), b"k9".to_vec()]);
assert_eq!(got, vec![Some(b"v1".to_vec()), None], "{name}: multi_get");
let scanned = tx.scan(Some(b"k1"), Some(b"k3"));
let keys: Vec<_> = scanned.iter().map(|r| r.key.clone()).collect();
assert_eq!(keys, vec![b"k1".to_vec(), b"k2".to_vec()], "{name}: scan");
let pre = tx.prefix_scan(b"k");
assert_eq!(pre.len(), 3, "{name}: prefix_scan");
tx.commit();
dirs.push(dir);
}
cleanup(dirs);
}
#[test]
fn all_engines_detect_write_conflict() {
let engines = open_all();
let mut dirs = Vec::new();
for (name, db, dir) in engines {
let t1 = db.begin();
let t2 = db.begin();
assert!(t1.set(b"k", b"1".to_vec()), "{name}: t1 set");
assert!(!t2.set(b"k", b"2".to_vec()), "{name}: t2 must conflict");
t1.commit();
t2.rollback();
dirs.push(dir);
}
cleanup(dirs);
}
#[test]
fn all_engines_bulk_load() {
let engines = open_all();
let mut dirs = Vec::new();
for (name, db, dir) in engines {
{
let mut bulk = db.begin_bulk();
bulk.put(b"a", b"1".to_vec());
bulk.put_batch(&[(b"b".to_vec(), b"2".to_vec())]);
bulk.put_batch_owned(vec![(b"c".to_vec(), b"3".to_vec())]);
bulk.finish();
}
let tx = db.begin();
assert_eq!(tx.get(b"a"), Some(b"1".to_vec()), "{name}: bulk a");
assert_eq!(tx.get(b"b"), Some(b"2".to_vec()), "{name}: bulk b");
assert_eq!(tx.get(b"c"), Some(b"3".to_vec()), "{name}: bulk c");
tx.commit();
dirs.push(dir);
}
cleanup(dirs);
}
#[test]
fn all_engines_ttl_and_meta() {
let engines = open_all();
let mut dirs = Vec::new();
for (name, db, dir) in engines {
let tx = db.begin();
assert!(tx.set_with_ttl(b"t", b"v".to_vec(), Duration::from_secs(60)));
let ttl = tx.get_ttl(b"t").expect("{name}: key exists");
assert!(ttl.is_some(), "{name}: should have a TTL");
assert!(tx.persist_key(b"t"), "{name}: persist");
assert_eq!(tx.get_ttl(b"t"), Some(None), "{name}: now permanent");
let meta = tx.get_meta(b"t").expect("{name}: meta");
assert_eq!(meta.key, b"t".to_vec());
assert!(!meta.deleted, "{name}: not deleted");
tx.commit();
dirs.push(dir);
}
cleanup(dirs);
}
#[test]
fn all_engines_vacuum_and_flush() {
let engines = open_all();
let mut dirs = Vec::new();
for (name, db, dir) in engines {
let t1 = db.begin();
t1.set(b"k", b"v1".to_vec());
t1.commit();
let t2 = db.begin();
t2.set(b"k", b"v2".to_vec());
t2.commit();
db.flush().unwrap_or_else(|e| panic!("{name}: flush: {e}"));
db.checkpoint()
.unwrap_or_else(|e| panic!("{name}: checkpoint: {e}"));
let stats = db.vacuum().unwrap_or_else(|e| panic!("{name}: vacuum: {e}"));
assert!(stats.xmin > 0, "{name}: xmin");
let tx = db.begin();
assert_eq!(tx.get(b"k"), Some(b"v2".to_vec()), "{name}: after vacuum");
tx.commit();
dirs.push(dir);
}
cleanup(dirs);
}
#[test]
fn memory_engine_has_no_path_and_no_merge() {
let db = open_engine("memory", None).unwrap();
assert!(db.path().is_none());
assert!(matches!(
db.merge(),
Err(EngineError::Unsupported { op: "merge", .. })
));
}
#[test]
fn disk_engines_expose_their_path() {
let dir = tmp_dir("path");
let db = open_engine("bitcask", Some(&dir)).unwrap();
assert_eq!(db.path(), Some(dir.as_path()));
drop(db);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn engine_is_object_safe_in_collections() {
let dirs: Vec<PathBuf> = vec![tmp_dir("obj1")];
let mut engines: Vec<Box<dyn KvEngine>> = Vec::new();
engines.push(open_engine("memory", None).unwrap());
engines.push(open_engine("lsm", Some(&dirs[0])).unwrap());
for db in &engines {
let tx = db.begin();
tx.set(b"x", b"y".to_vec());
tx.commit();
}
for db in &engines {
let tx = db.begin();
assert_eq!(tx.get(b"x"), Some(b"y".to_vec()), "{}", db.name());
tx.commit();
}
drop(engines);
cleanup(dirs.into_iter().map(Some).collect());
}
}