use std::fmt;
use reifydb_value::value::datetime::DateTime;
use crate::common::CommitVersion;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Floor {
Version(CommitVersion),
Instant(DateTime),
}
impl Floor {
pub fn monotonic_key(&self) -> u64 {
match self {
Self::Version(version) => version.0,
Self::Instant(instant) => instant.to_nanos(),
}
}
pub fn version(&self) -> Option<CommitVersion> {
match self {
Self::Version(version) => Some(*version),
Self::Instant(_) => None,
}
}
pub fn instant(&self) -> Option<DateTime> {
match self {
Self::Instant(instant) => Some(*instant),
Self::Version(_) => None,
}
}
pub fn is_same_domain(&self, other: &Self) -> bool {
matches!((self, other), (Self::Version(_), Self::Version(_)) | (Self::Instant(_), Self::Instant(_)))
}
}
impl fmt::Display for Floor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Version(version) => write!(f, "v{}", version.0),
Self::Instant(instant) => write!(f, "{instant}"),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum FloorTerm {
RowExpiry,
QueryDoneUntil,
LeaseMin,
ConsumerCheckpoint,
ConsumerPosition,
RetentionHorizon,
}
impl FloorTerm {
pub fn protects(&self) -> &'static str {
match self {
Self::RowExpiry => "rows younger than their declared ttl",
Self::QueryDoneUntil => "an in-flight query reading at its snapshot version",
Self::LeaseMin => "a held operator-state lease",
Self::ConsumerCheckpoint => "a CDC log consumer that has not yet consumed the version",
Self::ConsumerPosition => "a live flow that has not yet consumed the version",
Self::RetentionHorizon => "epoch samples still needed to resolve the longest declared ttl",
}
}
pub fn is_clock_driven(&self) -> bool {
match self {
Self::RowExpiry => true,
Self::QueryDoneUntil
| Self::LeaseMin
| Self::ConsumerCheckpoint
| Self::ConsumerPosition
| Self::RetentionHorizon => false,
}
}
pub fn all() -> &'static [Self] {
&[
Self::RowExpiry,
Self::QueryDoneUntil,
Self::LeaseMin,
Self::ConsumerCheckpoint,
Self::ConsumerPosition,
Self::RetentionHorizon,
]
}
pub fn index(&self) -> usize {
Self::all().iter().position(|term| term == self).expect("every term is listed in FloorTerm::all")
}
pub fn from_index(index: usize) -> Option<Self> {
Self::all().get(index).copied()
}
}
impl fmt::Display for FloorTerm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RowExpiry => write!(f, "row-expiry"),
Self::QueryDoneUntil => write!(f, "query-done-until"),
Self::LeaseMin => write!(f, "lease-min"),
Self::ConsumerCheckpoint => write!(f, "consumer-checkpoint"),
Self::ConsumerPosition => write!(f, "consumer-position"),
Self::RetentionHorizon => write!(f, "retention-horizon"),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum RetentionClass {
RowTtl,
BufferHistoricalGc,
PersistentFlush,
QueueLeaseReap,
QueueRetention,
CdcTruncate,
EpochLog,
}
impl RetentionClass {
pub fn all() -> &'static [Self] {
&[
Self::RowTtl,
Self::BufferHistoricalGc,
Self::PersistentFlush,
Self::QueueLeaseReap,
Self::QueueRetention,
Self::CdcTruncate,
Self::EpochLog,
]
}
pub fn name(&self) -> &'static str {
match self {
Self::RowTtl => "row-ttl-silent",
Self::BufferHistoricalGc => "buffer-historical-gc",
Self::PersistentFlush => "persistent-flush",
Self::QueueLeaseReap => "queue-lease-reap",
Self::QueueRetention => "queue-retention",
Self::CdcTruncate => "cdc-truncate",
Self::EpochLog => "epoch-log",
}
}
pub fn reclaims_versioned_data(&self) -> bool {
match self {
Self::RowTtl
| Self::BufferHistoricalGc
| Self::PersistentFlush
| Self::CdcTruncate
| Self::EpochLog
| Self::QueueRetention => true,
Self::QueueLeaseReap => false,
}
}
pub fn floor_terms(&self) -> &'static [FloorTerm] {
match self {
Self::RowTtl => &[FloorTerm::RowExpiry],
Self::BufferHistoricalGc => &[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin],
Self::PersistentFlush => {
&[FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin, FloorTerm::ConsumerPosition]
}
Self::QueueLeaseReap => &[],
Self::QueueRetention => &[FloorTerm::RowExpiry],
Self::CdcTruncate => &[FloorTerm::ConsumerCheckpoint],
Self::EpochLog => &[FloorTerm::RetentionHorizon],
}
}
pub fn constrained_by(&self, term: FloorTerm) -> bool {
self.floor_terms().contains(&term)
}
}
impl fmt::Display for RetentionClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::{FloorTerm, RetentionClass};
#[test]
fn every_class_declares_a_floor_term_for_exactly_the_data_it_reclaims() {
for class in RetentionClass::all() {
if class.reclaims_versioned_data() {
assert!(
!class.floor_terms().is_empty(),
"{class} declares no floor term, so nothing bounds what it deletes"
);
} else {
assert!(
class.floor_terms().is_empty(),
"{class} reclaims no versioned data, so a version floor cannot be what bounds it"
);
}
}
}
#[test]
fn class_names_are_unique_so_metrics_and_reports_cannot_collide() {
let mut names: Vec<&str> = RetentionClass::all().iter().map(|c| c.name()).collect();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "two classes share a name; their metrics and report lines would merge");
}
#[test]
fn row_expiry_classes_are_not_hostage_to_readers_of_other_data() {
for class in [RetentionClass::RowTtl] {
assert!(
!class.constrained_by(FloorTerm::ConsumerCheckpoint),
"{class} must not be pinned by a CDC consumer; it reclaims rows no consumer reads"
);
assert!(
!class.constrained_by(FloorTerm::LeaseMin),
"{class} must not be pinned by an operator-state lease"
);
assert!(
!class.constrained_by(FloorTerm::QueryDoneUntil),
"{class} must not be pinned by an in-flight query; transactional discovery protects it"
);
}
}
#[test]
fn version_history_classes_respect_every_reader_of_a_snapshot() {
let class = RetentionClass::BufferHistoricalGc;
for term in [FloorTerm::QueryDoneUntil, FloorTerm::LeaseMin] {
assert!(
class.constrained_by(term),
"{class} must keep the {term} term: it protects {}",
term.protects()
);
}
}
#[test]
fn a_lagging_subscription_must_not_pin_buffer_history_between_batches() {
assert!(
!RetentionClass::BufferHistoricalGc.constrained_by(FloorTerm::ConsumerCheckpoint),
"a CDC log consumer reads cdc.db, not buffer history, and must not pin it"
);
assert!(
RetentionClass::BufferHistoricalGc.constrained_by(FloorTerm::LeaseMin),
"an in-flight subscription batch protects its reads through its lease, so LeaseMin must stay"
);
}
#[test]
fn the_flush_floor_tracks_live_positions_while_cdc_truncation_tracks_durable_checkpoints() {
assert!(
RetentionClass::PersistentFlush.constrained_by(FloorTerm::ConsumerPosition),
"flushing the in-memory buffer may only be held back by a reader that is live now"
);
assert!(
!RetentionClass::PersistentFlush.constrained_by(FloorTerm::ConsumerCheckpoint),
"a throttled durable checkpoint lags the real read position and must not pin the buffer"
);
assert!(
RetentionClass::CdcTruncate.constrained_by(FloorTerm::ConsumerCheckpoint),
"cdc.db must retain everything a consumer would replay from after a crash"
);
assert!(
!RetentionClass::CdcTruncate.constrained_by(FloorTerm::ConsumerPosition),
"a live position is lost on restart, so it cannot govern durable CDC truncation"
);
}
#[test]
fn cdc_truncation_is_pinned_only_by_its_consumers() {
let class = RetentionClass::CdcTruncate;
assert!(
class.constrained_by(FloorTerm::ConsumerCheckpoint),
"the slowest CDC log consumer legitimately pins CDC"
);
assert!(
!class.constrained_by(FloorTerm::QueryDoneUntil),
"an in-flight query does not read the CDC log"
);
assert!(
!class.constrained_by(FloorTerm::LeaseMin),
"an operator-state lease does not read the CDC log"
);
}
#[test]
fn the_epoch_log_is_bounded_by_the_longest_ttl_it_must_still_answer() {
assert!(
RetentionClass::EpochLog.constrained_by(FloorTerm::RetentionHorizon),
"pruning epoch samples inside the retention horizon would make long ttls unresolvable"
);
}
}