use std::collections::HashSet;
use std::error::Error;
use std::sync::Arc;
use slatedb_txn_obj::Invariant;
use ulid::Ulid;
use crate::error::SlateDBError;
use crate::manifest::Manifest;
#[allow(dead_code)]
pub(crate) fn l0_ulid_cutoff(
dirty: &Manifest,
current: &Manifest,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let Some(watermark) = current.core.max_l0_ulid_timestamp_across_trees() else {
return Ok(());
};
let existing: HashSet<Ulid> = current
.core
.trees()
.flat_map(|tree| tree.l0.iter().map(|view| view.sst.id.unwrap_compacted_id()))
.collect();
for view in dirty.core.trees().flat_map(|tree| tree.l0.iter()) {
let sst_id = view.sst.id.unwrap_compacted_id();
if existing.contains(&sst_id) {
continue;
}
if sst_id.timestamp_ms() < watermark.timestamp_ms() {
return Err(Box::new(SlateDBError::InvalidClockTick {
last_tick: watermark.timestamp_ms() as i64,
next_tick: sst_id.timestamp_ms() as i64,
}));
}
}
Ok(())
}
#[allow(dead_code)]
pub(crate) fn manifest_invariants() -> Vec<Invariant<Manifest>> {
vec![Arc::new(l0_ulid_cutoff)]
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::sync::Arc;
use ulid::Ulid;
use super::*;
use crate::db_state::{SsTableHandle, SsTableId, SsTableInfo, SsTableView};
use crate::format::sst::SST_FORMAT_VERSION_LATEST;
use crate::manifest::{LsmTreeState, ManifestCore, Segment};
fn l0_view(ts_ms: u64, rand: u128) -> SsTableView {
let view_id = Ulid::from_parts(ts_ms, rand);
let handle = SsTableHandle::new(
SsTableId::Compacted(view_id),
SST_FORMAT_VERSION_LATEST,
SsTableInfo::default(),
);
SsTableView::new(view_id, handle)
}
fn l0_view_split(view_ts_ms: u64, sst_ts_ms: u64) -> SsTableView {
let view_id = Ulid::from_parts(view_ts_ms, 0);
let handle = SsTableHandle::new(
SsTableId::Compacted(Ulid::from_parts(sst_ts_ms, 0)),
SST_FORMAT_VERSION_LATEST,
SsTableInfo::default(),
);
SsTableView::new(view_id, handle)
}
fn manifest_with_root_l0(ts: &[u64]) -> Manifest {
let mut core = ManifestCore::new();
Arc::make_mut(&mut core.tree).l0 = ts
.iter()
.enumerate()
.map(|(i, &t)| l0_view(t, i as u128))
.collect();
Manifest::initial(core)
}
#[test]
fn fresh_current_accepts_any_dirty_l0() {
let current = manifest_with_root_l0(&[]);
let dirty = manifest_with_root_l0(&[1]);
l0_ulid_cutoff(&dirty, ¤t).unwrap();
}
#[test]
fn dirty_adds_l0_above_watermark_ok() {
let current = manifest_with_root_l0(&[100]);
let dirty = manifest_with_root_l0(&[100, 101]);
l0_ulid_cutoff(&dirty, ¤t).unwrap();
}
#[test]
fn dirty_adds_l0_below_watermark_errors() {
let current = manifest_with_root_l0(&[100]);
let dirty = manifest_with_root_l0(&[100, 50]);
let err = l0_ulid_cutoff(&dirty, ¤t).unwrap_err();
let err = err.downcast::<SlateDBError>().unwrap();
assert!(matches!(
*err,
SlateDBError::InvalidClockTick {
last_tick: 100,
next_tick: 50
}
));
}
#[test]
fn dirty_adds_l0_equal_to_watermark_ok() {
let current = manifest_with_root_l0(&[100]);
let dirty = manifest_with_root_l0(&[100, 100]);
l0_ulid_cutoff(&dirty, ¤t).unwrap();
}
#[test]
fn reincluding_existing_l0_is_idempotent() {
let current = manifest_with_root_l0(&[100, 200]);
let dirty = manifest_with_root_l0(&[100, 200]);
l0_ulid_cutoff(&dirty, ¤t).unwrap();
}
#[test]
fn watermark_uses_last_compacted_marker() {
let mut core = ManifestCore::new();
Arc::make_mut(&mut core.tree).last_compacted_l0_sst_view_id =
Some(Ulid::from_parts(300, 0));
let current = Manifest::initial(core);
let dirty = manifest_with_root_l0(&[250]);
let err = l0_ulid_cutoff(&dirty, ¤t).unwrap_err();
let err = err.downcast::<SlateDBError>().unwrap();
assert!(matches!(
*err,
SlateDBError::InvalidClockTick {
last_tick: 300,
next_tick: 250
}
));
}
#[test]
fn watermark_is_max_across_all_trees() {
let mut current_core = ManifestCore::new();
current_core.segments.push(Segment {
prefix: bytes::Bytes::from_static(b"seg"),
tree: Arc::new(LsmTreeState {
l0: VecDeque::from(vec![l0_view(400, 0)]),
..LsmTreeState::default()
}),
});
let current = Manifest::initial(current_core);
let mut dirty_core = ManifestCore::new();
Arc::make_mut(&mut dirty_core.tree).l0 = VecDeque::from(vec![l0_view(350, 0)]);
dirty_core.segments.push(Segment {
prefix: bytes::Bytes::from_static(b"seg"),
tree: Arc::new(LsmTreeState {
l0: VecDeque::from(vec![l0_view(400, 0)]),
..LsmTreeState::default()
}),
});
let dirty = Manifest::initial(dirty_core);
let err = l0_ulid_cutoff(&dirty, ¤t).unwrap_err();
let err = err.downcast::<SlateDBError>().unwrap();
assert!(matches!(
*err,
SlateDBError::InvalidClockTick {
last_tick: 400,
next_tick: 350
}
));
}
#[test]
fn check_uses_physical_sst_ulid_not_view_ulid() {
let current = manifest_with_root_l0(&[100]);
let mut dirty_core = ManifestCore::new();
Arc::make_mut(&mut dirty_core.tree).l0 = VecDeque::from(vec![l0_view_split(999, 50)]);
let dirty = Manifest::initial(dirty_core);
let err = l0_ulid_cutoff(&dirty, ¤t).unwrap_err();
let err = err.downcast::<SlateDBError>().unwrap();
assert!(matches!(
*err,
SlateDBError::InvalidClockTick {
last_tick: 100,
next_tick: 50
}
));
}
}