use std::collections::BTreeMap;
use chronix_core::ShardId;
#[derive(Debug, Clone)]
pub struct RetentionResult {
pub shards_dropped: usize,
pub segments_deleted: usize,
pub segments_preserved: usize,
pub segments_awaiting_readers: usize,
pub bytes_freed: u64,
}
#[must_use]
pub fn shards_to_drop(
shard_bounds: &BTreeMap<ShardId, (i64, i64)>,
cutoff_ts: i64,
) -> Vec<ShardId> {
shard_bounds
.iter()
.filter(|&(_, &(_, max_ts))| max_ts < cutoff_ts)
.map(|(&shard_id, _)| shard_id)
.collect()
}
#[must_use]
pub fn retention_reference(now_ns: i64, newest_data_ns: Option<i64>) -> i64 {
match newest_data_ns {
Some(newest) => now_ns.min(newest),
None => now_ns,
}
}
#[must_use]
pub fn retention_cutoff(reference_ns: i64, retention_ns: i64) -> i64 {
reference_ns.saturating_sub(retention_ns).max(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_shards_to_drop_when_all_recent() {
let mut bounds = BTreeMap::new();
bounds.insert(ShardId(1), (100, 200));
bounds.insert(ShardId(2), (200, 300));
let cutoff = 50; assert!(shards_to_drop(&bounds, cutoff).is_empty());
}
#[test]
fn drops_old_shards() {
let mut bounds = BTreeMap::new();
bounds.insert(ShardId(1), (100, 200));
bounds.insert(ShardId(2), (300, 400));
bounds.insert(ShardId(3), (500, 600));
let cutoff = 450; let dropped = shards_to_drop(&bounds, cutoff);
assert_eq!(dropped.len(), 2);
assert!(dropped.contains(&ShardId(1)));
assert!(dropped.contains(&ShardId(2)));
}
#[test]
fn boundary_shard_not_dropped() {
let mut bounds = BTreeMap::new();
bounds.insert(ShardId(1), (100, 200));
assert!(shards_to_drop(&bounds, 200).is_empty());
}
#[test]
fn retention_cutoff_basic() {
assert_eq!(retention_cutoff(1_000_000, 500_000), 500_000);
}
#[test]
fn retention_cutoff_no_underflow() {
assert_eq!(retention_cutoff(100, 1_000_000), 0);
}
#[test]
fn the_reference_is_the_clock_while_the_data_keeps_up() {
assert_eq!(retention_reference(1_000, Some(999)), 999);
assert_eq!(retention_reference(1_000, Some(1_000)), 1_000);
}
#[test]
fn a_clock_that_jumps_forward_cannot_expire_more_than_the_data_allows() {
let newest = 1_000_i64;
let clock_in_the_next_century = 3_000_000_000_000_i64;
assert_eq!(
retention_reference(clock_in_the_next_century, Some(newest)),
newest,
);
}
#[test]
fn a_database_that_stopped_writing_stops_expiring() {
let month = 30 * 86_400_000_000_000_i64;
let week = 7 * 86_400_000_000_000_i64;
let now = 100 * month;
let newest = now - month;
let cutoff = retention_cutoff(retention_reference(now, Some(newest)), week);
assert!(
cutoff < newest,
"the newest data must survive its own retention window",
);
}
#[test]
fn an_empty_database_falls_back_to_the_clock() {
assert_eq!(retention_reference(1_000, None), 1_000);
}
}