mod memtable;
mod sstable;
#[cfg(test)]
#[path = "lsm_tree_tests.rs"]
mod lsm_tree_tests;
#[cfg(test)]
#[path = "sample_app_tests.rs"]
mod sample_app_tests;
#[cfg(feature = "harness")]
pub mod recipe;
#[cfg(any(
feature = "wal",
feature = "tiered-compaction",
feature = "leveled-compaction",
feature = "snapshot",
feature = "lz4",
feature = "zstd",
feature = "block-cache-integration",
))]
pub mod features;
#[cfg(feature = "block-cache-integration")]
pub use features::block_cache_integration::{Block, BlockCache, BlockKey, LruBlockCache};
#[cfg(feature = "leveled-compaction")]
pub use features::leveled_compaction::{LeveledCompactionPlanner, LeveledManifest, LeveledRun};
#[cfg(feature = "lz4")]
pub use features::lz4::Lz4BlockCompressor;
#[cfg(feature = "snapshot")]
pub use features::snapshot::{Snapshot, SnapshotManager, SnapshotManifest};
#[cfg(feature = "tiered-compaction")]
pub use features::tiered_compaction::{TieredCompactionPlanner, TieredManifest, TieredRun};
#[cfg(feature = "wal")]
pub use features::wal::WriteAheadLog;
#[cfg(feature = "zstd")]
pub use features::zstd::ZstdBlockCompressor;
use std::collections::{BTreeMap, VecDeque};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use memtable::Memtable;
use sstable::SsTable;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BloomMode {
On,
Off,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FlushMode {
Background,
Sync,
}
const DEFAULT_MAX_IMMUTABLE: usize = 4;
struct Shared {
state: Mutex<State>,
signal: Condvar,
data_dir: PathBuf,
bloom_mode: BloomMode,
max_immutable: usize,
}
struct State {
immutable: VecDeque<Arc<Memtable>>,
sstables: Arc<Vec<Arc<SsTable>>>,
next_seq: u64,
shutdown: bool,
flush_err: Option<io::Error>,
}
pub struct LsmTree {
active: Memtable,
shared: Arc<Shared>,
flush_handle: Option<JoinHandle<()>>,
flush_threshold_bytes: usize,
flush_mode: FlushMode,
compaction_trigger: usize,
}
impl LsmTree {
pub fn open(data_dir: impl AsRef<Path>, flush_threshold_bytes: usize) -> io::Result<Self> {
Self::open_with(data_dir, flush_threshold_bytes, BloomMode::On)
}
pub fn open_with(
data_dir: impl AsRef<Path>,
flush_threshold_bytes: usize,
bloom_mode: BloomMode,
) -> io::Result<Self> {
let data_dir = data_dir.as_ref().to_path_buf();
fs::create_dir_all(&data_dir)?;
let mut files: Vec<PathBuf> = fs::read_dir(&data_dir)?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("sst-"))
.unwrap_or(false)
})
.collect();
files.sort();
let next_seq = files
.last()
.and_then(|p| p.file_stem().and_then(|s| s.to_str()))
.and_then(|stem| stem.strip_prefix("sst-"))
.and_then(|n| n.parse::<u64>().ok())
.map(|n| n + 1)
.unwrap_or(0);
let mut sstables = Vec::with_capacity(files.len());
for f in files {
sstables.push(Arc::new(SsTable::open(f)?));
}
let shared = Arc::new(Shared {
state: Mutex::new(State {
immutable: VecDeque::new(),
sstables: Arc::new(sstables),
next_seq,
shutdown: false,
flush_err: None,
}),
signal: Condvar::new(),
data_dir,
bloom_mode,
max_immutable: DEFAULT_MAX_IMMUTABLE,
});
Ok(Self {
active: Memtable::new(),
shared,
flush_handle: None,
flush_threshold_bytes,
flush_mode: FlushMode::Background,
compaction_trigger: 0,
})
}
pub fn set_flush_mode(&mut self, mode: FlushMode) -> &mut Self {
self.flush_mode = mode;
self
}
pub fn flush_mode(&self) -> FlushMode {
self.flush_mode
}
pub fn set_compaction_trigger(&mut self, trigger: usize) -> &mut Self {
self.compaction_trigger = trigger;
self
}
pub fn compaction_trigger(&self) -> usize {
self.compaction_trigger
}
pub fn compact(&mut self) -> io::Result<()> {
self.enqueue_active()?;
self.drain()?;
let ssts = {
let st = self.lock();
st.sstables.clone()
};
if ssts.len() < 2 {
return Ok(());
}
let mut merged: BTreeMap<String, Option<Vec<u8>>> = BTreeMap::new();
for sst in ssts.iter() {
for (key, value) in sst.entries() {
merged.insert(key, value);
}
}
let live: Vec<(String, Vec<u8>)> = merged
.into_iter()
.filter_map(|(k, v)| v.map(|val| (k, val)))
.collect();
let seq = self.reserve_seq();
let path = self.shared.data_dir.join(format!("sst-{seq:012}.dat"));
let new_sst = SsTable::write(
&path,
live.len(),
live.iter().map(|(k, v)| (k.as_str(), Some(v.as_slice()))),
)?;
let old_paths: Vec<PathBuf> = ssts.iter().map(|s| s.path().to_path_buf()).collect();
{
let mut st = self.lock();
st.sstables = Arc::new(vec![Arc::new(new_sst)]);
}
for p in old_paths {
let _ = fs::remove_file(p);
}
Ok(())
}
pub fn put(&mut self, key: &str, value: &[u8]) -> io::Result<()> {
self.active.put(key, Some(value.to_vec()));
self.maybe_rotate()
}
pub fn delete(&mut self, key: &str) -> io::Result<()> {
self.active.put(key, None);
self.maybe_rotate()
}
pub fn get(&self, key: &str) -> io::Result<Option<Vec<u8>>> {
if let Some(hit) = self.active.get(key) {
return Ok(hit.map(|v| v.to_vec()));
}
let (imms, ssts) = self.snapshot();
for m in &imms {
if let Some(hit) = m.get(key) {
return Ok(hit.map(|v| v.to_vec()));
}
}
let check_bloom = matches!(self.shared.bloom_mode, BloomMode::On);
for sst in ssts.iter().rev() {
if let Some(hit) = sst.get(key, check_bloom) {
return Ok(hit);
}
}
Ok(None)
}
pub fn range(&self, lo: Option<&str>, hi: Option<&str>) -> io::Result<Vec<(String, Vec<u8>)>> {
let mut merged: BTreeMap<String, Option<Vec<u8>>> = BTreeMap::new();
for (k, v) in self.active.range(lo, hi) {
merged
.entry(k.to_string())
.or_insert_with(|| v.map(|s| s.to_vec()));
}
let (imms, ssts) = self.snapshot();
for m in &imms {
for (k, v) in m.range(lo, hi) {
merged
.entry(k.to_string())
.or_insert_with(|| v.map(|s| s.to_vec()));
}
}
for sst in ssts.iter().rev() {
for (k, v) in sst.range(lo, hi) {
merged.entry(k).or_insert(v);
}
}
Ok(merged
.into_iter()
.filter_map(|(k, v)| v.map(|val| (k, val)))
.collect())
}
pub fn flush(&mut self) -> io::Result<()> {
self.enqueue_active()?;
self.drain()
}
pub fn sstable_count(&self) -> usize {
self.lock().sstables.len()
}
pub fn bloom_mode(&self) -> BloomMode {
self.shared.bloom_mode
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.shared.state.lock().unwrap_or_else(|e| e.into_inner())
}
fn snapshot(&self) -> (Vec<Arc<Memtable>>, Arc<Vec<Arc<SsTable>>>) {
let st = self.lock();
(
st.immutable.iter().rev().cloned().collect(),
Arc::clone(&st.sstables),
)
}
fn reserve_seq(&self) -> u64 {
let mut st = self.lock();
let seq = st.next_seq;
st.next_seq += 1;
seq
}
fn maybe_rotate(&mut self) -> io::Result<()> {
if self.active.approx_size_bytes() < self.flush_threshold_bytes {
return Ok(());
}
self.enqueue_active()?;
if self.compaction_trigger > 0 && self.sstable_count() >= self.compaction_trigger {
self.compact()?;
}
Ok(())
}
fn enqueue_active(&mut self) -> io::Result<()> {
if self.active.is_empty() {
return self.take_flush_err();
}
match self.flush_mode {
FlushMode::Sync => self.flush_inline(),
FlushMode::Background => {
self.ensure_worker();
let frozen = Arc::new(std::mem::replace(&mut self.active, Memtable::new()));
let mut st = self.lock();
if let Some(e) = st.flush_err.take() {
return Err(e);
}
while st.immutable.len() >= self.shared.max_immutable && !st.shutdown {
st = self
.shared
.signal
.wait(st)
.unwrap_or_else(|e| e.into_inner());
}
st.immutable.push_back(frozen);
self.shared.signal.notify_all();
Ok(())
}
}
}
fn flush_inline(&mut self) -> io::Result<()> {
if self.active.is_empty() {
return Ok(());
}
let seq = self.reserve_seq();
let path = self.shared.data_dir.join(format!("sst-{seq:012}.dat"));
let sst = SsTable::write(
&path,
self.active.entry_count(),
self.active.sorted_entries(),
)?;
self.active.clear();
push_sstable(&mut self.lock(), sst);
Ok(())
}
fn drain(&mut self) -> io::Result<()> {
if self.flush_handle.is_none() {
return Ok(());
}
let mut st = self.lock();
while !st.immutable.is_empty() && st.flush_err.is_none() {
st = self
.shared
.signal
.wait(st)
.unwrap_or_else(|e| e.into_inner());
}
match st.flush_err.take() {
Some(e) => Err(e),
None => Ok(()),
}
}
fn take_flush_err(&self) -> io::Result<()> {
match self.lock().flush_err.take() {
Some(e) => Err(e),
None => Ok(()),
}
}
fn ensure_worker(&mut self) {
if self.flush_handle.is_some() {
return;
}
let shared = Arc::clone(&self.shared);
self.flush_handle = Some(std::thread::spawn(move || flush_worker(&shared)));
}
}
fn flush_worker(shared: &Shared) {
loop {
let frozen = {
let mut st = shared.state.lock().unwrap_or_else(|e| e.into_inner());
loop {
if !st.immutable.is_empty() {
break;
}
if st.shutdown {
return;
}
st = shared.signal.wait(st).unwrap_or_else(|e| e.into_inner());
}
Arc::clone(st.immutable.front().unwrap())
};
let seq = {
let mut st = shared.state.lock().unwrap_or_else(|e| e.into_inner());
let seq = st.next_seq;
st.next_seq += 1;
seq
};
let path = shared.data_dir.join(format!("sst-{seq:012}.dat"));
let result = SsTable::write(&path, frozen.entry_count(), frozen.sorted_entries());
let mut st = shared.state.lock().unwrap_or_else(|e| e.into_inner());
match result {
Ok(sst) => push_sstable(&mut st, sst),
Err(e) => {
if st.flush_err.is_none() {
st.flush_err = Some(e);
}
}
}
st.immutable.pop_front();
shared.signal.notify_all();
}
}
fn push_sstable(st: &mut State, sst: SsTable) {
let mut runs = (*st.sstables).clone();
runs.push(Arc::new(sst));
st.sstables = Arc::new(runs);
}
impl Drop for LsmTree {
fn drop(&mut self) {
if self.flush_handle.is_some() {
let _ = self.enqueue_active();
{
let mut st = self.lock();
st.shutdown = true;
self.shared.signal.notify_all();
}
if let Some(h) = self.flush_handle.take() {
let _ = h.join();
}
} else {
let _ = self.flush_inline();
}
}
}