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::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_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, 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>>>,
manifest_in_place: Option<Vec<u8>>,
manifest_synced: bool,
}
mod flush;
pub(crate) use flush::flush_barrier;
impl Table {
pub(crate) fn new(
dir: &str,
schema: SchemaDescriptor,
recovery_source: RecoverySource,
ram_tier_bytes: usize,
capacity: Option<u64>,
) -> 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_in_place = loaded.as_ref().map(super::manifest::encode);
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(ram_tier_bytes),
shard_index: ShardIndex::open(dir, schema, capacity, 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),
manifest_in_place,
manifest_synced: false,
})
}
pub(crate) fn schema(&self) -> &SchemaDescriptor {
&self.shard_index.schema
}
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 loaded_mark(&self) -> Option<u64> {
self.loaded_mark
}
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(&mut self, batch: Batch) -> Result<(), StorageError> {
self.ingest_run(Rc::new(batch.into_consolidated().trimmed()))
}
pub(crate) fn ingest_run(&mut self, run: Rc<Batch>) -> Result<(), StorageError> {
if run.is_empty() {
return Ok(());
}
self.cached_full_scan.set(None);
let ingested = run.total_bytes();
self.memtable.push_run(run, &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 read = self.shard_index.maintain(budget)?;
self.upkeep_overdraft += read.saturating_sub(budget);
Ok(())
}
pub(crate) fn settle(&mut self) -> Result<(), StorageError> {
if self.held_in_ram {
return Ok(());
}
self.shard_index.maintain(u64::MAX).map(drop)
}
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, cut: Cut) -> impl Iterator<Item = Run> + '_ {
self.mem_runs(None, cut)
.chain(self.shard_index.shard_arcs(cut == Cut::Now).map(Run::Shard))
}
pub(crate) fn open_cursor(&self, cut: Cut) -> ReadCursor {
let cap = self.mem_run_count() + self.shard_index.shard_count();
from_runs(self.runs(cut), self.shard_index.schema, cap)
}
pub(crate) fn cursor_between(&self, first: &[u8], last: &[u8], cut: Cut) -> ReadCursor {
if first.is_empty() {
return self.open_cursor(cut);
}
let stride = self.shard_index.schema.pk_stride();
let band = 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");
self.range_cursor(Some(band), cut)
}
pub(crate) fn range_cursor(&self, range: Option<(PkBuf, Option<PkBuf>)>, cut: Cut) -> ReadCursor {
let schema = self.shard_index.schema;
let Some((start, end)) = range else {
return empty_cursor(schema);
};
let stride = schema.pk_stride();
let hi = end.unwrap_or_else(|| PkBuf::max(stride));
debug_assert!(
start.pk_bytes().len() == stride && hi.pk_bytes().len() == stride,
"range_cursor: a bound is not pk_stride wide",
);
let runs = self.mem_runs(Some((start, hi)), cut).chain(
self.shard_index
.shard_arcs_in_range(start, hi, cut == Cut::Now)
.map(Run::Shard),
);
let cap = self.mem_run_count() + self.shard_index.narrow_range_shards();
let end = end.as_ref().map(PkBuf::pk_bytes);
from_runs_in_band(runs, schema, cap, start.pk_bytes(), end)
}
pub(crate) fn gather(&self, keys: PkKeys, cut: Cut) -> PkSetGather {
let cursor = match keys.bounds() {
Some((first, last)) => self.cursor_between(first, last, cut),
None => empty_cursor(self.shard_index.schema),
};
PkSetGather::over(cursor, keys)
}
pub(crate) fn full_scan(&self) -> Rc<Batch> {
let rc = self
.cached_full_scan
.take()
.unwrap_or_else(|| self.open_cursor(Cut::Now).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.shard_arcs(true).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;