use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use anyhow::{Context, Result, anyhow, bail};
use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
use znippy_common::arrow::buffer::Buffer;
use crate::index_layout::{IndexEntry, IndexRow, ObjType, ObjectIndex, OneTableFourColumns};
const OBJECTS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("objects");
const META: TableDefinition<&str, u64> = TableDefinition::new("meta");
const META_ARRIVAL_SEQ: &str = "arrival_seq";
const META_TAIL_EPOCH: &str = "tail_epoch";
const META_TAIL_FORMAT: &str = "tail_format";
const TAIL_FORMAT: u64 = 1;
const TAIL_ROW_BYTES: usize = 41;
pub const TAIL_ORDINAL_BIT: u32 = 0x8000_0000;
pub fn is_tail_row(row: &IndexRow) -> bool {
is_tail_ordinal(row.ordinal)
}
#[inline]
pub fn is_tail_ordinal(ordinal: u32) -> bool {
ordinal & TAIL_ORDINAL_BIT != 0
}
fn tail_ordinal(seq: u64) -> u32 {
TAIL_ORDINAL_BIT | (seq as u32 & !TAIL_ORDINAL_BIT)
}
fn encode_row(seq: u64, e: &IndexEntry) -> [u8; TAIL_ROW_BYTES] {
let mut b = [0u8; TAIL_ROW_BYTES];
b[0..8].copy_from_slice(&seq.to_le_bytes());
b[8..16].copy_from_slice(&e.offset.to_le_bytes());
b[16..24].copy_from_slice(&e.len.to_le_bytes());
b[24] = e.obj_type.code();
b[25..33].copy_from_slice(&e.uncompressed_size.to_le_bytes());
b[33..41].copy_from_slice(&e.delta_base.to_le_bytes());
b
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TailRow {
seq: u64,
offset: u64,
len: u64,
obj_type: ObjType,
uncompressed_size: u64,
delta_base: u64,
}
fn decode_row(b: &[u8]) -> Result<TailRow> {
if b.len() != TAIL_ROW_BYTES {
bail!("tail row is {} bytes, expected {TAIL_ROW_BYTES}", b.len());
}
let u64_at = |i: usize| {
let mut w = [0u8; 8];
w.copy_from_slice(&b[i..i + 8]);
u64::from_le_bytes(w)
};
Ok(TailRow {
seq: u64_at(0),
offset: u64_at(8),
len: u64_at(16),
obj_type: ObjType::from_code(b[24])
.ok_or_else(|| anyhow!("tail row carries object type code {}", b[24]))?,
uncompressed_size: u64_at(25),
delta_base: u64_at(33),
})
}
impl TailRow {
fn as_index_row(&self) -> IndexRow {
IndexRow {
ordinal: tail_ordinal(self.seq),
offset: self.offset,
len: self.len,
obj_type: self.obj_type,
uncompressed_size: self.uncompressed_size,
delta_base: self.delta_base,
}
}
fn as_entry(&self, oid: &[u8]) -> IndexEntry {
IndexEntry {
oid: oid.to_vec(),
offset: self.offset,
len: self.len,
obj_type: self.obj_type,
uncompressed_size: self.uncompressed_size,
delta_base: self.delta_base,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RebuildTriggers {
pub tail_hits_per_row: f64,
pub min_tail_hits: u64,
pub tail_bytes: u64,
pub fold_ratio: f64,
}
impl RebuildTriggers {
pub const DEFAULT_TAIL_HITS_PER_ROW: f64 = 1.0;
pub const DEFAULT_MIN_TAIL_HITS: u64 = 4096;
pub const DEFAULT_TAIL_BYTES: u64 = 64 * 1024 * 1024;
pub const DEFAULT_FOLD_RATIO: f64 = 1.0;
pub fn miss_threshold(&self, projection_rows: u64) -> Option<u64> {
if self.min_tail_hits == 0 {
return None;
}
let scaled = (self.tail_hits_per_row * projection_rows as f64) as u64;
Some(scaled.max(self.min_tail_hits))
}
pub fn manual() -> Self {
Self {
tail_hits_per_row: 0.0,
min_tail_hits: 0,
tail_bytes: 0,
fold_ratio: 0.0,
}
}
}
impl Default for RebuildTriggers {
fn default() -> Self {
Self {
tail_hits_per_row: Self::DEFAULT_TAIL_HITS_PER_ROW,
fold_ratio: Self::DEFAULT_FOLD_RATIO,
min_tail_hits: Self::DEFAULT_MIN_TAIL_HITS,
tail_bytes: Self::DEFAULT_TAIL_BYTES,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebuildReason {
TailHits(u64),
TailBytes(u64),
Explicit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrdinalSpace {
pub raw: Vec<u8>,
pub folded_rows: u64,
pub tail_rows: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StackStats {
pub sealed_rows: u64,
pub total_rows: u64,
pub tail_hits: u64,
pub absent: u64,
pub unabsorbed_rows: u64,
pub tail_txns: u64,
pub tail_bytes: u64,
pub rebuilds: u64,
pub folds: u64,
pub persisted_loads: u64,
pub generation: u64,
}
pub struct ObjectReadStack<S: ObjectIndex = OneTableFourColumns> {
tail: Arc<Database>,
projection: RwLock<Arc<S>>,
triggers: RebuildTriggers,
total_rows: AtomicU64,
tail_hits: AtomicU64,
absent: AtomicU64,
tail_bytes: AtomicU64,
rebuilds: AtomicU64,
generation: AtomicU64,
unabsorbed_size: AtomicU64,
unabsorbed_types: [AtomicU64; 8],
unabsorbed_rows: AtomicU64,
tail_txns: AtomicU64,
persisted: Option<PathBuf>,
fold: Mutex<()>,
folds: AtomicU64,
persisted_loads: AtomicU64,
}
impl<S: ObjectIndex> ObjectReadStack<S> {
#[inline]
fn persists(&self) -> bool {
self.persisted.is_some()
}
pub fn open(tail_path: &Path, triggers: RebuildTriggers, cache_bytes: usize) -> Result<Self> {
let db = Database::builder()
.set_cache_size(cache_bytes)
.create(tail_path)
.with_context(|| format!("opening object tail at {}", tail_path.display()))?;
let persisted = S::persists().then(|| tail_path.with_extension("index"));
Self::from_db(db, triggers, persisted, Some(tail_path.to_path_buf()))
}
pub fn persisted_path(&self) -> Option<&Path> {
self.persisted.as_deref()
}
pub fn projection_name(&self) -> &'static str {
self.projection.read().expect("projection lock").name()
}
pub fn in_memory(triggers: RebuildTriggers) -> Result<Self> {
let db = Database::builder()
.create_with_backend(redb::backends::InMemoryBackend::new())
.context("creating an in-memory object tail")?;
Self::from_db(db, triggers, None, None)
}
fn from_db(
db: Database,
triggers: RebuildTriggers,
persisted: Option<PathBuf>,
tail_path: Option<PathBuf>,
) -> Result<Self> {
let w = db.begin_write()?;
let dropped = {
let _ = w.open_table(OBJECTS)?;
let mut meta = w.open_table(META)?;
let found = meta.get(META_TAIL_FORMAT)?.map(|v| v.value());
match found {
Some(TAIL_FORMAT) => 0,
None => {
meta.insert(META_TAIL_FORMAT, TAIL_FORMAT)?;
0
}
Some(other) => {
let mut objects = w.open_table(OBJECTS)?;
let n = objects.len()?;
objects.retain(|_, _| false)?;
meta.insert(META_TAIL_FORMAT, TAIL_FORMAT)?;
let epoch = meta.get(META_TAIL_EPOCH)?.map(|v| v.value()).unwrap_or(0);
meta.insert(META_TAIL_EPOCH, epoch + 1)?;
tracing::warn!(
target: "znippy.git",
found = other,
expected = TAIL_FORMAT,
rows = n,
"the object tail was written in a different layout; dropping it and \
re-indexing from the journal. Reads fall back to the pack until the \
drain catches up — slower, never wrong"
);
n
}
}
};
w.commit()?;
let _ = dropped;
let mut db = db;
let folded: Option<(Arc<S>, u128, u128)> = match persisted.as_deref() {
Some(path) => {
let _ = fs::remove_file(tmp_path(path));
load_persisted::<S>(path)
}
None => None,
};
let t_scan = std::time::Instant::now();
let mut entries = scan(&db)?;
let mut migrated = 0u64;
let (projection, unfolded, loaded) = match folded {
Some((folded, map_ms, build_ms)) => {
let dup: Vec<Vec<u8>> = entries
.iter()
.filter(|e| folded.lookup(&e.oid).is_some())
.map(|e| e.oid.clone())
.collect();
if !dup.is_empty() {
let w = db.begin_write()?;
{
let mut objects = w.open_table(OBJECTS)?;
for oid in &dup {
objects.remove(oid.as_slice())?;
}
}
w.commit()?;
tracing::warn!(
target: "znippy.git",
rows = dup.len(),
"the object tail held rows the persisted index already has — a fold \
was interrupted between writing the index and emptying the tail; \
the duplicates are dropped from the tail, nothing is lost"
);
entries.retain(|e| folded.lookup(&e.oid).is_none());
}
tracing::info!(
target: "znippy.git",
folded = folded.len(),
tail = entries.len(),
map_ms,
build_ms,
tail_ms = t_scan.elapsed().as_millis(),
stree_bytes = folded.resident_bytes().saturating_sub(folded.ipc_bytes()),
"mapped the persisted object index; the redb tail holds only what \
arrived after the last fold"
);
(folded, entries, 1u64)
}
None => {
let projection = Arc::new(S::build(&entries)?);
if let Some(path) = persisted.as_deref()
&& !entries.is_empty()
{
write_atomic(path, projection.persisted_bytes().expect("PERSISTS arm"))?;
let w = db.begin_write()?;
w.delete_table(OBJECTS)?;
let _ = w.open_table(OBJECTS)?;
w.commit()?;
migrated = 1;
tracing::info!(
target: "znippy.git",
rows = entries.len(),
path = %path.display(),
"folded the object tail into a persisted index on first open; the \
redb tail now holds only rows that arrive after this"
);
}
(projection, Vec::new(), 0u64)
}
};
compact_if_fragmented(&mut db, tail_path.as_deref());
let total = projection.len() as u64 + unfolded.len() as u64;
let mut unabsorbed_size = 0u64;
let mut unabsorbed_types = [0u64; 8];
let mut tail_bytes = 0u64;
for e in &unfolded {
unabsorbed_size += e.uncompressed_size;
unabsorbed_types[e.obj_type.code() as usize] += 1;
tail_bytes += e.len;
}
let db = Arc::new(db);
let stack = Self {
tail: db,
projection: RwLock::new(projection),
triggers,
total_rows: AtomicU64::new(total),
tail_hits: AtomicU64::new(0),
absent: AtomicU64::new(0),
tail_bytes: AtomicU64::new(tail_bytes),
rebuilds: AtomicU64::new(1),
generation: AtomicU64::new(1),
unabsorbed_size: AtomicU64::new(unabsorbed_size),
unabsorbed_types: unabsorbed_types.map(AtomicU64::new),
unabsorbed_rows: AtomicU64::new(unfolded.len() as u64),
tail_txns: AtomicU64::new(0),
persisted,
fold: Mutex::new(()),
folds: AtomicU64::new(migrated),
persisted_loads: AtomicU64::new(loaded),
};
Ok(stack)
}
#[inline]
pub fn projection_is_complete(&self) -> bool {
self.unabsorbed_rows.load(Ordering::Acquire) == 0
}
pub fn append(&self, entries: &[IndexEntry]) -> Result<Option<RebuildReason>> {
if entries.is_empty() {
return Ok(None);
}
let width = entries[0].oid.len();
if width != 20 && width != 32 {
bail!("oid width {width} is neither sha1 (20) nor sha256 (32)");
}
let guard = self.fold.lock().map_err(|_| anyhow!("the fold lock is poisoned"))?;
let w = self.tail.begin_write()?;
let mut added = 0u64;
let mut bytes = 0u64;
let mut added_size = 0u64;
let mut added_types = [0u64; 8];
{
let mut objects = w.open_table(OBJECTS)?;
let mut meta = w.open_table(META)?;
let mut seq = meta.get(META_ARRIVAL_SEQ)?.map(|v| v.value()).unwrap_or(0);
let projection = self.projection.read().expect("projection lock").clone();
for e in entries {
if e.oid.len() != width {
bail!(
"mixed oid widths in one append: {width} and {}",
e.oid.len()
);
}
let known: Option<TailRow> = match projection.lookup(&e.oid) {
Some(row) => Some(TailRow {
seq: 0,
offset: row.offset,
len: row.len,
obj_type: row.obj_type,
uncompressed_size: row.uncompressed_size,
delta_base: row.delta_base,
}),
None => match objects.get(e.oid.as_slice())? {
Some(existing) => Some(decode_row(existing.value())?),
None => None,
},
};
if let Some(old) = known {
let encoded = |t: ObjType| matches!(t, ObjType::OfsDelta | ObjType::RefDelta);
let kind_disagrees = !encoded(old.obj_type)
&& !encoded(e.obj_type)
&& old.obj_type != e.obj_type;
if kind_disagrees || old.uncompressed_size != e.uncompressed_size {
bail!(
"identity violation: {} is already stored as \
(offset {}, len {}, {}, size {}, delta_base {}) and cannot be \
redeclared as (offset {}, len {}, {}, size {}, delta_base {}) — \
the TYPE or the inflated SIZE differs, so the same oid is \
describing a different object. Placement (offset, len, \
delta_base) may differ freely: a re-pushed pack lands its \
copies elsewhere and the first writer's row stands.",
hex::encode(&e.oid),
old.offset,
old.len,
old.obj_type.as_str(),
old.uncompressed_size,
old.delta_base,
e.offset,
e.len,
e.obj_type.as_str(),
e.uncompressed_size,
e.delta_base,
);
}
continue;
}
objects.insert(e.oid.as_slice(), encode_row(seq, e).as_slice())?;
seq += 1;
added += 1;
bytes += e.len;
added_size += e.uncompressed_size;
added_types[e.obj_type.code() as usize] += 1;
}
meta.insert(META_ARRIVAL_SEQ, seq)?;
if added > 0 {
let epoch = meta.get(META_TAIL_EPOCH)?.map(|v| v.value()).unwrap_or(0);
meta.insert(META_TAIL_EPOCH, epoch + 1)?;
}
}
w.commit()?;
self.total_rows.fetch_add(added, Ordering::AcqRel);
self.unabsorbed_rows.fetch_add(added, Ordering::AcqRel);
self.unabsorbed_size.fetch_add(added_size, Ordering::AcqRel);
for (slot, n) in self.unabsorbed_types.iter().zip(added_types) {
slot.fetch_add(n, Ordering::AcqRel);
}
let total_bytes = self.tail_bytes.fetch_add(bytes, Ordering::AcqRel) + bytes;
drop(guard);
if let Some(threshold) = self.volume_threshold()
&& total_bytes >= threshold
{
self.rebuild()?;
return Ok(Some(RebuildReason::TailBytes(total_bytes)));
}
self.maybe_rebuild()
}
pub fn rebuild_due(&self) -> Option<RebuildReason> {
let hits = self.tail_hits.load(Ordering::Acquire);
if let Some(threshold) = self.triggers.miss_threshold(self.projection_len() as u64)
&& hits >= threshold
{
return Some(RebuildReason::TailHits(hits));
}
let bytes = self.tail_bytes.load(Ordering::Acquire);
if let Some(threshold) = self.volume_threshold()
&& bytes >= threshold
{
return Some(RebuildReason::TailBytes(bytes));
}
None
}
fn volume_threshold(&self) -> Option<u64> {
let floor = self.triggers.tail_bytes;
if floor == 0 {
return None;
}
if !self.persists() || self.triggers.fold_ratio <= 0.0 {
return Some(floor);
}
let index_bytes = self.projection.read().expect("projection lock").ipc_bytes() as f64;
Some(floor.max((self.triggers.fold_ratio * index_bytes) as u64))
}
pub fn maybe_rebuild(&self) -> Result<Option<RebuildReason>> {
match self.rebuild_due() {
Some(reason) => {
self.rebuild()?;
Ok(Some(reason))
}
None => Ok(None),
}
}
pub fn rebuild(&self) -> Result<()> {
let _g = self.fold.lock().map_err(|_| anyhow!("the fold lock is poisoned"))?;
self.fold_locked(None).map(|_| ())
}
fn fold_locked(&self, live: Option<&dyn Fn(&[u8]) -> bool>) -> Result<u64> {
let unfolded = scan(&self.tail)?;
let mut dropped = 0u64;
let mut merged: Vec<IndexEntry>;
if self.persists() {
let cur = self.projection.read().expect("projection lock").clone();
merged = Vec::with_capacity(cur.len() + unfolded.len());
for o in 0..cur.len() as u32 {
let e = cur.entry_at(o);
if live.is_none_or(|f| f(&e.oid)) {
merged.push(e);
} else {
dropped += 1;
}
}
} else {
merged = Vec::with_capacity(unfolded.len());
}
let forget: Vec<Vec<u8>> = if self.persists() {
unfolded.iter().map(|e| e.oid.clone()).collect()
} else {
Vec::new()
};
merged.extend(unfolded);
let total = merged.len() as u64;
let fresh = Arc::new(S::build(&merged)?);
drop(merged);
if let Some(path) = self.persisted.as_deref() {
write_atomic(path, fresh.persisted_bytes().expect("PERSISTS arm"))?;
if !forget.is_empty() {
let w = self.tail.begin_write()?;
{
let mut objects = w.open_table(OBJECTS)?;
for oid in &forget {
objects.remove(oid.as_slice())?;
}
}
w.commit()?;
}
self.folds.fetch_add(1, Ordering::AcqRel);
}
*self
.projection
.write()
.map_err(|_| anyhow!("the projection lock is poisoned"))? = fresh;
self.total_rows.store(total, Ordering::Release);
self.tail_hits.store(0, Ordering::Release);
self.tail_bytes.store(0, Ordering::Release);
self.unabsorbed_size.store(0, Ordering::Release);
self.unabsorbed_rows.store(0, Ordering::Release);
self.tail_txns.store(0, Ordering::Release);
for slot in &self.unabsorbed_types {
slot.store(0, Ordering::Release);
}
self.rebuilds.fetch_add(1, Ordering::AcqRel);
self.generation.fetch_add(1, Ordering::AcqRel);
Ok(dropped)
}
pub fn stats(&self) -> StackStats {
StackStats {
sealed_rows: self.projection_len() as u64,
total_rows: self.total_rows.load(Ordering::Acquire),
tail_hits: self.tail_hits.load(Ordering::Acquire),
absent: self.absent.load(Ordering::Acquire),
tail_bytes: self.tail_bytes.load(Ordering::Acquire),
rebuilds: self.rebuilds.load(Ordering::Acquire),
folds: self.folds.load(Ordering::Acquire),
persisted_loads: self.persisted_loads.load(Ordering::Acquire),
generation: self.generation.load(Ordering::Acquire),
unabsorbed_rows: self.unabsorbed_rows.load(Ordering::Acquire),
tail_txns: self.tail_txns.load(Ordering::Acquire),
}
}
pub fn projection_len(&self) -> usize {
self.projection.read().expect("projection lock").len()
}
pub fn oids_in_order(&self) -> Result<Vec<Vec<u8>>> {
let read = self.tail.begin_read()?;
let objects = read.open_table(OBJECTS)?;
let mut tail = Vec::with_capacity(objects.len()? as usize);
for row in objects.iter()? {
let (k, _) = row?;
tail.push(k.value().to_vec());
}
if !self.persists() {
return Ok(tail);
}
let cur = self.projection.read().expect("projection lock").clone();
let n = cur.len();
let mut out = Vec::with_capacity(n + tail.len());
let (mut i, mut j) = (0usize, 0usize);
while i < n || j < tail.len() {
let a = (i < n).then(|| cur.oid_at(i as u32));
let b = tail.get(j).map(Vec::as_slice);
match (a, b) {
(Some(a), Some(b)) => match a.as_slice().cmp(b) {
std::cmp::Ordering::Less => {
out.push(a);
i += 1;
}
std::cmp::Ordering::Greater => {
out.push(b.to_vec());
j += 1;
}
std::cmp::Ordering::Equal => {
out.push(a);
i += 1;
j += 1;
}
},
(Some(a), None) => {
out.push(a);
i += 1;
}
(None, Some(b)) => {
out.push(b.to_vec());
j += 1;
}
(None, None) => break,
}
}
Ok(out)
}
pub fn oids_raw(&self) -> Result<OrdinalSpace> {
let read = self.tail.begin_read()?;
let objects = read.open_table(OBJECTS)?;
let mut tail: Vec<Vec<u8>> = Vec::with_capacity(objects.len()? as usize);
for row in objects.iter()? {
let (k, _) = row?;
tail.push(k.value().to_vec());
}
let tail_rows = tail.len() as u64;
if !self.persists() {
let mut raw = Vec::with_capacity(tail.iter().map(Vec::len).sum());
for oid in &tail {
raw.extend_from_slice(oid);
}
return Ok(OrdinalSpace { raw, folded_rows: 0, tail_rows });
}
let cur = self.projection.read().expect("projection lock").clone();
let folded = cur.oids_raw();
let folded_rows = cur.len() as u64;
if tail.is_empty() {
return Ok(OrdinalSpace { raw: folded, folded_rows, tail_rows: 0 });
}
let w = match tail.first() {
Some(o) => o.len(),
None => unreachable!("tail is non-empty"),
};
let mut raw = Vec::with_capacity(folded.len() + tail.len() * w);
let (mut i, mut j) = (0usize, 0usize);
let n = folded.len() / w;
while i < n || j < tail.len() {
let a = (i < n).then(|| &folded[i * w..(i + 1) * w]);
let b = tail.get(j).map(Vec::as_slice);
match (a, b) {
(Some(a), Some(b)) => match a.cmp(b) {
std::cmp::Ordering::Less => {
raw.extend_from_slice(a);
i += 1;
}
std::cmp::Ordering::Greater => {
raw.extend_from_slice(b);
j += 1;
}
std::cmp::Ordering::Equal => {
raw.extend_from_slice(a);
i += 1;
j += 1;
}
},
(Some(a), None) => {
raw.extend_from_slice(a);
i += 1;
}
(None, Some(b)) => {
raw.extend_from_slice(b);
j += 1;
}
(None, None) => break,
}
}
Ok(OrdinalSpace { raw, folded_rows, tail_rows })
}
pub fn tail_epoch_and_len(&self) -> Result<(u64, u64)> {
let read = self.tail.begin_read()?;
let meta = read.open_table(META)?;
let epoch = meta.get(META_TAIL_EPOCH)?.map(|v| v.value()).unwrap_or(0);
let objects = read.open_table(OBJECTS)?;
let folded = if self.persists() { self.projection_len() as u64 } else { 0 };
Ok((epoch, objects.len()? + folded))
}
pub fn extents_with_rows(&self, extents: &[(u64, u64)]) -> Result<Vec<bool>> {
let mut hit = vec![false; extents.len()];
let mut order: Vec<usize> = (0..extents.len()).filter(|&i| extents[i].1 > 0).collect();
order.sort_unstable_by_key(|&i| extents[i].0);
let starts: Vec<u64> = order.iter().map(|&i| extents[i].0).collect();
let mut wanted = order.len();
if wanted == 0 {
return Ok(hit);
}
let mut place = |offset: u64| -> bool {
let p = starts.partition_point(|&s| s <= offset);
if p == 0 {
return false;
}
let i = order[p - 1];
let (start, len) = extents[i];
if offset < start + len && !hit[i] {
hit[i] = true;
wanted -= 1;
}
wanted == 0
};
if self.persists() {
let cur = self.projection.read().expect("projection lock").clone();
for o in 0..cur.len() as u32 {
if place(cur.extent_at(o).0) {
return Ok(hit);
}
}
}
let read = self.tail.begin_read()?;
let objects = read.open_table(OBJECTS)?;
for row in objects.iter()? {
let (_, v) = row?;
if place(decode_row(v.value())?.offset) {
break;
}
}
Ok(hit)
}
pub fn retain(&self, live: &dyn Fn(&[u8]) -> bool) -> Result<u64> {
let _g = self.fold.lock().map_err(|_| anyhow!("the fold lock is poisoned"))?;
let w = self.tail.begin_write()?;
let mut dropped = 0u64;
{
let mut objects = w.open_table(OBJECTS)?;
let dead: Vec<Vec<u8>> = objects
.iter()?
.filter_map(|row| row.ok())
.filter(|(k, _)| !live(k.value()))
.map(|(k, _)| k.value().to_vec())
.collect();
for oid in &dead {
objects.remove(oid.as_slice())?;
dropped += 1;
}
if dropped > 0 {
let mut meta = w.open_table(META)?;
let epoch = meta.get(META_TAIL_EPOCH)?.map(|v| v.value()).unwrap_or(0);
meta.insert(META_TAIL_EPOCH, epoch + 1)?;
}
}
w.commit()?;
let folded_dead = self.fold_locked(Some(live))?;
if folded_dead > 0 {
let w = self.tail.begin_write()?;
{
let mut meta = w.open_table(META)?;
let epoch = meta.get(META_TAIL_EPOCH)?.map(|v| v.value()).unwrap_or(0);
meta.insert(META_TAIL_EPOCH, epoch + 1)?;
}
w.commit()?;
}
Ok(dropped + folded_dead)
}
fn fill_from_tail(&self, oids: &[&[u8]], out: &mut [Option<IndexRow>]) -> Result<()> {
self.tail_txns.fetch_add(1, Ordering::AcqRel);
let read = self.tail.begin_read()?;
let objects = read.open_table(OBJECTS)?;
let mut hits = 0u64;
let mut absent = 0u64;
for (slot, oid) in out.iter_mut().zip(oids) {
if slot.is_some() {
continue;
}
match objects.get(*oid)? {
Some(v) => {
*slot = Some(decode_row(v.value())?.as_index_row());
hits += 1;
}
None => absent += 1,
}
}
self.tail_hits.fetch_add(hits, Ordering::AcqRel);
self.absent.fetch_add(absent, Ordering::AcqRel);
Ok(())
}
fn absences_only(&self, out: &[Option<IndexRow>]) {
let n = out.iter().filter(|s| s.is_none()).count() as u64;
self.absent.fetch_add(n, Ordering::AcqRel);
}
}
fn scan(db: &Database) -> Result<Vec<IndexEntry>> {
let read = db.begin_read()?;
let objects = read.open_table(OBJECTS)?;
let mut out = Vec::with_capacity(objects.len()? as usize);
for row in objects.iter()? {
let (k, v) = row?;
out.push(decode_row(v.value())?.as_entry(k.value()));
}
Ok(out)
}
fn tmp_path(path: &Path) -> PathBuf {
let mut name = path.file_name().map(|n| n.to_os_string()).unwrap_or_default();
name.push(".tmp");
path.with_file_name(name)
}
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
let tmp = tmp_path(path);
{
let mut f = File::create(&tmp)
.with_context(|| format!("creating {}", tmp.display()))?;
f.write_all(bytes)
.with_context(|| format!("writing {} bytes to {}", bytes.len(), tmp.display()))?;
f.sync_all()
.with_context(|| format!("syncing {}", tmp.display()))?;
}
fs::rename(&tmp, path)
.with_context(|| format!("renaming {} over {}", tmp.display(), path.display()))?;
if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
File::open(dir)
.and_then(|d| d.sync_all())
.with_context(|| format!("syncing the directory of {}", path.display()))?;
}
Ok(())
}
const COMPACT_FRAGMENTED_FLOOR: u64 = 16 << 20;
fn compact_if_fragmented(db: &mut Database, tail_path: Option<&Path>) {
let Some(tail_path) = tail_path else { return }; let file_len = match fs::metadata(tail_path) {
Ok(m) => m.len(),
Err(_) => return,
};
let (stored, metadata) = {
let w = match db.begin_write() {
Ok(w) => w,
Err(e) => {
tracing::debug!(target: "znippy.git", error = %e, "redb stats unavailable; not compacting");
return;
}
};
let s = w.stats().map(|s| (s.stored_bytes(), s.metadata_bytes()));
let _ = w.abort();
match s {
Ok(s) => s,
Err(e) => {
tracing::debug!(target: "znippy.git", error = %e, "redb stats unavailable; not compacting");
return;
}
}
};
let dead = file_len.saturating_sub(stored + metadata);
if dead < COMPACT_FRAGMENTED_FLOOR {
return;
}
let t = std::time::Instant::now();
match db.compact() {
Ok(did) => tracing::info!(
target: "znippy.git",
file_before = file_len,
file_after = fs::metadata(tail_path).map(|m| m.len()).unwrap_or(0),
stored_bytes = stored,
compacted = did,
ms = t.elapsed().as_millis(),
tail = %tail_path.display(),
"compacted the redb object tail: it was carrying dead pages from before a fold"
),
Err(e) => tracing::warn!(
target: "znippy.git",
file_len,
stored_bytes = stored,
error = %e,
"could not compact the redb object tail; it keeps its size, nothing else changes"
),
}
}
fn map_persisted(path: &Path) -> Result<Option<Buffer>> {
let f = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e).with_context(|| format!("opening {}", path.display())),
};
let len = f
.metadata()
.with_context(|| format!("stat {}", path.display()))?
.len();
if len == 0 {
return Ok(None);
}
#[cfg(target_os = "linux")]
{
let map = unsafe { memmap2::Mmap::map(&f) }
.with_context(|| format!("mapping {}", path.display()))?;
let map = Arc::new(map);
let ptr = std::ptr::NonNull::new(map.as_ptr() as *mut u8)
.ok_or_else(|| anyhow!("mmap of {} returned a null pointer", path.display()))?;
let buf = unsafe { Buffer::from_custom_allocation(ptr, map.len(), map) };
Ok(Some(buf))
}
#[cfg(not(target_os = "linux"))]
{
use std::io::Read;
let mut f = f;
let mut mb = znippy_common::arrow::buffer::MutableBuffer::with_capacity(len as usize);
let mut v = Vec::with_capacity(len as usize);
f.read_to_end(&mut v)
.with_context(|| format!("reading {}", path.display()))?;
mb.extend_from_slice(&v);
Ok(Some(mb.into()))
}
}
fn load_persisted<S: ObjectIndex>(path: &Path) -> Option<(Arc<S>, u128, u128)> {
let t0 = std::time::Instant::now();
match map_persisted(path) {
Ok(None) => None,
Ok(Some(buf)) => {
let map_ms = t0.elapsed().as_millis();
let t1 = std::time::Instant::now();
match S::from_persisted(buf) {
Ok(s) => Some((Arc::new(s), map_ms, t1.elapsed().as_millis())),
Err(e) => {
tracing::warn!(
target: "znippy.git",
path = %path.display(),
error = %format!("{e:#}"),
"the persisted object index could not be read; opening without it and \
re-indexing from the journal — slower until the drain catches up, \
never wrong"
);
None
}
}
}
Err(e) => {
tracing::warn!(
target: "znippy.git",
path = %path.display(),
error = %format!("{e:#}"),
"the persisted object index could not be opened; opening without it"
);
None
}
}
}
impl<S: ObjectIndex> ObjectIndex for ObjectReadStack<S> {
fn build(entries: &[IndexEntry]) -> Result<Self> {
let stack = Self::in_memory(RebuildTriggers::default())?;
stack.append(entries)?;
stack.rebuild()?;
Ok(stack)
}
fn lookup(&self, oid: &[u8]) -> Option<IndexRow> {
if let Some(row) = self.projection.read().expect("projection lock").lookup(oid) {
return Some(row);
}
if self.projection_is_complete() {
self.absent.fetch_add(1, Ordering::AcqRel);
return None;
}
let mut out = [None];
let _ = self.fill_from_tail(&[oid], &mut out);
out[0]
}
fn lookup_batch(&self, oids: &[&[u8]]) -> Vec<Option<IndexRow>> {
let mut out = self
.projection
.read()
.expect("projection lock")
.lookup_batch(oids);
if out.iter().any(Option::is_none) {
if self.projection_is_complete() {
self.absences_only(&out);
} else {
let _ = self.fill_from_tail(oids, &mut out);
}
}
out
}
fn ordinals_batch(&self, oids: &[&[u8]]) -> Vec<Option<u32>> {
let mut out = self
.projection
.read()
.expect("projection lock")
.ordinals_batch(oids);
if out.iter().any(Option::is_none) {
if self.projection_is_complete() {
let n = out.iter().filter(|s| s.is_none()).count() as u64;
self.absent.fetch_add(n, Ordering::AcqRel);
return out;
}
let mut rows: Vec<Option<IndexRow>> = out
.iter()
.map(|o| {
o.map(|ordinal| IndexRow {
ordinal,
offset: 0,
len: 0,
obj_type: ObjType::Blob,
uncompressed_size: 0,
delta_base: 0,
})
})
.collect();
let _ = self.fill_from_tail(oids, &mut rows);
for (slot, row) in out.iter_mut().zip(&rows) {
*slot = row.map(|r| r.ordinal);
}
}
out
}
fn extents_batch(&self, oids: &[&[u8]]) -> Vec<Option<(u64, u64)>> {
let mut out = self
.projection
.read()
.expect("projection lock")
.extents_batch(oids);
if out.iter().any(Option::is_none) {
if self.projection_is_complete() {
let n = out.iter().filter(|s| s.is_none()).count() as u64;
self.absent.fetch_add(n, Ordering::AcqRel);
return out;
}
let mut rows: Vec<Option<IndexRow>> = out
.iter()
.map(|e| {
e.map(|(offset, len)| IndexRow {
ordinal: 0,
offset,
len,
obj_type: ObjType::Blob,
uncompressed_size: 0,
delta_base: 0,
})
})
.collect();
let _ = self.fill_from_tail(oids, &mut rows);
for (slot, row) in out.iter_mut().zip(&rows) {
*slot = row.map(|r| (r.offset, r.len));
}
}
out
}
fn sum_uncompressed(&self) -> u64 {
self.projection
.read()
.expect("projection lock")
.sum_uncompressed()
+ self.unabsorbed_size.load(Ordering::Acquire)
}
fn count_type(&self, t: ObjType) -> usize {
self.projection
.read()
.expect("projection lock")
.count_type(t)
+ self.unabsorbed_types[t.code() as usize].load(Ordering::Acquire) as usize
}
fn name(&self) -> &'static str {
"ObjectReadStack"
}
fn oid_at(&self, ordinal: u32) -> Vec<u8> {
assert!(!is_tail_ordinal(ordinal), "ordinal {ordinal:#x} is tail-served, not positional");
self.projection.read().expect("projection lock").oid_at(ordinal)
}
fn entry_at(&self, ordinal: u32) -> IndexEntry {
assert!(!is_tail_ordinal(ordinal), "ordinal {ordinal:#x} is tail-served, not positional");
self.projection.read().expect("projection lock").entry_at(ordinal)
}
fn extent_at(&self, ordinal: u32) -> (u64, u64) {
assert!(!is_tail_ordinal(ordinal), "ordinal {ordinal:#x} is tail-served, not positional");
self.projection.read().expect("projection lock").extent_at(ordinal)
}
fn len(&self) -> usize {
self.total_rows.load(Ordering::Acquire) as usize
}
fn ipc_bytes(&self) -> usize {
self.projection.read().expect("projection lock").ipc_bytes()
}
fn resident_bytes(&self) -> usize {
self.projection
.read()
.expect("projection lock")
.resident_bytes()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arms::DEFAULT_REDB_CACHE_BYTES;
use crate::index_layout::{FourTables, synthetic_entries};
type Stack = ObjectReadStack<OneTableFourColumns>;
fn manual() -> Stack {
Stack::in_memory(RebuildTriggers::manual()).expect("in-memory stack")
}
fn split_stack(sealed: &[IndexEntry], tail: &[IndexEntry]) -> Stack {
let s = manual();
s.append(sealed).expect("seal half");
s.rebuild().expect("rebuild");
s.append(tail).expect("append tail");
s
}
#[test]
fn the_projection_is_incomplete_but_never_wrong() {
let all = synthetic_entries(500, 20, 0xC0FFEE);
let (sealed, tail) = all.split_at(300);
let stack = split_stack(sealed, tail);
assert_eq!(stack.projection_len(), 300, "the projection must be short");
assert_eq!(stack.len(), 500, "the stack answers for the repository");
assert!(
tail.iter().filter(|e| e.delta_base != 0).count() >= 10,
"the un-absorbed half carries no delta bases — the tail encoding would be untested"
);
let sealed_only = FourTables::build(sealed).expect("sealed-only index");
let complete = FourTables::build(&all).expect("complete index");
{
let proj = stack.projection.read().unwrap();
for e in tail {
assert!(
proj.lookup(&e.oid).is_none(),
"the projection already holds {} — this test would be vacuous",
hex::encode(&e.oid)
);
}
for e in sealed {
assert!(
proj.lookup(&e.oid).is_some(),
"the projection lost sealed object {}",
hex::encode(&e.oid)
);
}
}
let refs: Vec<&[u8]> = all.iter().map(|e| e.oid.as_slice()).collect();
let batched = stack.lookup_batch(&refs);
for (i, e) in all.iter().enumerate() {
let via_serial = stack.lookup(&e.oid).unwrap_or_else(|| {
panic!(
"the stack lost {} object {}, serial path",
if i < 300 { "sealed" } else { "tail" },
hex::encode(&e.oid)
)
});
let via_batch = batched[i].unwrap_or_else(|| {
panic!(
"the stack lost {} object {}, batch path",
if i < 300 { "sealed" } else { "tail" },
hex::encode(&e.oid)
)
});
assert_eq!(via_serial, via_batch, "serial and batch disagree");
assert_eq!(via_serial.offset, e.offset);
assert_eq!(via_serial.len, e.len);
assert_eq!(via_serial.obj_type, e.obj_type);
assert_eq!(via_serial.uncompressed_size, e.uncompressed_size);
assert_eq!(
via_serial.delta_base,
e.delta_base,
"the delta base of {} did not survive the {} path",
hex::encode(&e.oid),
if i < 300 { "projection" } else { "tail" }
);
let truth = complete.lookup(&e.oid).expect("complete index has it");
assert_eq!(via_serial.offset, truth.offset);
assert_eq!(via_serial.len, truth.len);
assert_eq!(via_serial.obj_type, truth.obj_type);
assert_eq!(via_serial.uncompressed_size, truth.uncompressed_size);
assert_eq!(via_serial.delta_base, truth.delta_base);
if i < 300 {
assert!(
!is_tail_row(&via_serial),
"sealed row wearing a tail ordinal"
);
assert_eq!(
via_serial.ordinal,
sealed_only.lookup(&e.oid).unwrap().ordinal,
"the projection's ordinal for {} is not its rank in the generation the \
projection covers",
hex::encode(&e.oid)
);
} else {
assert!(
is_tail_row(&via_serial),
"tail row {} has no tail ordinal",
hex::encode(&e.oid)
);
}
}
}
#[test]
fn only_a_tail_miss_is_absent_and_only_a_tail_hit_counts() {
let all = synthetic_entries(200, 20, 7);
let (sealed, tail) = all.split_at(120);
let stack = split_stack(sealed, tail);
let nowhere = synthetic_entries(64, 20, 0x00AB_5E47);
let mut refs: Vec<&[u8]> = Vec::new();
refs.extend(sealed.iter().map(|e| e.oid.as_slice()));
refs.extend(tail.iter().map(|e| e.oid.as_slice()));
refs.extend(nowhere.iter().map(|e| e.oid.as_slice()));
let rows = stack.lookup_batch(&refs);
assert_eq!(rows[..120].iter().filter(|r| r.is_some()).count(), 120);
assert_eq!(rows[120..200].iter().filter(|r| r.is_some()).count(), 80);
assert!(
rows[200..].iter().all(Option::is_none),
"an object in no repository resolved to a row"
);
let st = stack.stats();
assert_eq!(st.tail_hits, 80, "tail-served lookups miscounted");
assert_eq!(st.absent, 64, "genuine absences miscounted");
}
#[test]
fn an_append_only_violation_is_refused() {
let entries = synthetic_entries(16, 20, 11);
let stack = manual();
stack.append(&entries).expect("first append");
stack
.append(&entries)
.expect("identical re-append is idempotent");
assert_eq!(
stack.len(),
16,
"an idempotent re-append changed the row count"
);
let mut moved = entries[3].clone();
moved.offset += 1;
stack
.append(std::slice::from_ref(&moved))
.expect("a re-pushed pack at a new offset was refused");
let row = stack.lookup(&entries[3].oid).expect("still there");
assert_eq!(
row.offset, entries[3].offset,
"the second placement overwrote the first — the projection now names \
an offset that is not where the first writer put the bytes"
);
assert_eq!(stack.len(), 16, "a re-placement added a row");
let mut retyped = entries[4].clone();
retyped.uncompressed_size += 1;
let err = stack
.append(std::slice::from_ref(&retyped))
.expect_err("an oid that changed size was accepted");
assert!(
err.to_string().contains("identity violation"),
"wrong error: {err}"
);
let based = entries
.iter()
.find(|e| e.delta_base != 0)
.expect("the fixture must carry a delta");
let mut rebased = based.clone();
rebased.delta_base += 8;
stack
.append(std::slice::from_ref(&rebased))
.expect("a re-pushed pack that re-deltaed was refused");
assert_eq!(
stack.lookup(&based.oid).unwrap().delta_base,
based.delta_base,
"the second pack's base was adopted — the projection now names a base \
the FIRST writer never recorded, which is the thing the old guard \
was right to fear"
);
}
#[test]
fn each_trigger_fires_on_its_own_signal() {
let entries = synthetic_entries(64, 20, 21);
let bytes: u64 = entries.iter().map(|e| e.len).sum();
let stack = Stack::in_memory(RebuildTriggers {
tail_bytes: bytes,
..RebuildTriggers::manual()
})
.unwrap();
let g0 = stack.stats().generation;
let reason = stack.append(&entries).unwrap();
assert!(
matches!(reason, Some(RebuildReason::TailBytes(_))),
"the volume trigger did not fire at exactly its threshold: {reason:?}"
);
assert_eq!(stack.projection_len(), 64, "the rebuild absorbed nothing");
assert_eq!(stack.stats().generation, g0 + 1);
assert_eq!(
stack.stats().tail_bytes,
0,
"the byte counter was not reset"
);
let all = synthetic_entries(200, 20, 22);
let (sealed, tail) = all.split_at(100);
let stack = Stack::in_memory(RebuildTriggers {
tail_hits_per_row: 0.0,
min_tail_hits: 100,
tail_bytes: 0,
fold_ratio: 0.0,
})
.unwrap();
stack.append(sealed).unwrap();
stack.rebuild().unwrap();
stack.append(tail).unwrap();
let g1 = stack.stats().generation;
assert_eq!(stack.projection_len(), 100);
let nowhere = synthetic_entries(1000, 20, 23);
let refs: Vec<&[u8]> = nowhere.iter().map(|e| e.oid.as_slice()).collect();
assert!(stack.lookup_batch(&refs).iter().all(Option::is_none));
assert_eq!(stack.stats().absent, 1000);
assert_eq!(
stack.rebuild_due(),
None,
"absences tripped the miss trigger"
);
stack.maybe_rebuild().unwrap();
assert_eq!(
stack.stats().generation,
g1,
"1000 absences must not rebuild"
);
assert_eq!(stack.projection_len(), 100);
let refs: Vec<&[u8]> = tail.iter().map(|e| e.oid.as_slice()).collect();
assert_eq!(stack.lookup_batch(&refs).iter().flatten().count(), 100);
assert_eq!(
stack.rebuild_due(),
Some(RebuildReason::TailHits(100)),
"100 tail-served lookups did not trip a threshold of 100"
);
assert!(stack.maybe_rebuild().unwrap().is_some());
assert_eq!(
stack.projection_len(),
200,
"the rebuild did not absorb the tail"
);
assert_eq!(stack.stats().generation, g1 + 1);
}
#[test]
fn the_column_scans_answer_for_the_repository_not_the_projection() {
let all = synthetic_entries(300, 20, 71);
let (sealed, tail) = all.split_at(180);
let stack = split_stack(sealed, tail);
let complete = OneTableFourColumns::build(&all).unwrap();
assert!(
stack.projection.read().unwrap().sum_uncompressed() < complete.sum_uncompressed(),
"the projection already sums to the whole repository — this test would be vacuous"
);
assert_eq!(
stack.sum_uncompressed(),
complete.sum_uncompressed(),
"sum_uncompressed answered for the projection, not the repository"
);
for t in ObjType::ALL {
assert_eq!(
stack.count_type(t),
complete.count_type(t),
"count_type({}) answered for the projection",
t.as_str()
);
}
let mut rewritten = all[0].clone();
rewritten.uncompressed_size += 1_000_000;
let batch = [all[7].clone(), rewritten];
assert!(stack.append(&batch).is_err());
assert_eq!(stack.sum_uncompressed(), complete.sum_uncompressed());
stack.rebuild().unwrap();
assert_eq!(stack.sum_uncompressed(), complete.sum_uncompressed());
for t in ObjType::ALL {
assert_eq!(stack.count_type(t), complete.count_type(t));
}
}
#[test]
fn the_narrow_batch_paths_also_fall_through() {
let all = synthetic_entries(300, 20, 81);
let (sealed, tail) = all.split_at(180);
let stack = split_stack(sealed, tail);
let refs: Vec<&[u8]> = all.iter().map(|e| e.oid.as_slice()).collect();
let ordinals = stack.ordinals_batch(&refs);
assert_eq!(
ordinals.iter().filter(|o| o.is_some()).count(),
300,
"the ordinal path lost {} of 300 objects",
300 - ordinals.iter().filter(|o| o.is_some()).count()
);
for (o, e) in ordinals[180..].iter().zip(tail) {
assert!(
o.unwrap() & TAIL_ORDINAL_BIT != 0,
"tail object {} got a projection ordinal",
hex::encode(&e.oid)
);
}
let extents = stack.extents_batch(&refs);
for (x, e) in extents.iter().zip(&all) {
assert_eq!(
*x,
Some((e.offset, e.len)),
"extent lost or wrong for {}",
hex::encode(&e.oid)
);
}
}
#[test]
fn a_rebuild_re_derives_ordinals_and_clears_the_tail_bit() {
let all = synthetic_entries(120, 32, 31);
let (sealed, tail) = all.split_at(60);
let stack = split_stack(sealed, tail);
let before = stack.lookup(&tail[0].oid).unwrap();
assert!(is_tail_row(&before));
stack.rebuild().unwrap();
let after = stack.lookup(&tail[0].oid).unwrap();
assert!(
!is_tail_row(&after),
"an absorbed row kept its tail ordinal"
);
assert_eq!(after.offset, before.offset, "absorption changed a fact");
assert_eq!(after.len, before.len);
assert_eq!(after.obj_type, before.obj_type);
assert_eq!(after.uncompressed_size, before.uncompressed_size);
assert_eq!(after.delta_base, before.delta_base);
let based = tail
.iter()
.find(|e| e.delta_base != 0)
.expect("the tail half must carry at least one delta");
let row = stack.lookup(&based.oid).unwrap();
assert_eq!(
row.delta_base,
based.delta_base,
"the rebuild moved {}'s delta base from {} to {}",
hex::encode(&based.oid),
based.delta_base,
row.delta_base
);
assert_ne!(
row.delta_base, row.ordinal as u64,
"a delta base that equals a row ordinal is the mistake §13 forbids"
);
let complete = OneTableFourColumns::build(&all).unwrap();
assert_eq!(
after.ordinal,
complete.lookup(&tail[0].oid).unwrap().ordinal,
"the rebuilt ordinal is not the lexicographic rank"
);
}
#[test]
fn the_crash_recovery_diff_is_exact_at_the_pack_boundary() {
let all = synthetic_entries(400, 20, 0xB17_5E7);
let bounds = [(0usize, 100usize), (100, 200), (200, 300), (300, 400)];
let extents: Vec<(u64, u64)> = bounds
.iter()
.map(|&(a, b)| {
let start = all[a].offset;
let end = all[b - 1].offset + all[b - 1].len;
(start, end - start)
})
.collect();
assert_eq!(
extents[1].0,
extents[0].0 + extents[0].1,
"the fixture's packs must be adjacent or the boundary is not under test"
);
let stack = manual();
for (i, &(a, b)) in bounds.iter().enumerate() {
if i != 1 {
stack.append(&all[a..b]).unwrap();
}
}
assert_eq!(stack.len(), 300, "the middle pack must be the missing one");
let hit = stack.extents_with_rows(&extents).unwrap();
assert_eq!(
hit,
vec![true, false, true, true],
"pack B has no rows in the tail and was reported absorbed"
);
assert_eq!(
stack.extents_with_rows(&extents[..2]).unwrap(),
vec![true, false],
"a row one byte past pack B's extent was counted as B's"
);
let past = all[399].offset + all[399].len;
assert_eq!(
stack.extents_with_rows(&[(0, 0), (past, 4096)]).unwrap(),
vec![false, false]
);
assert_eq!(
stack
.extents_with_rows(&[(all[7].offset, all[7].len)])
.unwrap(),
vec![true]
);
}
#[test]
fn a_file_backed_tail_reopens_warm() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("objects.tail.redb");
let all = synthetic_entries(90, 20, 41);
{
let stack =
ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
stack.append(&all[..40]).unwrap();
stack.rebuild().unwrap();
stack.append(&all[40..]).unwrap();
assert_eq!(stack.projection_len(), 40);
}
let stack =
ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
).unwrap();
assert_eq!(stack.len(), 90, "the repository is {} of 90 rows", stack.len());
assert_eq!(stack.projection_len(), 40, "the mapped index holds what the fold wrote");
assert_eq!(stack.stats().persisted_loads, 1, "the open must have mapped, not rebuilt");
assert_eq!(stack.stats().unabsorbed_rows, 50, "the 50 later rows are redb's to serve");
for e in &all {
let row = stack
.lookup(&e.oid)
.unwrap_or_else(|| panic!("reopen lost {}", hex::encode(&e.oid)));
assert_eq!(row.offset, e.offset);
assert_eq!(row.uncompressed_size, e.uncompressed_size);
assert_eq!(row.delta_base, e.delta_base, "reopen lost a delta base");
}
assert!(
all.iter().filter(|e| e.delta_base != 0).count() >= 5,
"the fixture must carry delta bases across the reopen"
);
}
fn folded_then_appended(
dir: &Path,
all: &[IndexEntry],
split: usize,
) -> (PathBuf, PathBuf, ObjectReadStack<OneTableFourColumns>) {
let tail = dir.join("objects.tail");
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
stack.append(&all[..split]).unwrap();
stack.rebuild().unwrap();
stack.append(&all[split..]).unwrap();
let index = stack.persisted_path().expect("the default arm persists").to_path_buf();
(tail, index, stack)
}
fn redb_rows(stack: &ObjectReadStack<OneTableFourColumns>) -> u64 {
let r = stack.tail.begin_read().unwrap();
r.open_table(OBJECTS).unwrap().len().unwrap()
}
#[test]
fn a_rebuild_on_a_file_backed_stack_is_a_fold() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(90, 20, 43);
let (_tail, index, stack) = folded_then_appended(dir.path(), &all, 40);
assert!(index.is_file(), "the fold must persist the projection at {}", index.display());
assert!(
fs::metadata(&index).unwrap().len() > 40 * 61,
"the persisted index must carry at least the rows' own bytes"
);
assert_eq!(stack.stats().folds, 1);
assert_eq!(redb_rows(&stack), 50, "redb must hold only what arrived after the fold");
assert_eq!(stack.projection_len(), 40);
assert_eq!(stack.len(), 90);
assert!(!stack.projection_is_complete());
for e in &all {
let row = stack.lookup(&e.oid).unwrap_or_else(|| panic!("lost {}", hex::encode(&e.oid)));
assert_eq!((row.offset, row.len, row.delta_base), (e.offset, e.len, e.delta_base));
}
assert!(is_tail_row(&stack.lookup(&all[80].oid).unwrap()));
assert!(!is_tail_row(&stack.lookup(&all[10].oid).unwrap()));
stack.rebuild().unwrap();
assert_eq!(redb_rows(&stack), 0);
assert_eq!(stack.projection_len(), 90);
assert!(stack.projection_is_complete());
assert_eq!(stack.stats().folds, 2);
}
#[test]
fn re_appending_a_folded_row_creates_no_second_row() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(40, 20, 50);
let tail = dir.path().join("objects.tail");
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
stack.append(&all).unwrap();
stack.rebuild().unwrap();
assert_eq!(redb_rows(&stack), 0, "precondition: everything folded");
let mut again = all.clone();
for e in &mut again {
e.offset += 1_000_000;
}
stack.append(&again).unwrap();
assert_eq!(redb_rows(&stack), 0, "a folded row re-appended must not land in redb");
assert_eq!(stack.len(), 40);
assert_eq!(stack.oids_in_order().unwrap().len(), 40);
assert_eq!(stack.lookup(&all[3].oid).unwrap().offset, all[3].offset);
let mut lie = all[7].clone();
lie.uncompressed_size += 1;
let err = stack.append(&[lie]).unwrap_err().to_string();
assert!(err.contains("identity violation"), "{err}");
assert_eq!(redb_rows(&stack), 0);
}
#[test]
fn a_warm_open_maps_the_folded_index_and_does_not_rebuild() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(90, 20, 44);
let (tail, _index, stack) = folded_then_appended(dir.path(), &all, 60);
drop(stack);
let again = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
let s = again.stats();
assert_eq!(s.persisted_loads, 1, "the open must have taken the index off disk");
assert_eq!(s.folds, 0, "a warm open folds nothing — there is nothing to migrate");
assert_eq!(again.projection_len(), 60, "the mapped projection is what the fold wrote");
assert_eq!(again.len(), 90);
assert_eq!(s.unabsorbed_rows, 30);
assert_eq!(redb_rows(&again), 30);
let expect_size: u64 = all.iter().map(|e| e.uncompressed_size).sum();
assert_eq!(again.sum_uncompressed(), expect_size);
for t in [ObjType::Commit, ObjType::Tree, ObjType::Blob, ObjType::OfsDelta] {
assert_eq!(
again.count_type(t),
all.iter().filter(|e| e.obj_type == t).count(),
"count_type({t:?}) over both layers"
);
}
for e in &all {
let row = again.lookup(&e.oid).unwrap_or_else(|| panic!("lost {}", hex::encode(&e.oid)));
assert_eq!(row.uncompressed_size, e.uncompressed_size);
}
}
#[test]
fn an_interrupted_fold_is_reconciled_at_the_next_open() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(60, 20, 45);
let (tail, _index, stack) = folded_then_appended(dir.path(), &all, 30);
{
let w = stack.tail.begin_write().unwrap();
{
let mut objects = w.open_table(OBJECTS).unwrap();
for (i, e) in all[..30].iter().enumerate() {
objects.insert(e.oid.as_slice(), encode_row(1000 + i as u64, e).as_slice()).unwrap();
}
}
w.commit().unwrap();
}
assert_eq!(redb_rows(&stack), 60, "precondition: every row is now in both layers");
drop(stack);
let again = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(redb_rows(&again), 30, "the duplicates must leave redb at open");
assert_eq!(again.len(), 60);
assert_eq!(again.tail_epoch_and_len().unwrap().1, 60);
let oids = again.oids_in_order().unwrap();
assert_eq!(oids.len(), 60, "each object once");
assert!(oids.windows(2).all(|w| w[0] < w[1]), "oid order, no repeats");
}
#[test]
fn the_tail_wide_answers_cover_both_layers() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(90, 20, 46);
let (_tail, _index, stack) = folded_then_appended(dir.path(), &all, 40);
let mut expect: Vec<Vec<u8>> = all.iter().map(|e| e.oid.clone()).collect();
expect.sort();
assert_eq!(stack.oids_in_order().unwrap(), expect, "the merge of both layers, in oid order");
assert_eq!(stack.tail_epoch_and_len().unwrap().1, 90);
let folded = &all[3];
let tailed = &all[77];
let nowhere = (u64::MAX / 2, 7u64);
let hit = stack
.extents_with_rows(&[(folded.offset, folded.len), (tailed.offset, tailed.len), nowhere])
.unwrap();
assert_eq!(hit, vec![true, true, false], "folded / tail / absent");
}
#[test]
fn the_ordinal_space_is_both_layers_with_provenance() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(90, 20, 51);
let (_tail, _index, stack) = folded_then_appended(dir.path(), &all, 40);
let expect: Vec<u8> = stack.oids_in_order().unwrap().concat();
let space = stack.oids_raw().unwrap();
assert_eq!(space.raw, expect, "the space must be oids_in_order, concatenated");
assert_eq!((space.folded_rows, space.tail_rows), (40, 50));
assert_eq!(space.raw.len(), 90 * 20);
stack.rebuild().unwrap();
let space = stack.oids_raw().unwrap();
assert_eq!(space.raw, expect, "a fold does not change the space");
assert_eq!((space.folded_rows, space.tail_rows), (90, 0), "everything off the index now");
}
#[test]
fn the_volume_trigger_scales_with_the_persisted_index() {
let dir = tempfile::tempdir().unwrap();
let mut all = synthetic_entries(400, 20, 52);
for e in &mut all {
e.len = 100;
}
let triggers = RebuildTriggers {
tail_bytes: 1,
fold_ratio: 1.0,
..RebuildTriggers::manual()
};
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&dir.path().join("objects.tail"),
triggers,
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
stack.append(&all[..300]).unwrap();
let folds = stack.stats().folds;
assert_eq!(folds, 1, "the first batch folds at the floor");
let index_bytes = stack.ipc_bytes() as u64;
assert!(index_bytes > 10_000, "precondition: a real index, got {index_bytes} B");
stack.append(&all[300..350]).unwrap();
assert_eq!(stack.stats().folds, folds, "a small append must not fold a big index");
assert_eq!(stack.stats().unabsorbed_rows, 50, "the 50 rows wait in redb");
let mut big: Vec<IndexEntry> = all[350..400].to_vec();
for e in &mut big {
e.len = index_bytes; }
stack.append(&big[..1]).unwrap();
assert_eq!(stack.stats().folds, folds + 1, "crossing the index size folds");
assert!(stack.projection_is_complete());
let mem = ObjectReadStack::<OneTableFourColumns>::in_memory(triggers).unwrap();
mem.append(&all[..300]).unwrap();
let g = mem.stats().generation;
mem.append(&all[300..350]).unwrap();
assert!(
mem.stats().generation > g,
"without persistence the fixed floor still rebuilds on every batch"
);
assert_eq!(mem.stats().folds, 0, "and nothing is written");
}
#[test]
fn retain_rewrites_the_persisted_index_without_the_dead_rows() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(90, 20, 47);
let (tail, index, stack) = folded_then_appended(dir.path(), &all, 40);
let dead_folded = all[5].oid.clone();
let dead_tailed = all[70].oid.clone();
let before = fs::metadata(&index).unwrap().len();
let epoch_before = stack.tail_epoch_and_len().unwrap().0;
let dropped = stack
.retain(&|oid: &[u8]| oid != dead_folded.as_slice() && oid != dead_tailed.as_slice())
.unwrap();
assert_eq!(dropped, 2, "one from each layer");
assert!(stack.lookup(&dead_folded).is_none(), "the folded dead row must be gone");
assert!(stack.lookup(&dead_tailed).is_none(), "the tail dead row must be gone");
assert_eq!(stack.len(), 88);
assert_eq!(stack.projection_len(), 88, "retain folds: everything live is in the index");
assert_eq!(redb_rows(&stack), 0);
assert!(
fs::metadata(&index).unwrap().len() > before,
"the index was rewritten (88 rows now, was 40)"
);
assert!(
stack.tail_epoch_and_len().unwrap().0 > epoch_before,
"a changed oid set is a changed epoch"
);
for e in all.iter().filter(|e| e.oid != dead_folded && e.oid != dead_tailed) {
assert!(stack.lookup(&e.oid).is_some(), "live row {} lost", hex::encode(&e.oid));
}
drop(stack);
let again = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(again.stats().persisted_loads, 1);
assert_eq!(again.len(), 88);
assert!(again.lookup(&dead_folded).is_none());
}
#[test]
fn a_damaged_persisted_index_is_ignored_and_redb_still_answers() {
let dir = tempfile::tempdir().unwrap();
let all = synthetic_entries(60, 20, 48);
let (tail, index, stack) = folded_then_appended(dir.path(), &all, 30);
drop(stack);
let bytes = fs::read(&index).unwrap();
fs::write(&index, &bytes[..bytes.len() / 2]).unwrap();
let again = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(again.stats().persisted_loads, 0, "a damaged index must not count as loaded");
assert_eq!(again.len(), 30, "the 30 redb rows are what the store knows");
for e in &all[30..] {
assert!(again.lookup(&e.oid).is_some(), "redb row {} lost", hex::encode(&e.oid));
}
for e in &all[..30] {
assert!(
again.lookup(&e.oid).is_none(),
"a row only the damaged index held must read absent, so the journal re-indexes it"
);
}
again.append(&all[..30]).unwrap();
again.rebuild().unwrap();
assert_eq!(again.len(), 60);
drop(again);
let third = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(third.stats().persisted_loads, 1);
assert_eq!(third.len(), 60);
}
#[test]
fn a_cold_open_of_a_pre_fold_store_folds_it_once() {
let dir = tempfile::tempdir().unwrap();
let tail = dir.path().join("objects.tail");
let all = synthetic_entries(70, 20, 49);
{
let db = Database::create(&tail).unwrap();
let w = db.begin_write().unwrap();
{
let mut objects = w.open_table(OBJECTS).unwrap();
for (i, e) in all.iter().enumerate() {
objects.insert(e.oid.as_slice(), encode_row(i as u64, e).as_slice()).unwrap();
}
let mut meta = w.open_table(META).unwrap();
meta.insert(META_TAIL_FORMAT, TAIL_FORMAT).unwrap();
meta.insert(META_ARRIVAL_SEQ, all.len() as u64).unwrap();
}
w.commit().unwrap();
}
let index = tail.with_extension("index");
assert!(!index.exists(), "precondition: no persisted index");
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(stack.stats().persisted_loads, 0, "a cold open has nothing to map");
assert_eq!(stack.stats().folds, 1, "and folds exactly once");
assert!(index.is_file(), "the fold must have written {}", index.display());
assert_eq!(redb_rows(&stack), 0, "redb is emptied by the migration fold");
assert_eq!(stack.len(), 70);
assert!(stack.projection_is_complete());
for e in &all {
assert!(stack.lookup(&e.oid).is_some(), "migrated row {} lost", hex::encode(&e.oid));
}
drop(stack);
let again = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(again.stats().persisted_loads, 1, "the second open is warm");
assert_eq!(again.stats().folds, 0);
assert_eq!(again.len(), 70);
}
#[test]
fn a_migrated_store_gets_its_redb_file_back() {
let dir = tempfile::tempdir().unwrap();
let tail = dir.path().join("objects.tail");
let n = 200_000usize;
let all = synthetic_entries(n, 20, 53);
{
let db = Database::create(&tail).unwrap();
let w = db.begin_write().unwrap();
{
let mut objects = w.open_table(OBJECTS).unwrap();
for (i, e) in all.iter().enumerate() {
objects.insert(e.oid.as_slice(), encode_row(i as u64, e).as_slice()).unwrap();
}
let mut meta = w.open_table(META).unwrap();
meta.insert(META_TAIL_FORMAT, TAIL_FORMAT).unwrap();
meta.insert(META_ARRIVAL_SEQ, n as u64).unwrap();
}
w.commit().unwrap();
}
let before = fs::metadata(&tail).unwrap().len();
assert!(
before >= COMPACT_FRAGMENTED_FLOOR,
"precondition: the fixture must be above the compaction floor, is {before} B"
);
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&tail,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(stack.stats().folds, 1, "the migration fold ran");
assert_eq!(redb_rows(&stack), 0);
let after = fs::metadata(&tail).unwrap().len();
assert!(
after < before / 4,
"the migrated redb must shrink: {after} >= {before} / 4"
);
assert!(stack.lookup(&all[123].oid).is_some());
stack.append(&synthetic_entries(10, 20, 54)).unwrap();
assert_eq!(redb_rows(&stack), 10);
}
#[test]
fn a_packed_tail_row_round_trips_and_refuses_nonsense() {
let mut e = synthetic_entries(1, 20, 51)[0].clone();
e.delta_base = 4242;
let e = &e;
let packed = encode_row(9, e);
let back = decode_row(&packed).unwrap();
assert_eq!(back.seq, 9);
assert_eq!(back.offset, e.offset);
assert_eq!(back.len, e.len);
assert_eq!(back.obj_type, e.obj_type);
assert_eq!(back.uncompressed_size, e.uncompressed_size);
assert_eq!(
back.delta_base, e.delta_base,
"the delta base did not survive the tail row"
);
assert_ne!(
back.delta_base, back.uncompressed_size,
"the fixture must not let the two u64 fields alias"
);
assert!(decode_row(&packed[..TAIL_ROW_BYTES - 1]).is_err());
assert!(
decode_row(&packed[..33]).is_err(),
"a row in the old 33-byte encoding must be refused, not reinterpreted"
);
let mut bad = packed;
bad[24] = 5; assert!(
decode_row(&bad).is_err(),
"an unassigned object type code was accepted"
);
}
#[test]
fn a_fully_absorbed_stack_agrees_with_both_arrow_arms() {
let entries = synthetic_entries(400, 20, 61);
let absent = synthetic_entries(100, 20, 62);
let stack = Stack::build(&entries).unwrap();
let a = FourTables::build(&entries).unwrap();
let b = OneTableFourColumns::build(&entries).unwrap();
assert_eq!(stack.len(), 400);
assert_eq!(stack.projection_len(), 400, "build left rows in the tail");
let mut refs: Vec<&[u8]> = entries.iter().map(|e| e.oid.as_slice()).collect();
refs.extend(absent.iter().map(|e| e.oid.as_slice()));
let rs = stack.lookup_batch(&refs);
assert_eq!(
rs,
a.lookup_batch(&refs),
"the stack disagrees with FourTables"
);
assert_eq!(
rs,
b.lookup_batch(&refs),
"the stack disagrees with OneTableFourColumns"
);
assert_eq!(
stack.stats().tail_hits,
0,
"a fully absorbed stack still went to the tail for a row"
);
assert_eq!(stack.stats().absent, 100);
}
#[test]
fn a_complete_projection_answers_absent_without_asking_the_tail() {
let entries = synthetic_entries(200, 20, 91);
let nowhere = synthetic_entries(500, 20, 92);
let stack = Stack::build(&entries).unwrap();
assert!(stack.projection_is_complete());
let refs: Vec<&[u8]> = nowhere.iter().map(|e| e.oid.as_slice()).collect();
assert!(stack.lookup_batch(&refs).iter().all(Option::is_none));
assert!(stack.lookup(&nowhere[0].oid).is_none());
assert!(stack.ordinals_batch(&refs).iter().all(Option::is_none));
assert!(stack.extents_batch(&refs).iter().all(Option::is_none));
let st = stack.stats();
assert_eq!(st.unabsorbed_rows, 0);
assert_eq!(
st.tail_txns, 0,
"a complete projection opened {} redb transactions to say 'no'",
st.tail_txns
);
assert_eq!(st.absent, 500 + 1 + 500 + 500, "absences went uncounted");
let more = synthetic_entries(1, 20, 93);
stack.append(&more).unwrap();
assert!(!stack.projection_is_complete());
assert!(stack.lookup_batch(&refs).iter().all(Option::is_none));
assert!(
stack.stats().tail_txns > 0,
"an incomplete projection did not consult the tail"
);
assert!(
stack.lookup(&more[0].oid).is_some(),
"the appended row is not reachable"
);
}
#[test]
fn a_tail_in_a_foreign_layout_is_dropped_and_the_store_keeps_working() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("objects.tail.redb");
let all = synthetic_entries(90, 20, 41);
{
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
stack.append(&all).unwrap();
assert_eq!(stack.tail_epoch_and_len().unwrap().1, 90);
}
{
let db = Database::create(&path).unwrap();
let w = db.begin_write().unwrap();
{
let mut meta = w.open_table(META).unwrap();
meta.insert(META_TAIL_FORMAT, TAIL_FORMAT + 7).unwrap();
}
w.commit().unwrap();
}
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(
stack.tail_epoch_and_len().unwrap().1,
0,
"a foreign-layout tail was kept; the next read would bail on the row width"
);
assert_eq!(stack.projection_len(), 0);
stack.append(&all).unwrap();
assert_eq!(stack.tail_epoch_and_len().unwrap().1, 90);
let got = stack.lookup(&all[7].oid).expect("re-appended row");
assert_eq!(got.offset, all[7].offset);
drop(stack);
let again = ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(
again.tail_epoch_and_len().unwrap().1,
90,
"the rewritten version did not stick, so every open would drop the tail"
);
}
#[test]
fn a_tail_with_no_version_stamp_is_adopted_and_stamped_not_dropped() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("objects.tail.redb");
let all = synthetic_entries(50, 20, 41);
{
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
stack.append(&all).unwrap();
}
{
let db = Database::create(&path).unwrap();
let w = db.begin_write().unwrap();
{
let mut meta = w.open_table(META).unwrap();
meta.remove(META_TAIL_FORMAT).unwrap();
}
w.commit().unwrap();
}
let stack = ObjectReadStack::<OneTableFourColumns>::open(
&path,
RebuildTriggers::manual(),
DEFAULT_REDB_CACHE_BYTES,
)
.unwrap();
assert_eq!(stack.tail_epoch_and_len().unwrap().1, 50, "an unstamped tail was dropped");
let r = stack.tail.begin_read().unwrap();
let meta = r.open_table(META).unwrap();
assert_eq!(
meta.get(META_TAIL_FORMAT).unwrap().map(|v| v.value()),
Some(TAIL_FORMAT),
"the tail was adopted but not stamped, so it is re-examined for ever"
);
}
}