use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::OnceLock;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use arc_swap::ArcSwap;
use bytes::Bytes;
use parking_lot::{Condvar, Mutex, RwLock};
use crate::batch::{self, BatchOp, WriteBatch};
use crate::compaction;
use crate::error::{Error, Result};
use crate::flush;
use crate::fs::{Fs, StdFs, WritableFile};
use crate::iter::{DbIterator, InternalIterator, IteratorMode, ReadOptions};
use crate::key::{self, Kind, MAX_SEQ};
use crate::log::{self, encode_record_parts, read_records};
use crate::manifest::{self, file_no_u32, Edit, ManifestState, ManifestWriter, DEFAULT_CF};
use crate::memtable::{self, Memtable};
use crate::sst::BlockCache;
use crate::version::{by_smallest, table_name, SuperVersion, TableFile, Version, NUM_LEVELS};
pub const MAX_GROUP_BYTES: usize = 4 << 20;
const MANIFEST_ROLL_BYTES: u64 = 64 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Durability {
Full,
Eventual { sync_interval: Duration },
}
#[derive(Clone)]
pub struct Options {
pub create_if_missing: bool,
pub error_if_exists: bool,
pub durability: Durability,
pub fs: Arc<dyn Fs>,
pub write_buffer_size: usize,
pub max_immutable_memtables: usize,
pub target_file_size: u64,
pub level0_compaction_trigger: usize,
pub level0_slowdown_trigger: usize,
pub level0_stop_trigger: usize,
pub max_bytes_for_level_base: u64,
pub level_multiplier: u64,
pub max_subcompactions: usize,
pub max_background_compactions: usize,
pub max_background_jobs: usize,
pub block_cache_size: u64,
pub bytes_per_sync: u64,
pub recycle_wal_files: usize,
}
impl Default for Options {
fn default() -> Self {
Options {
create_if_missing: true,
error_if_exists: false,
durability: Durability::Full,
fs: Arc::new(StdFs),
write_buffer_size: 64 << 20,
max_immutable_memtables: 2,
target_file_size: 64 << 20,
level0_compaction_trigger: 4,
level0_slowdown_trigger: 20,
level0_stop_trigger: 36,
max_bytes_for_level_base: 512 << 20,
level_multiplier: 10,
max_subcompactions: 4,
max_background_compactions: 1,
max_background_jobs: 6,
block_cache_size: 512 << 20,
bytes_per_sync: 0,
recycle_wal_files: 1,
}
}
}
impl fmt::Debug for Options {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Options")
.field("create_if_missing", &self.create_if_missing)
.field("error_if_exists", &self.error_if_exists)
.field("durability", &self.durability)
.field("fs", &self.fs)
.field("write_buffer_size", &self.write_buffer_size)
.field("max_immutable_memtables", &self.max_immutable_memtables)
.field("target_file_size", &self.target_file_size)
.field("level0_compaction_trigger", &self.level0_compaction_trigger)
.field("level0_slowdown_trigger", &self.level0_slowdown_trigger)
.field("level0_stop_trigger", &self.level0_stop_trigger)
.field("max_bytes_for_level_base", &self.max_bytes_for_level_base)
.field("level_multiplier", &self.level_multiplier)
.field("max_subcompactions", &self.max_subcompactions)
.field(
"max_background_compactions",
&self.max_background_compactions,
)
.field("max_background_jobs", &self.max_background_jobs)
.field("block_cache_size", &self.block_cache_size)
.field("bytes_per_sync", &self.bytes_per_sync)
.field("recycle_wal_files", &self.recycle_wal_files)
.finish()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WriteOptions {
pub sync: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Stats {
pub commit_groups: u64,
pub committed_batches: u64,
pub wal_bytes: u64,
pub wal_fsyncs: u64,
pub l0_slowdowns: u64,
pub l0_stop_micros: u64,
pub memtable_wait_micros: u64,
pub flushes: u64,
pub flushed_bytes: u64,
pub compactions: u64,
pub compaction_read_bytes: u64,
pub compaction_written_bytes: u64,
pub compaction_micros: u64,
pub largest_wal_bytes: u64,
}
#[derive(Default)]
struct Counters {
commit_groups: AtomicU64,
committed_batches: AtomicU64,
wal_bytes: AtomicU64,
wal_fsyncs: AtomicU64,
l0_slowdowns: AtomicU64,
l0_stop_micros: AtomicU64,
memtable_wait_micros: AtomicU64,
flushes: AtomicU64,
flushed_bytes: AtomicU64,
compactions: AtomicU64,
compaction_read_bytes: AtomicU64,
compaction_written_bytes: AtomicU64,
compaction_micros: AtomicU64,
largest_wal_bytes: AtomicU64,
}
fn add(counter: &AtomicU64, n: u64) {
counter.fetch_add(n, Ordering::Relaxed);
}
fn micros_since(start: Instant) -> u64 {
start.elapsed().as_micros() as u64
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RecoveryReport {
pub wal_files: usize,
pub batches: u64,
pub discarded_bytes: u64,
pub orphan_tables: usize,
pub dropped_wal_files: usize,
}
#[derive(Clone)]
pub struct ColumnFamily {
inner: Arc<CfInner>,
}
struct CfInner {
id: u32,
name: String,
dropped: AtomicBool,
}
impl ColumnFamily {
fn new(id: u32, name: &str) -> Self {
ColumnFamily {
inner: Arc::new(CfInner {
id,
name: name.to_string(),
dropped: AtomicBool::new(false),
}),
}
}
#[cfg(test)]
pub(crate) fn for_tests(id: u32, name: &str) -> Self {
Self::new(id, name)
}
pub fn name(&self) -> &str {
&self.inner.name
}
pub(crate) fn id(&self) -> u32 {
self.inner.id
}
fn check(&self) -> Result<()> {
if self.inner.dropped.load(Ordering::Acquire) {
Err(Error::ColumnFamilyDropped(self.inner.name.clone()))
} else {
Ok(())
}
}
}
impl fmt::Debug for ColumnFamily {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ColumnFamily({:?}, id {})",
self.inner.name, self.inner.id
)
}
}
#[derive(Clone)]
pub struct Db {
handle: Arc<Handle>,
}
struct Handle {
inner: Arc<Inner>,
threads: Mutex<Vec<JoinHandle<()>>>,
}
struct Inner {
dir: PathBuf,
fs: Arc<dyn Fs>,
durability: Durability,
write_buffer_size: usize,
max_imms: usize,
target_file_size: u64,
block_cache: Arc<BlockCache>,
compaction: compaction::Config,
l0_slowdown: usize,
l0_stop: usize,
cursors: Mutex<Vec<Vec<u8>>>,
cfs: RwLock<HashMap<String, ColumnFamily>>,
structure: Mutex<Structure>,
sv: ArcSwap<SuperVersion>,
sv_lock: Mutex<()>,
compacting: Mutex<HashSet<u64>>,
compaction_threads: usize,
compaction_threads_used: Mutex<usize>,
visible_seq: AtomicU64,
commit: Mutex<CommitState>,
queue: Mutex<Queue>,
queue_cv: Condvar,
queued: AtomicUsize,
leader_busy: AtomicBool,
last_group: AtomicUsize,
publish: Mutex<std::collections::BTreeMap<u64, u64>>,
publish_cv: Condvar,
recycled_wals: Mutex<Vec<u64>>,
recycle_wal_files: usize,
current_wal: Mutex<Arc<WalFile>>,
barrier: Mutex<SyncBarrier>,
graveyard: crate::version::Graveyard,
poisoned: AtomicBool,
poison_cause: Mutex<Option<Arc<str>>>,
bg: Mutex<Background>,
bg_cv: Condvar,
recovery: RecoveryReport,
counters: Counters,
_lock: Box<dyn Send + Sync>,
}
#[derive(Default)]
struct SyncBarrier {
older: Vec<Arc<WalFile>>,
dir_dirty: bool,
}
#[derive(Default)]
struct Background {
shutdown: bool,
flush_error: Option<Error>,
}
struct Structure {
writer: ManifestWriter,
state: ManifestState,
}
struct WalFile {
file: Arc<dyn WritableFile>,
number: u64,
dirty: AtomicBool,
written: AtomicU64,
}
struct CommitState {
wal: Arc<WalFile>,
last_seq: u64,
buf: Vec<u8>,
}
struct Pending {
batch: WriteBatch,
sync: bool,
slot: Mutex<Option<Slot>>,
ready: AtomicBool,
}
enum Slot {
Insert(Arc<Group>, u64),
Wait(Arc<Group>),
InsertAll(Arc<Group>, Vec<u64>),
Failed(Error),
}
const MAX_PARALLEL_INSERT: usize = 8;
struct Group {
mem: Arc<Memtable>,
first: u64,
last: u64,
remaining: AtomicUsize,
}
#[derive(Default)]
struct Queue {
waiting: VecDeque<Arc<Pending>>,
leader_active: bool,
}
const SPIN: Duration = Duration::from_micros(50);
fn spin_until(done: impl Fn() -> bool) -> bool {
static SPINNERS: AtomicUsize = AtomicUsize::new(0);
static CORES: OnceLock<usize> = OnceLock::new();
let cores = *CORES.get_or_init(|| std::thread::available_parallelism().map_or(4, |n| n.get()));
if SPINNERS.fetch_add(1, Ordering::Relaxed) >= cores {
SPINNERS.fetch_sub(1, Ordering::Relaxed);
return done();
}
let spun = (|| {
for _ in 0..64 {
if done() {
return true;
}
std::hint::spin_loop();
}
let start = Instant::now();
while start.elapsed() < SPIN {
if done() {
return true;
}
std::thread::yield_now();
}
done()
})();
SPINNERS.fetch_sub(1, Ordering::Relaxed);
spun
}
pub(crate) fn wal_name(no: u64) -> String {
format!("{no:06}.wal")
}
fn parse_wal_name(name: &str) -> Option<u64> {
name.strip_suffix(".wal")?.parse().ok()
}
impl Db {
pub fn open(path: impl AsRef<Path>, opts: Options) -> Result<Db> {
let dir = path.as_ref().to_path_buf();
let fs = opts.fs.clone();
if opts.create_if_missing {
fs.create_dir_all(&dir)?;
}
let lock = fs.lock(&dir.join("LOCK"))?;
let mut state = match manifest::current(fs.as_ref(), &dir)? {
Some(_) if opts.error_if_exists => {
return Err(Error::InvalidArgument(format!(
"{} already exists",
dir.display()
)))
}
Some(no) => manifest::read(fs.as_ref(), &dir, no)?,
None if opts.create_if_missing => ManifestState::new_database(),
None => {
return Err(Error::InvalidArgument(format!(
"{} does not exist",
dir.display()
)))
}
};
let mut recovery = RecoveryReport::default();
let live: std::collections::HashSet<u64> = state
.levels
.iter()
.flat_map(|l| l.keys().copied())
.collect();
let (highest, orphans) = flush::remove_orphans(fs.as_ref(), &dir, &|n| live.contains(&n))?;
recovery.orphan_tables = orphans;
state.next_file = state.next_file.max(highest + 1);
let mut wals: Vec<u64> = fs
.list(&dir)?
.iter()
.filter_map(|name| parse_wal_name(name))
.collect();
wals.sort_unstable();
let mut recycled = Vec::new();
for no in wals.iter().filter(|no| **no < state.log_number) {
if recycled.len() < opts.recycle_wal_files {
recycled.push(*no);
} else {
fs.remove(&dir.join(wal_name(*no))).ok();
}
}
wals.retain(|no| *no >= state.log_number);
if let Some(last) = wals.last() {
state.next_file = state.next_file.max(last + 1);
}
let manifest_no = state.allocate_file();
let wal_no = state.allocate_file();
let mem = Memtable::new(wals.first().copied().unwrap_or(wal_no));
let mut last_seq = state.last_seq;
for props in state.levels.iter().flat_map(|l| l.values()) {
last_seq = last_seq.max(props.largest_seq);
}
for t in &state.tombstones {
last_seq = last_seq.max(t.seq);
}
let mut cut = None;
for (i, &no) in wals.iter().enumerate() {
let data = fs.read(&dir.join(wal_name(no)))?;
let log = read_records(&data, file_no_u32(no)?);
if log.prev_last_seq.is_some_and(|prev| prev > last_seq) {
cut = Some(i);
break;
}
recovery.wal_files += 1;
recovery.discarded_bytes += log.discarded as u64;
for record in log.records {
let decoded = batch::decode(record)?;
recovery.batches += 1;
let count = decoded.ops.len() as u64;
last_seq = last_seq.max(decoded.first_seq + count.saturating_sub(1));
apply(&mem, decoded.ops, decoded.first_seq, |cf| {
state.cfs.contains_key(&cf)
});
}
}
if let Some(i) = cut {
for &no in &wals[i..] {
let path = dir.join(wal_name(no));
recovery.discarded_bytes += fs.read(&path).map(|d| d.len() as u64).unwrap_or(0);
fs.remove(&path)?;
recovery.dropped_wal_files += 1;
}
wals.truncate(i);
}
let wal_file = fs.create(&dir.join(wal_name(wal_no)))?;
let mut header = Vec::new();
log::encode_header(&mut header, file_no_u32(wal_no)?, last_seq);
wal_file.append(&header)?;
let writer = ManifestWriter::install(fs.as_ref(), &dir, manifest_no, &state)?;
for name in fs.list(&dir)? {
if manifest::parse_manifest_name(&name).is_some_and(|no| no != manifest_no) {
fs.remove(&dir.join(&name)).ok();
}
}
let block_cache = Arc::new(BlockCache::new(opts.block_cache_size));
let graveyard = crate::version::Graveyard::default();
let mut version = Version::empty();
for (level, files) in state.levels.iter().enumerate() {
for (number, props) in files {
version.levels[level].push(Arc::new(
TableFile::new(
fs.clone(),
block_cache.clone(),
&dir,
*number,
props.clone(),
)
.with_graveyard(graveyard.clone()),
));
}
}
version.sort_level0();
for level in &mut version.levels[1..] {
level.sort_by(by_smallest);
}
version.tombstones = state.tombstones.clone();
let cfs = state
.cfs
.iter()
.map(|(id, name)| (name.clone(), ColumnFamily::new(*id, name)))
.collect();
let wal = Arc::new(WalFile {
file: wal_file,
number: wal_no,
dirty: AtomicBool::new(false),
written: AtomicU64::new(0),
});
let inner = Arc::new(Inner {
dir,
fs,
durability: opts.durability,
write_buffer_size: opts.write_buffer_size.max(1),
max_imms: opts.max_immutable_memtables.max(1),
target_file_size: opts.target_file_size.max(1),
block_cache,
compaction: compaction::Config {
l0_trigger: opts.level0_compaction_trigger.max(1),
level_base_bytes: opts.max_bytes_for_level_base.max(1),
level_multiplier: opts.level_multiplier.max(2),
target_file_size: opts.target_file_size.max(1),
subcompactions: opts.max_subcompactions.max(1),
writeback: opts.bytes_per_sync > 0,
},
l0_slowdown: opts.level0_slowdown_trigger.max(1),
l0_stop: opts
.level0_stop_trigger
.max(opts.level0_slowdown_trigger)
.max(1),
cursors: Mutex::new(vec![Vec::new(); NUM_LEVELS]),
cfs: RwLock::new(cfs),
structure: Mutex::new(Structure { writer, state }),
sv: ArcSwap::from_pointee(SuperVersion {
mem: Arc::new(mem),
imms: Vec::new(),
version: Arc::new(version),
}),
sv_lock: Mutex::new(()),
compacting: Mutex::new(HashSet::new()),
compaction_threads: opts.max_background_jobs.saturating_sub(1).max(1),
compaction_threads_used: Mutex::new(0),
visible_seq: AtomicU64::new(last_seq),
commit: Mutex::new(CommitState {
wal: wal.clone(),
last_seq,
buf: Vec::new(),
}),
queue: Mutex::new(Queue::default()),
queue_cv: Condvar::new(),
queued: AtomicUsize::new(0),
leader_busy: AtomicBool::new(false),
last_group: AtomicUsize::new(1),
publish: Mutex::new(std::collections::BTreeMap::new()),
publish_cv: Condvar::new(),
recycled_wals: Mutex::new(recycled),
recycle_wal_files: opts.recycle_wal_files,
current_wal: Mutex::new(wal),
barrier: Mutex::new(SyncBarrier::default()),
graveyard,
poisoned: AtomicBool::new(false),
poison_cause: Mutex::new(None),
bg: Mutex::new(Background::default()),
bg_cv: Condvar::new(),
recovery,
counters: Counters::default(),
_lock: lock,
});
let mut threads = Vec::new();
let flusher = inner.clone();
threads.push(
std::thread::Builder::new()
.name("solistore-flush".into())
.spawn(move || flusher.flush_loop())
.map_err(Error::from)?,
);
for i in 0..opts.max_background_compactions.max(1) {
let compactor = inner.clone();
threads.push(
std::thread::Builder::new()
.name(format!("solistore-compact-{i}"))
.spawn(move || compactor.compaction_loop())
.map_err(Error::from)?,
);
}
if let Durability::Eventual { sync_interval } = opts.durability {
let syncer = inner.clone();
threads.push(
std::thread::Builder::new()
.name("solistore-wal-sync".into())
.spawn(move || syncer.sync_loop(sync_interval))
.map_err(Error::from)?,
);
}
Ok(Db {
handle: Arc::new(Handle {
inner,
threads: Mutex::new(threads),
}),
})
}
fn inner(&self) -> &Inner {
&self.handle.inner
}
pub fn path(&self) -> &Path {
&self.inner().dir
}
pub fn recovery_report(&self) -> &RecoveryReport {
&self.inner().recovery
}
pub fn stats(&self) -> Stats {
let c = &self.inner().counters;
let get = |a: &AtomicU64| a.load(Ordering::Relaxed);
Stats {
commit_groups: get(&c.commit_groups),
committed_batches: get(&c.committed_batches),
wal_bytes: get(&c.wal_bytes),
wal_fsyncs: get(&c.wal_fsyncs),
l0_slowdowns: get(&c.l0_slowdowns),
l0_stop_micros: get(&c.l0_stop_micros),
memtable_wait_micros: get(&c.memtable_wait_micros),
flushes: get(&c.flushes),
flushed_bytes: get(&c.flushed_bytes),
compactions: get(&c.compactions),
compaction_read_bytes: get(&c.compaction_read_bytes),
compaction_written_bytes: get(&c.compaction_written_bytes),
compaction_micros: get(&c.compaction_micros),
largest_wal_bytes: get(&c.largest_wal_bytes),
}
}
pub fn memtable_bytes(&self) -> usize {
let sv = self.inner().sv.load();
std::iter::once(&sv.mem)
.chain(&sv.imms)
.map(|m| m.approximate_size())
.sum()
}
pub fn table_readers_bytes(&self) -> usize {
self.inner().block_cache.pinned() as usize
}
pub fn block_cache_usage(&self) -> u64 {
self.inner().block_cache.usage()
}
pub fn live_table_bytes(&self) -> u64 {
self.inner()
.sv
.load()
.version
.files()
.map(|f| f.props.size)
.sum()
}
pub fn approximate_size(&self, cf: &ColumnFamily, start: &[u8], end: &[u8]) -> Result<u64> {
cf.check()?;
if start >= end {
return Ok(0);
}
let lo = key::first_of(cf.id(), start);
let hi = key::first_of(cf.id(), end);
let sv = self.inner().sv.load();
let mut total = 0;
for file in sv.version.files() {
let (smallest, largest) = (&file.props.smallest, &file.props.largest);
if key::compare(largest, &lo).is_lt() || key::compare(smallest, &hi).is_ge() {
continue;
}
if key::compare(smallest, &lo).is_ge() && key::compare(largest, &hi).is_lt() {
total += file.props.size;
continue;
}
let reader = file.reader()?;
let from = reader.approximate_offset_of(&lo)?;
let to = reader.approximate_offset_of(&hi)?;
total += to.saturating_sub(from);
}
Ok(total)
}
pub fn pending_range_tombstones(&self) -> usize {
self.inner().sv.load().version.tombstones.len()
}
pub fn files_per_level(&self) -> Vec<usize> {
self.inner()
.sv
.load()
.version
.levels
.iter()
.map(Vec::len)
.collect()
}
pub fn cf_handle(&self, name: &str) -> Option<ColumnFamily> {
self.inner().cfs.read().get(name).cloned()
}
pub fn default_cf(&self) -> ColumnFamily {
self.cf_handle(DEFAULT_CF)
.expect("the default column family always exists")
}
pub fn cf_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.inner().cfs.read().keys().cloned().collect();
names.sort();
names
}
pub fn create_cf(&self, name: &str) -> Result<ColumnFamily> {
if name.is_empty() {
return Err(Error::InvalidArgument("empty column family name".into()));
}
let inner = self.inner();
let mut structure = inner.structure.lock();
if inner.cfs.read().contains_key(name) {
return Err(Error::ColumnFamilyExists(name.to_string()));
}
let id = structure.state.next_cf_id;
let next = id
.checked_add(1)
.ok_or_else(|| Error::InvalidArgument("column family ids exhausted".into()))?;
inner.append_edits(
&mut structure,
&[
Edit::CreateCf {
id,
name: name.to_string(),
},
Edit::NextCfId(next),
],
)?;
let cf = ColumnFamily::new(id, name);
inner.cfs.write().insert(name.to_string(), cf.clone());
Ok(cf)
}
pub fn drop_cf(&self, name: &str) -> Result<()> {
if name == DEFAULT_CF {
return Err(Error::InvalidArgument(
"the default column family cannot be dropped".into(),
));
}
let inner = self.inner();
let cf = self
.cf_handle(name)
.ok_or_else(|| Error::ColumnFamilyNotFound(name.to_string()))?;
let id = cf.id();
inner.log_and_apply(
|sv| {
let mut edits = vec![Edit::DropCf { id }];
let mut version = (*sv.version).clone();
let mut retired = Vec::new();
for (level, files) in version.levels.iter_mut().enumerate() {
files.retain(|f| {
let doomed = compaction::file_bounds(f).is_some_and(|(c, _, _)| c == id);
if doomed {
edits.push(Edit::DeleteFile {
level: level as u32,
number: f.number,
});
retired.push(f.clone());
}
!doomed
});
}
Ok(Some(VersionChange {
edits,
sv: SuperVersion {
mem: sv.mem.clone(),
imms: sv.imms.clone(),
version: Arc::new(version),
},
retired,
}))
},
|| {},
)?;
cf.inner.dropped.store(true, Ordering::Release);
inner.cfs.write().remove(name);
Ok(())
}
pub fn compact_range(
&self,
cf: &ColumnFamily,
start: Option<&[u8]>,
end: Option<&[u8]>,
) -> Result<()> {
cf.check()?;
self.flush()?;
let inner = self.inner();
let prefix = cf.id().to_be_bytes();
let lo = [&prefix[..], start.unwrap_or_default()].concat();
let hi = match end {
Some(end) => [&prefix[..], end].concat(),
None => cf
.id()
.checked_add(1)
.map_or_else(|| vec![0xff; 5], |next| next.to_be_bytes().to_vec()),
};
let wanted = |v: &Version, busy: compaction::Busy<'_>| {
compaction::pick_for_tombstones(v, busy)
.or_else(|| compaction::pick_level0_overlapping(v, &lo, &hi, busy))
};
for _ in 0..10_000 {
match inner.claim(wanted) {
Some((c, version)) => {
let outcome = inner.compact(&c, &version);
inner.release(&c);
outcome?;
}
None => {
let version = inner.sv.load().version.clone();
if wanted(&version, &|_| false).is_none() {
break;
}
inner.check()?;
let mut bg = inner.bg.lock();
inner.bg_cv.wait_for(&mut bg, Duration::from_millis(10));
}
}
}
Ok(())
}
pub fn ingest<K, V, E>(
&self,
cf: &ColumnFamily,
entries: impl IntoIterator<Item = std::result::Result<(K, V), E>>,
threads: usize,
) -> Result<IngestReport>
where
K: AsRef<[u8]>,
V: AsRef<[u8]>,
E: fmt::Display,
{
cf.check()?;
let inner = self.inner();
inner.check()?;
let id = cf.id();
if !inner.untouched(&inner.sv.load(), id) {
return Err(Error::InvalidArgument(format!(
"ingest needs an untouched column family; {} holds entries, deletes or \
range deletes",
cf.name()
)));
}
let threads = threads.max(1);
let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<(Vec<u8>, Vec<u8>)>>(threads);
let rx = Mutex::new(rx);
let written: Mutex<Vec<(u64, crate::sst::TableProps)>> = Mutex::new(Vec::new());
let failed: Mutex<Option<Error>> = Mutex::new(None);
let mut report = IngestReport::default();
let read = std::thread::scope(|scope| {
for _ in 0..threads {
scope.spawn(|| loop {
let Ok(chunk) = rx.lock().recv() else {
return;
};
if failed.lock().is_some() {
continue; }
match inner.ingest_chunk(id, &chunk) {
Ok(files) => written.lock().extend(files),
Err(e) => {
failed.lock().get_or_insert(e);
}
}
});
}
let tx = tx;
let mut chunk: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
let mut size = 0usize;
let mut last: Option<Vec<u8>> = None;
for item in entries {
let (k, v) =
item.map_err(|e| Error::InvalidArgument(format!("ingest source: {e}")))?;
let (k, v) = (k.as_ref(), v.as_ref());
match &mut last {
Some(l) if l.as_slice() >= k => {
return Err(Error::InvalidArgument(
"ingest keys must be strictly increasing".into(),
));
}
Some(l) => {
l.clear();
l.extend_from_slice(k);
}
None => last = Some(k.to_vec()),
}
size += k.len() + v.len() + key::MIN_LEN;
report.entries += 1;
report.bytes += (k.len() + v.len()) as u64;
chunk.push((key::encode(id, k, 0, Kind::Put), v.to_vec()));
if size >= INGEST_CHUNK_BYTES {
size = 0;
if failed.lock().is_some() || tx.send(std::mem::take(&mut chunk)).is_err() {
break;
}
}
}
if !chunk.is_empty() {
let _ = tx.send(chunk);
}
Ok(())
});
let mut files = written.into_inner();
let outcome = read
.and_then(|()| failed.into_inner().map_or(Ok(()), Err))
.and_then(|()| inner.install_ingested(id, &mut files));
if let Err(e) = outcome {
for (number, _) in &files {
inner.fs.remove(&inner.dir.join(table_name(*number))).ok();
}
return Err(e);
}
report.files = files.len();
Ok(report)
}
pub fn checkpoint(&self, path: impl AsRef<Path>) -> Result<()> {
let target = path.as_ref();
let inner = self.inner();
if inner.fs.exists(target) {
return Err(Error::InvalidArgument(format!(
"{} already exists",
target.display()
)));
}
self.flush()?;
let sv = inner.sv.load_full();
let mut state = ManifestState::empty();
state.cfs = inner.structure.lock().state.cfs.clone();
state.next_cf_id = inner.structure.lock().state.next_cf_id;
state.tombstones = sv.version.tombstones.clone();
let mut highest = 0;
for (level, files) in sv.version.levels.iter().enumerate() {
for f in files {
state.levels[level].insert(f.number, f.props.clone());
state.last_seq = state.last_seq.max(f.props.largest_seq);
highest = highest.max(f.number);
}
}
for t in &state.tombstones {
state.last_seq = state.last_seq.max(t.seq);
}
state.next_file = highest + 1;
state.log_number = state.next_file;
inner.fs.create_dir_all(target)?;
for f in sv.version.files() {
inner.fs.link(
&inner.dir.join(table_name(f.number)),
&target.join(table_name(f.number)),
)?;
}
let manifest_no = state.allocate_file();
ManifestWriter::install(inner.fs.as_ref(), target, manifest_no, &state)?;
Ok(())
}
pub fn put(
&self,
cf: &ColumnFamily,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) -> Result<()> {
let mut batch = WriteBatch::new();
batch.put(cf, key, value);
self.write(batch)
}
pub fn delete(&self, cf: &ColumnFamily, key: impl AsRef<[u8]>) -> Result<()> {
let mut batch = WriteBatch::new();
batch.delete(cf, key);
self.write(batch)
}
pub fn write(&self, batch: WriteBatch) -> Result<()> {
self.write_opt(batch, &WriteOptions::default())
}
pub fn write_opt(&self, batch: WriteBatch, opts: &WriteOptions) -> Result<()> {
if batch.is_empty() {
return Ok(());
}
for cf in batch.column_families() {
cf.check()?;
}
self.inner().commit(batch, opts.sync)
}
pub fn sync_wal(&self) -> Result<()> {
let inner = self.inner();
inner.check()?;
let wal = inner.current_wal.lock().clone();
inner.sync_wal_file(&wal)
}
pub fn flush(&self) -> Result<()> {
let inner = self.inner();
{
let mut commit = inner.commit.lock();
if !inner.sv.load().mem.is_empty() {
inner.switch_memtable(&mut commit)?;
}
}
let mut bg = inner.bg.lock();
loop {
inner.check()?;
if let Some(e) = &bg.flush_error {
return Err(e.clone());
}
if inner.sv.load().imms.is_empty() {
return Ok(());
}
inner.bg_cv.wait(&mut bg);
}
}
pub fn get(&self, cf: &ColumnFamily, key: impl AsRef<[u8]>) -> Result<Option<Bytes>> {
cf.check()?;
let inner = self.inner();
let sv = inner.sv.load();
let snapshot = inner.visible_seq.load(Ordering::Acquire);
sv.get(cf.id(), key.as_ref(), snapshot)
}
pub fn multi_get<K: AsRef<[u8]>>(
&self,
cf: &ColumnFamily,
keys: &[K],
) -> Result<Vec<Option<Bytes>>> {
cf.check()?;
let inner = self.inner();
let sv = inner.sv.load();
let snapshot = inner.visible_seq.load(Ordering::Acquire);
keys.iter()
.map(|k| sv.get(cf.id(), k.as_ref(), snapshot))
.collect()
}
pub fn iterator(&self, cf: &ColumnFamily, mode: IteratorMode<'_>) -> Result<DbIterator> {
self.iterator_opt(cf, ReadOptions::default(), mode)
}
pub fn iterator_opt(
&self,
cf: &ColumnFamily,
opts: ReadOptions,
mode: IteratorMode<'_>,
) -> Result<DbIterator> {
cf.check()?;
let inner = self.inner();
let sv = inner.sv.load_full();
let snapshot = inner.visible_seq.load(Ordering::Acquire);
DbIterator::new(sv, cf.id(), snapshot, opts, mode)
}
}
impl fmt::Debug for Db {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Db({})", self.inner().dir.display())
}
}
impl Drop for Handle {
fn drop(&mut self) {
self.inner.bg.lock().shutdown = true;
self.inner.bg_cv.notify_all();
for thread in self.threads.lock().drain(..) {
thread.join().ok();
}
let wal = self.inner.current_wal.lock().clone();
if !self.inner.poisoned.load(Ordering::Acquire) {
let mut end = Vec::new();
if let Ok(no) = file_no_u32(wal.number) {
log::encode_end(&mut end, no);
if wal.file.append(&end).is_ok() {
self.inner.sync_wal_file(&wal).ok();
}
}
}
}
}
struct Defer<F: FnMut()>(F);
impl<F: FnMut()> Drop for Defer<F> {
fn drop(&mut self) {
(self.0)()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IngestReport {
pub entries: u64,
pub bytes: u64,
pub files: usize,
}
const INGEST_CHUNK_BYTES: usize = 96 << 20;
struct VersionChange {
edits: Vec<Edit>,
sv: SuperVersion,
retired: Vec<Arc<TableFile>>,
}
fn tidy(change: &mut VersionChange) {
let mut version = (*change.sv.version).clone();
for t in version.tombstones.clone() {
for (level, files) in version.levels.iter_mut().enumerate() {
files.retain(|f| {
let doomed = compaction::fully_covered(f, &t);
if doomed {
change.edits.push(Edit::DeleteFile {
level: level as u32,
number: f.number,
});
change.retired.push(f.clone());
}
!doomed
});
}
}
let files: Vec<Arc<TableFile>> = version.files().cloned().collect();
version.tombstones.retain(|t| {
let needed = files.iter().any(|f| compaction::may_hold_covered(f, t));
if !needed {
change.edits.push(Edit::RemoveRangeTombstone { seq: t.seq });
}
needed
});
change.sv.version = Arc::new(version);
}
type Entry<'a> = (u64, u32, &'a [u8], Kind, &'a [u8]);
fn apply(mem: &Memtable, ops: Vec<BatchOp<'_>>, first_seq: u64, live: impl Fn(u32) -> bool) {
let entries: Vec<Entry<'_>> = (first_seq..)
.zip(&ops)
.filter_map(|(seq, op)| match *op {
BatchOp::Put { cf, key, value } if live(cf) => Some((seq, cf, key, Kind::Put, value)),
BatchOp::Delete { cf, key } if live(cf) => Some((seq, cf, key, Kind::Delete, &b""[..])),
_ => None,
})
.collect();
let total: usize = entries
.iter()
.map(|e| key::MIN_LEN + e.2.len() + e.4.len())
.sum();
let buf = memtable::arena_copy(total, |out| {
for &(seq, cf, k, kind, v) in &entries {
out.extend_from_slice(&cf.to_be_bytes());
out.extend_from_slice(k);
out.extend_from_slice(&key::trailer_bytes(seq, kind));
out.extend_from_slice(v);
}
});
let mut bytes = 0;
let mut at = 0;
for &(_, _, k, _, v) in &entries {
let key_end = at + key::MIN_LEN + k.len();
bytes += mem.insert_shared(
buf.slice(at..key_end),
buf.slice(key_end..key_end + v.len()),
);
at = key_end + v.len();
}
let mut last = 0;
for (seq, op) in (first_seq..).zip(ops) {
if let BatchOp::DeleteRange { cf, start, end } = op {
if live(cf) {
mem.insert_range_tombstone(cf, start, end, seq);
}
}
last = seq;
}
mem.account(bytes, last);
}
impl Inner {
fn poison(&self) {
self.poisoned.store(true, Ordering::Release);
self.bg_cv.notify_all();
}
fn poison_because(&self, cause: &Error) {
self.poison_cause
.lock()
.get_or_insert_with(|| Arc::from(cause.to_string()));
self.poison();
}
fn poisoned_error(&self) -> Error {
Error::Poisoned(self.poison_cause.lock().clone())
}
fn check(&self) -> Result<()> {
if self.poisoned.load(Ordering::Acquire) {
Err(self.poisoned_error())
} else {
Ok(())
}
}
fn io<T>(&self, step: impl FnOnce() -> Result<T>) -> Result<T> {
step().inspect_err(|e| self.poison_because(e))
}
fn sync_wal_file(&self, wal: &WalFile) -> Result<()> {
{
let mut barrier = self.barrier.lock();
for old in std::mem::take(&mut barrier.older) {
old.dirty.store(false, Ordering::Release);
self.io(|| Ok(old.file.sync()?))?;
}
if barrier.dir_dirty {
self.io(|| Ok(self.fs.sync_dir(&self.dir)?))?;
barrier.dir_dirty = false;
}
}
wal.dirty.store(false, Ordering::Release);
self.io(|| Ok(wal.file.sync()?))
}
fn append_edits(&self, structure: &mut Structure, edits: &[Edit]) -> Result<()> {
self.check()?;
self.io(|| structure.writer.append(edits))?;
for edit in edits {
structure.state.apply(edit);
}
if structure.writer.size() > MANIFEST_ROLL_BYTES {
let old = structure.writer.number();
let no = structure.state.allocate_file();
let writer = self.io(|| {
ManifestWriter::install(self.fs.as_ref(), &self.dir, no, &structure.state)
})?;
structure.writer = writer;
self.fs
.remove(&self.dir.join(manifest::manifest_name(old)))
.ok();
}
Ok(())
}
fn untouched(&self, sv: &SuperVersion, id: u32) -> bool {
let cf_of = |k: &[u8]| key::parse(k).map(|p| p.cf);
if sv.version.tombstones.iter().any(|t| t.cf == id) {
return false;
}
for mem in std::iter::once(&sv.mem).chain(&sv.imms) {
if mem.tombstones().iter().any(|t| t.cf == id) {
return false;
}
let mut it = memtable::MemIter::new(mem.clone());
it.seek_ge(&key::first_of(id, b""));
if it.valid() && cf_of(it.key()) == Some(id) {
return false;
}
}
!sv.version.levels.iter().flatten().any(|f| {
match (cf_of(&f.props.smallest), cf_of(&f.props.largest)) {
(Some(lo), Some(hi)) => lo <= id && id <= hi,
_ => true,
}
})
}
fn ingest_chunk(
&self,
id: u32,
chunk: &[(Vec<u8>, Vec<u8>)],
) -> Result<Vec<(u64, crate::sst::TableProps)>> {
let mut out =
compaction::OutputFiles::new(self.fs.as_ref(), &self.dir, self.target_file_size, 0);
let mut allocate = || self.allocate_file();
for (k, v) in chunk {
out.add(id, k, v, &mut allocate)?;
}
out.finish()
}
fn install_ingested(&self, id: u32, files: &mut [(u64, crate::sst::TableProps)]) -> Result<()> {
files.sort_by(|a, b| key::compare(&a.1.smallest, &b.1.smallest));
if files.is_empty() {
return Ok(());
}
let bottom = NUM_LEVELS - 1;
let tables: Vec<Arc<TableFile>> = files
.iter()
.map(|(number, props)| {
Arc::new(
TableFile::new(
self.fs.clone(),
self.block_cache.clone(),
&self.dir,
*number,
props.clone(),
)
.with_graveyard(self.graveyard.clone()),
)
})
.collect();
for table in &tables {
table.reader()?;
}
self.log_and_apply(
|sv| {
if !self.untouched(sv, id) {
return Err(Error::InvalidArgument(
"the column family was written to during the ingest".into(),
));
}
let edits = tables
.iter()
.map(|f| Edit::AddFile {
level: bottom as u32,
number: f.number,
props: f.props.clone(),
})
.collect();
let mut version = (*sv.version).clone();
version.levels[bottom].extend(tables.iter().cloned());
version.levels[bottom].sort_by(by_smallest);
Ok(Some(VersionChange {
edits,
sv: SuperVersion {
mem: sv.mem.clone(),
imms: sv.imms.clone(),
version: Arc::new(version),
},
retired: Vec::new(),
}))
},
|| {},
)?;
self.notify_background();
Ok(())
}
fn allocate_file(&self) -> u64 {
self.structure.lock().state.allocate_file()
}
fn install(&self, change: impl FnOnce(&SuperVersion) -> SuperVersion) {
let _guard = self.sv_lock.lock();
let current = self.sv.load_full();
self.sv.store(Arc::new(change(¤t)));
}
fn notify_background(&self) {
let _bg = self.bg.lock();
self.bg_cv.notify_all();
}
fn commit(&self, batch: WriteBatch, sync: bool) -> Result<()> {
let me = Arc::new(Pending {
batch,
sync,
slot: Mutex::new(None),
ready: AtomicBool::new(false),
});
let mut queue = self.queue.lock();
queue.waiting.push_back(me.clone());
self.queued.fetch_add(1, Ordering::Relaxed);
let mut spun = false;
let lead = loop {
if me.ready.load(Ordering::Acquire) {
break Err(me.slot.lock().take().expect("ready means set"));
}
let at_head = queue.waiting.front().is_some_and(|p| Arc::ptr_eq(p, &me));
if at_head && !queue.leader_active {
break Ok(());
}
if !spun {
spun = true;
parking_lot::MutexGuard::unlocked(&mut queue, || {
spin_until(|| {
me.ready.load(Ordering::Acquire)
|| !self.leader_busy.load(Ordering::Acquire)
});
});
continue;
}
spun = false;
self.queue_cv.wait(&mut queue);
};
let slot = match lead {
Err(slot) => {
drop(queue);
slot
}
Ok(()) => {
queue.leader_active = true;
self.leader_busy.store(true, Ordering::Release);
let fsync = self.durability == Durability::Full || me.sync;
let want = self.last_group.load(Ordering::Relaxed);
if fsync && queue.waiting.len() < want {
parking_lot::MutexGuard::unlocked(&mut queue, || {
spin_until(|| self.queued.load(Ordering::Acquire) >= want);
});
}
let mut group = Vec::new();
let mut bytes = 0;
while let Some(next) = queue.waiting.front() {
let size = next.batch.size_in_bytes();
if !group.is_empty() && bytes + size > MAX_GROUP_BYTES {
break;
}
bytes += size;
group.push(queue.waiting.pop_front().expect("front exists"));
}
self.queued.fetch_sub(group.len(), Ordering::Relaxed);
self.last_group.store(group.len(), Ordering::Relaxed);
drop(queue);
let guard = LeaderGuard {
inner: self,
group: &group,
me: &me,
};
let written = self.write_group(&group);
match guard.finish(written) {
Slot::InsertAll(g, firsts) => {
for (pending, first) in group.iter().zip(firsts) {
apply(&g.mem, pending.batch.ops(), first, |_| true);
}
self.publish_group(&g);
return Ok(());
}
slot => slot,
}
}
};
match slot {
Slot::Failed(e) => Err(e),
Slot::Insert(group, first) => {
self.insert_and_publish(&me.batch, &group, first);
Ok(())
}
Slot::Wait(group) => {
self.wait_published(group.last);
Ok(())
}
Slot::InsertAll(..) => unreachable!("only the leader inserts a whole group"),
}
}
fn write_group(&self, group: &[Arc<Pending>]) -> Result<(Arc<Group>, Vec<u64>)> {
self.check()?;
let mut commit = self.commit.lock();
self.throttle()?;
if self.sv.load().mem.approximate_size() >= self.write_buffer_size {
self.switch_memtable(&mut commit)?;
}
let CommitState { wal, last_seq, buf } = &mut *commit;
let wal_no = file_no_u32(wal.number)?;
buf.clear();
let mut seq = *last_seq;
let mut firsts = Vec::with_capacity(group.len());
for pending in group {
firsts.push(seq + 1);
let header = pending.batch.header(seq + 1);
encode_record_parts(buf, wal_no, &[&header, pending.batch.ops_bytes()]);
seq += pending.batch.len() as u64;
}
if seq > MAX_SEQ {
return Err(Error::InvalidArgument("sequence numbers exhausted".into()));
}
let sync = self.durability == Durability::Full || group.iter().any(|p| p.sync);
self.io(|| Ok(wal.file.append(buf)?))?;
wal.written.fetch_add(buf.len() as u64, Ordering::Relaxed);
add(&self.counters.wal_bytes, buf.len() as u64);
add(&self.counters.commit_groups, 1);
add(&self.counters.committed_batches, group.len() as u64);
if sync {
add(&self.counters.wal_fsyncs, 1);
self.sync_wal_file(wal)?;
} else {
wal.dirty.store(true, Ordering::Release);
}
if buf.capacity() > 2 * MAX_GROUP_BYTES {
*buf = Vec::new();
}
let first = *last_seq + 1;
*last_seq = seq;
let mem = self.sv.load().mem.clone();
Ok((
Arc::new(Group {
mem,
first,
last: seq,
remaining: AtomicUsize::new(group.len()),
}),
firsts,
))
}
fn insert_and_publish(&self, batch: &WriteBatch, group: &Group, first: u64) {
apply(&group.mem, batch.ops(), first, |_| true);
if group.remaining.fetch_sub(1, Ordering::AcqRel) == 1 {
self.publish_group(group);
} else {
self.wait_published(group.last);
}
}
fn publish_group(&self, group: &Group) {
let mut done = self.publish.lock();
done.insert(group.first, group.last);
let mut visible = self.visible_seq.load(Ordering::Acquire);
let start = visible;
while let Some(last) = done.remove(&(visible + 1)) {
visible = last;
}
if visible == start {
drop(done);
self.wait_published(group.last);
return;
}
self.visible_seq.store(visible, Ordering::Release);
drop(done);
self.publish_cv.notify_all();
}
fn wait_published(&self, seq: u64) {
if spin_until(|| self.visible_seq.load(Ordering::Acquire) >= seq) {
return;
}
let mut published = self.publish.lock();
while self.visible_seq.load(Ordering::Acquire) < seq {
self.publish_cv.wait(&mut published);
}
}
fn throttle(&self) -> Result<()> {
let l0 = self.sv.load().version.levels[0].len();
if l0 >= self.l0_stop {
let start = Instant::now();
let _count = Defer(|| add(&self.counters.l0_stop_micros, micros_since(start)));
let mut bg = self.bg.lock();
while self.sv.load().version.levels[0].len() >= self.l0_stop {
self.check()?;
if let Some(e) = &bg.flush_error {
return Err(e.clone());
}
if bg.shutdown {
return Err(Error::InvalidArgument("the database is closing".into()));
}
self.bg_cv.wait(&mut bg);
}
} else if l0 >= self.l0_slowdown {
add(&self.counters.l0_slowdowns, 1);
std::thread::sleep(Duration::from_millis(1));
}
Ok(())
}
fn switch_memtable(&self, commit: &mut CommitState) -> Result<()> {
{
let start = Instant::now();
let _count = Defer(|| add(&self.counters.memtable_wait_micros, micros_since(start)));
let mut bg = self.bg.lock();
while self.sv.load().imms.len() >= self.max_imms {
self.check()?;
if let Some(e) = &bg.flush_error {
return Err(e.clone());
}
if bg.shutdown {
return Err(Error::InvalidArgument("the database is closing".into()));
}
self.bg_cv.wait(&mut bg);
}
}
self.wait_published(commit.last_seq);
let written = commit.wal.written.load(Ordering::Relaxed);
self.counters
.largest_wal_bytes
.fetch_max(written, Ordering::Relaxed);
let mut end = Vec::new();
log::encode_end(&mut end, file_no_u32(commit.wal.number)?);
self.io(|| Ok(commit.wal.file.append(&end)?))?;
commit.wal.dirty.store(true, Ordering::Release);
let number = self.allocate_file();
let new_path = self.dir.join(wal_name(number));
let file = {
let mut recycled = self.recycled_wals.lock();
match recycled.pop() {
Some(old) => {
self.io(|| Ok(self.fs.reuse(&self.dir.join(wal_name(old)), &new_path)?))?
}
None => self.io(|| Ok(self.fs.create(&new_path)?))?,
}
};
let mut header = Vec::new();
log::encode_header(&mut header, file_no_u32(number)?, commit.last_seq);
self.io(|| Ok(file.append(&header)?))?;
{
let mut barrier = self.barrier.lock();
barrier.older.push(commit.wal.clone());
barrier.dir_dirty = true;
}
let wal = Arc::new(WalFile {
file,
number,
dirty: AtomicBool::new(false),
written: AtomicU64::new(0),
});
commit.wal = wal.clone();
*self.current_wal.lock() = wal;
self.install(|sv| SuperVersion {
mem: Arc::new(Memtable::new(number)),
imms: std::iter::once(sv.mem.clone())
.chain(sv.imms.iter().cloned())
.collect(),
version: sv.version.clone(),
});
self.notify_background();
Ok(())
}
fn flush_loop(&self) {
loop {
let imm = {
let mut bg = self.bg.lock();
loop {
if bg.shutdown || self.poisoned.load(Ordering::Acquire) {
return;
}
if let Some(imm) = self.sv.load().imms.last() {
break imm.clone();
}
self.bg_cv.wait(&mut bg);
}
};
if let Err(e) = self.flush_memtable(&imm) {
self.poison_because(&e);
self.bg.lock().flush_error = Some(e);
self.notify_background();
return;
}
}
}
fn log_and_apply(
&self,
change: impl FnOnce(&SuperVersion) -> Result<Option<VersionChange>>,
after_log: impl FnOnce(),
) -> Result<bool> {
let _guard = self.sv_lock.lock();
let current = self.sv.load_full();
let Some(mut change) = change(¤t)? else {
return Ok(false);
};
tidy(&mut change);
{
let mut structure = self.structure.lock();
let next_file = structure.state.next_file;
change.edits.push(Edit::NextFile(next_file));
self.append_edits(&mut structure, &change.edits)?;
}
after_log();
self.sv.store(Arc::new(change.sv));
change.retired.iter().for_each(|f| f.mark_obsolete());
crate::version::retry_deletes(self.fs.as_ref(), &self.graveyard);
Ok(true)
}
fn live_cfs(&self) -> HashSet<u32> {
self.structure.lock().state.cfs.keys().copied().collect()
}
fn flush_memtable(&self, imm: &Arc<Memtable>) -> Result<()> {
let tombstones = imm.tombstones();
let (next_wal, watermark) = {
let sv = self.sv.load();
let at = sv
.imms
.iter()
.position(|m| Arc::ptr_eq(m, imm))
.expect("the memtable being flushed");
let next_wal = if at == 0 {
sv.mem.first_wal()
} else {
sv.imms[at - 1].first_wal()
};
let watermark = sv
.version
.tombstones
.iter()
.chain(&tombstones)
.map(|t| t.seq)
.max()
.unwrap_or(0);
(next_wal, watermark)
};
let live = self.live_cfs();
let files = flush::write_tables(
self.fs.as_ref(),
&self.dir,
imm,
&|cf| live.contains(&cf),
self.target_file_size,
watermark,
|| self.allocate_file(),
)?;
add(&self.counters.flushes, 1);
add(
&self.counters.flushed_bytes,
files.iter().map(|(_, p)| p.size).sum(),
);
let tables: Vec<Arc<TableFile>> = files
.iter()
.map(|(number, props)| {
Arc::new(
TableFile::new(
self.fs.clone(),
self.block_cache.clone(),
&self.dir,
*number,
props.clone(),
)
.with_graveyard(self.graveyard.clone()),
)
})
.collect();
for table in &tables {
table.reader()?;
}
self.log_and_apply(
|sv| {
let mut edits: Vec<Edit> = files
.iter()
.map(|(number, props)| Edit::AddFile {
level: 0,
number: *number,
props: props.clone(),
})
.collect();
edits.extend(tombstones.iter().cloned().map(Edit::AddRangeTombstone));
edits.push(Edit::LogNumber(next_wal));
edits.push(Edit::LastSeq(imm.max_seq()));
let mut version = (*sv.version).clone();
version.levels[0].extend(tables);
version.sort_level0();
version.tombstones.extend(tombstones.iter().cloned());
Ok(Some(VersionChange {
edits,
sv: SuperVersion {
mem: sv.mem.clone(),
imms: sv
.imms
.iter()
.filter(|m| !Arc::ptr_eq(m, imm))
.cloned()
.collect(),
version: Arc::new(version),
},
retired: Vec::new(),
}))
},
|| {
self.barrier.lock().older.retain(|w| w.number >= next_wal);
let mut recycled = self.recycled_wals.lock();
for name in self.fs.list(&self.dir).unwrap_or_default() {
let Some(no) = parse_wal_name(&name).filter(|no| *no < next_wal) else {
continue;
};
if recycled.contains(&no) {
continue;
}
if recycled.len() < self.recycle_wal_files {
recycled.push(no);
} else {
self.fs.remove(&self.dir.join(&name)).ok();
}
}
},
)?;
self.notify_background();
Ok(())
}
fn reserve_threads(&self, want: usize) -> usize {
let mut used = self.compaction_threads_used.lock();
let granted = want
.min(self.compaction_threads.saturating_sub(*used))
.max(1);
*used += granted;
granted
}
fn release_threads(&self, n: usize) {
let mut used = self.compaction_threads_used.lock();
*used = used.saturating_sub(n);
}
fn claim(
&self,
choose: impl FnOnce(&Version, compaction::Busy<'_>) -> Option<compaction::Compaction>,
) -> Option<(compaction::Compaction, Arc<Version>)> {
let mut busy = self.compacting.lock();
let version = self.sv.load().version.clone();
let picked = choose(&version, &|n| busy.contains(&n))?;
busy.extend(picked.all_inputs().map(|(_, f)| f.number));
Some((picked, version))
}
fn release(&self, c: &compaction::Compaction) {
let mut busy = self.compacting.lock();
for (_, f) in c.all_inputs() {
busy.remove(&f.number);
}
drop(busy);
self.notify_background();
}
fn compaction_loop(&self) {
loop {
let (picked, version) = {
let mut bg = self.bg.lock();
loop {
if bg.shutdown || self.poisoned.load(Ordering::Acquire) {
return;
}
let cursors = self.cursors.lock().clone();
if let Some(claimed) =
self.claim(|v, busy| compaction::pick(v, &cursors, &self.compaction, busy))
{
break claimed;
}
if self
.bg_cv
.wait_for(&mut bg, Duration::from_secs(1))
.timed_out()
{
crate::version::retry_deletes(self.fs.as_ref(), &self.graveyard);
}
}
};
let outcome = self.compact(&picked, &version);
self.release(&picked);
if let Err(e) = outcome {
self.poison_because(&e);
self.bg.lock().flush_error = Some(e);
self.notify_background();
return;
}
}
}
fn compact(&self, c: &compaction::Compaction, version: &Version) -> Result<()> {
let start = Instant::now();
let output_level = c.output_level;
let outputs = if c.is_trivial_move() {
Vec::new()
} else {
let live = self.live_cfs();
let threads = self.reserve_threads(self.compaction.subcompactions);
let cfg = compaction::Config {
subcompactions: threads,
..self.compaction
};
let outcome = compaction::run(
c,
version,
&|cf| live.contains(&cf),
self.fs.as_ref(),
&self.dir,
&cfg,
&|| self.allocate_file(),
);
self.release_threads(threads);
outcome?
};
let new_tables: Vec<Arc<TableFile>> = if c.is_trivial_move() {
vec![c.inputs[0].clone()]
} else {
outputs
.iter()
.map(|(number, props)| {
Arc::new(
TableFile::new(
self.fs.clone(),
self.block_cache.clone(),
&self.dir,
*number,
props.clone(),
)
.with_graveyard(self.graveyard.clone()),
)
})
.collect()
};
for table in &new_tables {
table.reader()?;
}
let applied = self.log_and_apply(
|sv| {
let current = c
.all_inputs()
.all(|(level, f)| sv.version.levels[level].iter().any(|g| Arc::ptr_eq(g, f)));
if !current {
return Ok(None);
}
let mut edits: Vec<Edit> = c
.all_inputs()
.map(|(level, f)| Edit::DeleteFile {
level: level as u32,
number: f.number,
})
.collect();
edits.extend(new_tables.iter().map(|f| Edit::AddFile {
level: output_level as u32,
number: f.number,
props: f.props.clone(),
}));
let mut version = (*sv.version).clone();
for (level, f) in c.all_inputs() {
version.levels[level].retain(|g| !Arc::ptr_eq(g, f));
}
version.levels[output_level].extend(new_tables.iter().cloned());
version.levels[output_level].sort_by(by_smallest);
let retired = if c.is_trivial_move() {
Vec::new()
} else {
c.all_inputs().map(|(_, f)| f.clone()).collect()
};
Ok(Some(VersionChange {
edits,
sv: SuperVersion {
mem: sv.mem.clone(),
imms: sv.imms.clone(),
version: Arc::new(version),
},
retired,
}))
},
|| {},
)?;
add(&self.counters.compactions, 1);
add(&self.counters.compaction_micros, micros_since(start));
if !c.is_trivial_move() {
add(
&self.counters.compaction_read_bytes,
c.all_inputs().map(|(_, f)| f.props.size).sum(),
);
add(
&self.counters.compaction_written_bytes,
outputs.iter().map(|(_, p)| p.size).sum(),
);
}
if !applied && !c.is_trivial_move() {
new_tables.iter().for_each(|t| t.mark_obsolete());
}
if c.output_level > c.level {
self.cursors.lock()[c.level] = c.largest();
}
self.notify_background();
Ok(())
}
fn sync_loop(&self, interval: Duration) {
loop {
{
let mut bg = self.bg.lock();
if !bg.shutdown {
self.bg_cv.wait_for(&mut bg, interval);
}
if bg.shutdown {
return;
}
}
if self.poisoned.load(Ordering::Acquire) {
return;
}
let wal = self.current_wal.lock().clone();
let behind = !self.barrier.lock().older.is_empty();
if (wal.dirty.load(Ordering::Acquire) || behind) && self.sync_wal_file(&wal).is_err() {
return;
}
}
}
}
struct LeaderGuard<'a> {
inner: &'a Inner,
group: &'a [Arc<Pending>],
me: &'a Arc<Pending>,
}
impl LeaderGuard<'_> {
fn finish(self, written: Result<(Arc<Group>, Vec<u64>)>) -> Slot {
let mut mine = None;
let leader_inserts = self.group.len() > MAX_PARALLEL_INSERT;
let mut queue = self.inner.queue.lock();
for (i, pending) in self.group.iter().enumerate() {
let slot = match &written {
Ok((group, firsts)) if leader_inserts => {
if Arc::ptr_eq(pending, self.me) {
Slot::InsertAll(group.clone(), firsts.clone())
} else {
Slot::Wait(group.clone())
}
}
Ok((group, firsts)) => Slot::Insert(group.clone(), firsts[i]),
Err(e) => Slot::Failed(e.clone()),
};
if Arc::ptr_eq(pending, self.me) {
mine = Some(slot);
} else {
*pending.slot.lock() = Some(slot);
pending.ready.store(true, Ordering::Release);
}
}
queue.leader_active = false;
self.inner.leader_busy.store(false, Ordering::Release);
drop(queue);
self.inner.queue_cv.notify_all();
std::mem::forget(self);
mine.expect("the leader is in its own group")
}
}
impl Drop for LeaderGuard<'_> {
fn drop(&mut self) {
self.inner.poison();
let mut queue = self.inner.queue.lock();
for pending in self.group {
if !Arc::ptr_eq(pending, self.me) {
*pending.slot.lock() = Some(Slot::Failed(self.inner.poisoned_error()));
pending.ready.store(true, Ordering::Release);
}
}
queue.leader_active = false;
self.inner.leader_busy.store(false, Ordering::Release);
drop(queue);
self.inner.queue_cv.notify_all();
}
}