use std::{collections::BTreeMap, sync::Arc};
use reifydb_runtime::sync::mutex::Mutex;
use crate::lifecycle::class::RetentionClass;
#[derive(Clone)]
pub struct RetentionCoverage {
owners: Arc<Mutex<BTreeMap<RetentionClass, &'static str>>>,
absences: Arc<Mutex<BTreeMap<RetentionClass, &'static str>>>,
}
impl RetentionCoverage {
pub fn new() -> Self {
Self {
owners: Arc::new(Mutex::new(BTreeMap::new())),
absences: Arc::new(Mutex::new(BTreeMap::new())),
}
}
pub fn cover(&self, class: RetentionClass, owner: &'static str) {
self.owners.lock().entry(class).or_insert(owner);
}
pub fn absent(&self, class: RetentionClass, reason: &'static str) {
self.absences.lock().entry(class).or_insert(reason);
}
pub fn owner(&self, class: RetentionClass) -> Option<&'static str> {
self.owners.lock().get(&class).copied()
}
pub fn absence(&self, class: RetentionClass) -> Option<&'static str> {
self.absences.lock().get(&class).copied()
}
pub fn is_covered(&self, class: RetentionClass) -> bool {
self.owners.lock().contains_key(&class)
}
pub fn len(&self) -> usize {
self.owners.lock().len()
}
pub fn is_empty(&self) -> bool {
self.owners.lock().is_empty()
}
}
impl Default for RetentionCoverage {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_class_reclaimed_outside_the_lifecycle_subsystem_still_counts_as_covered() {
let coverage = RetentionCoverage::new();
coverage.cover(RetentionClass::RowTtl, "retention-evict-silent");
coverage.cover(RetentionClass::CdcTruncate, "cdc-subsystem");
assert_eq!(coverage.owner(RetentionClass::CdcTruncate), Some("cdc-subsystem"));
assert!(coverage.is_covered(RetentionClass::RowTtl));
assert!(
!coverage.is_covered(RetentionClass::EpochLog),
"a class nobody claimed must stay uncovered so the report can still name it"
);
}
#[test]
fn the_first_owner_of_a_class_keeps_it() {
let coverage = RetentionCoverage::new();
coverage.cover(RetentionClass::CdcTruncate, "cdc-truncate");
coverage.cover(RetentionClass::CdcTruncate, "someone-else");
assert_eq!(coverage.owner(RetentionClass::CdcTruncate), Some("cdc-truncate"));
assert_eq!(coverage.len(), 1, "a second claim must not create a second entry");
}
#[test]
fn a_fresh_registry_claims_nothing() {
let coverage = RetentionCoverage::new();
assert!(coverage.is_empty());
for class in RetentionClass::all() {
assert!(!coverage.is_covered(*class), "{} must start uncovered", class.name());
assert!(coverage.absence(*class).is_none(), "{} must start with no absence", class.name());
}
}
#[test]
fn a_lane_declared_absent_is_explained_without_becoming_covered() {
let coverage = RetentionCoverage::new();
coverage.absent(RetentionClass::CdcTruncate, "no cdc store registered");
assert_eq!(coverage.absence(RetentionClass::CdcTruncate), Some("no cdc store registered"));
assert!(
!coverage.is_covered(RetentionClass::CdcTruncate),
"an absent lane has no executor, so it must not count as covered"
);
assert_eq!(coverage.owner(RetentionClass::CdcTruncate), None);
}
#[test]
fn covering_a_class_never_declares_it_absent() {
let coverage = RetentionCoverage::new();
coverage.cover(RetentionClass::EpochLog, "epoch-log");
assert!(
coverage.absence(RetentionClass::EpochLog).is_none(),
"a class with an executor has no absence to report"
);
}
#[test]
fn the_first_reason_a_lane_is_declared_absent_is_the_one_reported() {
let coverage = RetentionCoverage::new();
coverage.absent(RetentionClass::CdcTruncate, "no cdc store registered");
coverage.absent(RetentionClass::CdcTruncate, "some later excuse");
assert_eq!(coverage.absence(RetentionClass::CdcTruncate), Some("no cdc store registered"));
}
}