use std::cell::Cell;
use std::fs;
use std::io;
use std::rc::Rc;
use gnitz_wire::PkKeys;
use super::manifest::Manifest;
use super::run_set::RunSet;
use super::shard_index::{ShardBudget, ShardIndex};
use gnitz_wire::PkBuf;
use gnitz_zset::repr::pk_group_end;
use gnitz_zset::repr::Batch;
#[cfg(test)]
use gnitz_zset::repr::MappedShard;
use gnitz_zset::repr::StorageError;
use gnitz_zset::repr::{empty_cursor, from_runs, from_runs_at, from_runs_in_band, PkSetGather, ReadCursor};
use gnitz_zset::repr::{first_live_payload_group, Run, StoredRow};
use gnitz_zset::schema::key::{key_range_between_cuts, probe_key, KeyCut};
use gnitz_zset::schema::SchemaDescriptor;
pub(super) const MEMTABLE_BYTES: usize = 192 << 10;
const SEAL_FOLD_RATIO: usize = 64;
const UPKEEP_LEVY: u64 = 64;
pub(crate) const DEFAULT_RAM_TIER_BYTES: usize = 32 << 20;
#[derive(Clone, Copy)]
pub(crate) struct StoreBudgets {
ram_tier_bytes: usize,
shard: ShardBudget,
}
#[cfg(test)]
impl Default for StoreBudgets {
fn default() -> Self {
StoreBudgets::new(DEFAULT_RAM_TIER_BYTES)
}
}
impl StoreBudgets {
pub(crate) fn new(ram_tier_bytes: usize) -> Self {
StoreBudgets {
ram_tier_bytes,
shard: ShardBudget::Unbounded,
}
}
pub(crate) fn bounded(self, capacity_bytes: Option<u64>) -> Self {
StoreBudgets {
shard: capacity_bytes.map_or(ShardBudget::Unbounded, ShardBudget::Dehydrate),
..self
}
}
pub(crate) fn delta(self, budget: u64) -> Self {
StoreBudgets { shard: ShardBudget::Drop(budget), ..self }
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum RecoverySource {
SalReplay,
Rederive {
resume_at: Option<u64>,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Cut {
Now,
Sealed,
}
pub(crate) struct Table {
pending: RunSet,
memtable: RunSet,
ram_tier: RunSet,
shard_index: ShardIndex,
rederived: bool,
loaded_mark: Option<u64>,
caller_record: Vec<u8>,
held_in_ram: bool,
upkeep_overdraft: u64,
live_row_scratch: Cell<Vec<StoredRow>>,
cached_full_scan: Cell<Option<Rc<Batch>>>,
durable_manifest: Option<Vec<u8>>,
}
mod flush;
pub(crate) use flush::flush_barrier;
impl Table {
pub(crate) fn new(
dir: &str,
schema: SchemaDescriptor,
recovery_source: RecoverySource,
budgets: StoreBudgets,
) -> Result<Self, StorageError> {
let created = match fs::create_dir(dir) {
Ok(()) => true,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(dir)?;
true
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => false,
Err(e) => return Err(e.into()),
};
let rederived = matches!(recovery_source, RecoverySource::Rederive { .. });
let loaded = match recovery_source {
_ if created => None,
RecoverySource::Rederive { resume_at: None } => None,
RecoverySource::Rederive { resume_at: Some(want) } => super::manifest::read_at(dir, want)?,
RecoverySource::SalReplay => super::manifest::read(dir)?,
};
if loaded.is_none() && rederived && !created {
super::manifest::unlink(dir)?;
}
let loaded_mark = loaded.as_ref().map(|m| m.checkpoint_mark);
let Manifest { caller_record, shards, .. } = loaded.unwrap_or_default();
let skip_pk_filter = rederived;
Ok(Table {
pending: RunSet::new(MEMTABLE_BYTES),
memtable: RunSet::new(MEMTABLE_BYTES),
ram_tier: RunSet::new(budgets.ram_tier_bytes),
shard_index: ShardIndex::open(dir, schema, budgets.shard, skip_pk_filter, &shards)?,
rederived,
loaded_mark,
caller_record,
held_in_ram: false,
upkeep_overdraft: 0,
live_row_scratch: Cell::new(Vec::new()),
cached_full_scan: Cell::new(None),
durable_manifest: None,
})
}
pub(crate) fn schema(&self) -> &SchemaDescriptor {
&self.shard_index.schema
}
pub(crate) fn dropped_max(&self) -> PkBuf {
self.shard_index.dropped_max()
}
pub(crate) fn has_skeleton_rows(&self) -> bool {
self.shard_index.has_skeleton_shard()
}
pub(crate) fn is_rederived(&self) -> bool {
self.rederived
}
pub(crate) fn recovery_source(&self) -> RecoverySource {
match self.rederived {
true => RecoverySource::Rederive { resume_at: self.loaded_mark },
false => RecoverySource::SalReplay,
}
}
pub(crate) fn resumed_from_checkpoint(&self) -> bool {
self.rederived && self.loaded_mark.is_some()
}
pub(crate) fn append_terminal_run(&mut self, run: &Batch) -> Result<(), StorageError> {
self.cached_full_scan.set(None);
self.shard_index.append_terminal_run(run)
}
pub(crate) fn verify_shards(&self) -> Result<(), StorageError> {
self.shard_index.verify_shards()
}
pub(crate) fn swap_schema(&mut self, schema: SchemaDescriptor) -> Result<(), StorageError> {
self.shard_index.swap_schema(schema)?;
self.pending.widen_runs(&schema);
self.memtable.widen_runs(&schema);
self.ram_tier.widen_runs(&schema);
self.cached_full_scan.set(None);
Ok(())
}
pub(crate) fn hold_in_ram(&mut self) {
self.held_in_ram = true;
}
#[inline]
pub(crate) fn ingest_owned_batch(&mut self, batch: Batch) -> Result<(), StorageError> {
self.push_memtable(batch.into_consolidated())
}
pub(crate) fn ingest_borrowed_batch(&mut self, batch: &Batch) -> Result<(), StorageError> {
self.push_memtable(batch.to_consolidated())
}
fn push_memtable(&mut self, batch: Batch) -> Result<(), StorageError> {
if batch.is_empty() {
return Ok(());
}
self.cached_full_scan.set(None);
let ingested = batch.total_bytes();
self.memtable.push(batch, &self.shard_index.schema);
if self.memtable.is_full() {
self.fold_to_ram()?;
}
self.upkeep(ingested)
}
pub(crate) fn clear(&mut self) {
self.cached_full_scan.set(None);
self.pending.clear();
self.memtable.clear();
self.ram_tier.clear();
self.shard_index.clear();
}
pub(crate) fn ingest_pending(&mut self, batch: Batch) {
let batch = batch.into_consolidated();
if batch.is_empty() {
return;
}
self.cached_full_scan.set(None);
self.pending.push(batch, &self.shard_index.schema);
}
pub(crate) fn has_pending(&self) -> bool {
self.pending.len() > 0 || self.shard_index.pending_arcs().next().is_some()
}
pub(crate) fn seal(&mut self) -> Result<Option<Batch>, StorageError> {
let schema = self.shard_index.schema;
let run = self.pending.drain_into(&mut self.memtable, &schema);
if run
.as_ref()
.is_some_and(|run| run.len() * SEAL_FOLD_RATIO >= self.memtable.row_count())
{
self.memtable.fold(&schema);
}
let shards: Vec<Run> = self.shard_index.pending_arcs().map(Run::Shard).collect();
let delta = match shards.is_empty() {
true => run,
false => {
let cap = shards.len() + 1;
let mem = run.into_iter().map(Run::Mem);
Some(from_runs(mem.chain(shards), schema, cap).materialize())
}
};
self.shard_index.seal_pending();
if self.memtable.is_full() {
self.fold_to_ram()?;
}
self.upkeep(delta.as_ref().map_or(0, |delta| delta.total_bytes()))?;
Ok(delta.map(Rc::unwrap_or_clone).filter(|delta| !delta.is_empty()))
}
fn upkeep(&mut self, ingested: usize) -> Result<(), StorageError> {
if self.held_in_ram || !self.shard_index.owed() {
return Ok(());
}
let levy = ingested as u64 * UPKEEP_LEVY;
let budget = levy.saturating_sub(self.upkeep_overdraft);
self.upkeep_overdraft = self.upkeep_overdraft.saturating_sub(levy);
let done = self.shard_index.maintain(budget)?;
self.upkeep_overdraft += done.read.saturating_sub(budget);
self.evicted(done.evicted);
Ok(())
}
pub(crate) fn settle(&mut self) -> Result<(), StorageError> {
if self.held_in_ram {
return Ok(());
}
let done = self.shard_index.maintain(u64::MAX)?;
self.evicted(done.evicted);
Ok(())
}
fn evicted(&mut self, evicted: bool) {
if evicted {
self.cached_full_scan.set(None);
}
}
pub(crate) fn checkpoint_mark(&self) -> u64 {
self.loaded_mark.unwrap_or(0)
}
pub(crate) fn set_caller_record(&mut self, record: Vec<u8>) {
self.caller_record = record;
}
fn ram_tiers(&self, cut: Cut) -> impl Iterator<Item = &RunSet> + Clone {
[&self.pending, &self.memtable, &self.ram_tier]
.into_iter()
.skip(usize::from(cut == Cut::Sealed))
}
fn mem_runs(&self, bound: Option<(PkBuf, PkBuf)>, cut: Cut) -> impl Iterator<Item = Run> + '_ {
self.ram_tiers(cut)
.flat_map(move |set| set.runs_overlapping(bound))
.cloned()
.map(Run::Mem)
}
fn mem_run_count(&self) -> usize {
self.ram_tiers(Cut::Now).map(|s| s.len()).sum()
}
pub(crate) fn runs(&self) -> impl Iterator<Item = Run> + '_ {
self.mem_runs(None, Cut::Now)
.chain(self.shard_index.all_shard_arcs_iter().map(Run::Shard))
}
pub(crate) fn open_cursor(&self) -> ReadCursor {
let cap = self.mem_run_count() + self.shard_index.shard_count();
from_runs(self.runs(), self.shard_index.schema, cap)
}
pub(crate) fn cursor_for_keys(&self, keys: &Batch, cut: Cut) -> ReadCursor {
debug_assert!(keys.is_consolidated() && !keys.is_empty());
self.cursor_between(keys.get_pk_bytes(0), keys.get_pk_bytes(keys.len() - 1), cut)
}
pub(crate) fn cursor_between(&self, first: &[u8], last: &[u8], cut: Cut) -> ReadCursor {
let (runs, cap, start) = self.runs_over_prefixes(first, last, cut);
from_runs_at(runs, self.shard_index.schema, cap, start.pk_bytes())
}
fn runs_over_prefixes<'a>(
&'a self,
first: &[u8],
last: &[u8],
cut: Cut,
) -> (impl Iterator<Item = Run> + 'a, usize, PkBuf) {
let stride = self.shard_index.schema.pk_stride();
let (start, end) = key_range_between_cuts(KeyCut::min_of(first), KeyCut::above(last), stride)
.expect("`first <= last`, so the band from one's group to the other's holds a key");
let (runs, cap) = self.runs_in_range(start, end, cut);
(runs, cap, start)
}
fn runs_in_range(&self, lo: PkBuf, end: Option<PkBuf>, cut: Cut) -> (impl Iterator<Item = Run> + '_, usize) {
let stride = self.shard_index.schema.pk_stride();
let hi = end.unwrap_or_else(|| PkBuf::max(stride));
debug_assert!(
lo.pk_bytes().len() == stride && hi.pk_bytes().len() == stride,
"runs_in_range: a bound is not pk_stride wide",
);
let runs = self.mem_runs(Some((lo, hi)), cut).chain(
self.shard_index
.shard_arcs_in_range(lo, hi, cut == Cut::Now)
.map(Run::Shard),
);
(runs, self.mem_run_count() + self.shard_index.narrow_range_shards())
}
pub(crate) fn range_cursor(&self, range: Option<(PkBuf, Option<PkBuf>)>) -> ReadCursor {
let Some((start, end)) = range else {
return empty_cursor(self.shard_index.schema);
};
let (runs, cap) = self.runs_in_range(start, end, Cut::Now);
let end = end.as_ref().map(PkBuf::pk_bytes);
from_runs_in_band(runs, self.shard_index.schema, cap, start.pk_bytes(), end)
}
pub(crate) fn gather(&self, keys: PkKeys, cut: Cut) -> PkSetGather {
let schema = self.shard_index.schema;
let Some((first, last)) = keys.bounds() else {
return PkSetGather::over_runs(std::iter::empty(), schema, 0, keys);
};
let (runs, cap, _) = self.runs_over_prefixes(first, last, cut);
PkSetGather::over_runs(runs, schema, cap, keys)
}
pub(crate) fn full_scan(&self) -> Rc<Batch> {
let rc = self
.cached_full_scan
.take()
.unwrap_or_else(|| self.open_cursor().materialize());
self.cached_full_scan.set(Some(Rc::clone(&rc)));
rc
}
pub(crate) fn estimated_rows(&self) -> usize {
self.ram_tiers(Cut::Now).map(|s| s.row_count()).sum::<usize>() + self.shard_index.total_rows()
}
#[cfg(test)]
pub(crate) fn all_shard_arcs(&self) -> Vec<Rc<MappedShard>> {
self.shard_index.all_shard_arcs_iter().collect()
}
#[cfg(test)]
pub(crate) fn level_shape(&self) -> (usize, [usize; 2]) {
self.shard_index.level_shape()
}
#[inline]
pub(crate) fn has_pk_bytes(&self, key: &[u8]) -> bool {
let mut w: i64 = 0;
self.for_each_pk_candidate(key, |run, row| w += run.get_weight(row));
w > 0
}
fn for_each_pk_candidate(&self, key: &[u8], mut f: impl FnMut(&Run, usize)) {
let mut visit = |run: Run, start: usize| {
for row in start..pk_group_end(&run, start) {
f(&run, row);
}
};
let fingerprint = probe_key(key);
for set in self.ram_tiers(Cut::Now) {
set.find_pk_bytes(key, fingerprint, |batch, start| {
visit(Run::Mem(Rc::clone(batch)), start)
});
}
self.shard_index.find_pk_bytes(key, fingerprint, |shard, start| {
visit(Run::Shard(Rc::clone(shard)), start)
});
}
pub(crate) fn live_row_at(&self, key: &[u8]) -> (i64, Option<StoredRow>) {
let mut pool = self.live_row_scratch.take();
let mut total_w: i64 = 0;
self.for_each_pk_candidate(key, |run, row| {
total_w += run.get_weight(row);
pool.push(StoredRow::new(run.clone(), row));
});
let mut row = None;
if total_w > 0 {
let schema = &self.shard_index.schema;
let winner = first_live_payload_group(schema, &pool);
debug_assert!(
winner.is_some(),
"positive net PK weight implies a positive payload group"
);
row = winner.map(|i| pool.swap_remove(i));
}
pool.clear();
self.live_row_scratch.set(pool);
(total_w, row)
}
}
#[cfg(test)]
#[path = "tests/table.rs"]
mod tests;
#[cfg(test)]
#[path = "benches/table.rs"]
mod bench;