use reifydb_value::value::{datetime::DateTime, duration::Duration};
use crate::operator::state::seal::coord::Coord;
pub const SEAL_GATE_STEP: Duration = Duration::from_milliseconds_const(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct AdmissibleSpan(Duration);
impl AdmissibleSpan {
pub fn duration(self) -> Duration {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SealInstant(DateTime);
impl SealInstant {
pub fn at(self) -> DateTime {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct EvictionInstant(DateTime);
impl EvictionInstant {
pub fn at(self) -> DateTime {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SealedThrough(DateTime);
impl SealedThrough {
pub fn from_order(order: u64) -> Self {
Self(<DateTime as Coord>::from_order(order))
}
pub fn at(self) -> DateTime {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SealRule {
admissible: AdmissibleSpan,
}
impl SealRule {
pub fn tumbling(size: Duration, lateness: Duration) -> Self {
Self::extended_by_seal(size, lateness)
}
pub fn sliding(size: Duration, lateness: Duration) -> Self {
Self::extended_by_seal(size, lateness)
}
pub fn session(gap: Duration, lateness: Duration) -> Self {
Self::extended_by_seal(gap, lateness)
}
pub fn rolling(span: Duration, lateness: Duration) -> Self {
Self::extended_by_seal(span, lateness)
}
pub fn of(admissible: Duration) -> Self {
Self {
admissible: AdmissibleSpan(admissible),
}
}
fn extended_by_seal(base: Duration, lateness: Duration) -> Self {
Self {
admissible: AdmissibleSpan(base.try_add(lateness).unwrap_or(base)),
}
}
pub fn admissible(self) -> AdmissibleSpan {
self.admissible
}
pub fn is_inert(self) -> bool {
self.admissible.0.is_zero()
}
pub fn seal_instant(self, event: DateTime) -> SealInstant {
SealInstant(event.saturating_add(self.admissible.0).saturating_add(SEAL_GATE_STEP))
}
pub fn seal_instant_from_order(self, event_order: u64) -> SealInstant {
self.seal_instant(<DateTime as Coord>::from_order(event_order))
}
pub fn horizon_at(self, at: DateTime) -> Option<DateTime> {
at.checked_sub(self.admissible.0).and_then(|horizon| horizon.checked_sub(SEAL_GATE_STEP))
}
}
pub fn seal_horizon<C: Coord>(watermark: C, lateness: C::Span) -> C {
watermark.saturating_sub_span(lateness)
}
pub fn is_sealed<C: Coord>(event: C, horizon: C) -> bool {
event < horizon
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EvictionRule {
span: Duration,
}
impl EvictionRule {
pub fn rolling(span: Duration) -> Self {
Self {
span,
}
}
pub fn eviction_instant(self, event: DateTime) -> EvictionInstant {
EvictionInstant(event.saturating_add(self.span))
}
pub fn eviction_instant_from_order(self, event_order: u64) -> EvictionInstant {
self.eviction_instant(<DateTime as Coord>::from_order(event_order))
}
}
#[cfg(test)]
mod tests {
use reifydb_value::factory::time::at_millis;
use super::*;
fn ms(millis: u64) -> Duration {
Duration::from_milliseconds_const(millis as i64)
}
#[test]
fn a_seal_instant_is_one_past_the_admissible_span() {
let rule = SealRule::tumbling(ms(1_000), ms(200));
assert_eq!(rule.admissible().duration(), ms(1_200));
assert_eq!(rule.seal_instant(at_millis(5_000)).at(), at_millis(6_201));
}
#[test]
fn the_horizon_trails_the_ledger_by_the_whole_admissible_span() {
let rule = SealRule::tumbling(ms(30_000), ms(45_000));
let ledger = at_millis(358_262);
let horizon = rule.horizon_at(ledger).expect("the ledger is past one admissible span");
assert_eq!(horizon, at_millis(283_261), "ledger - (size + lateness) - 1");
assert!(horizon < ledger, "a frontier at or past the ledger reclaims windows that have not sealed");
assert_eq!(
rule.seal_instant(horizon).at(),
ledger,
"and it is the exact inverse of arming, so a window at this horizon sealed at precisely \
this ledger rather than one millisecond either side of it"
);
}
#[test]
fn a_ledger_short_of_one_admissible_span_has_sealed_nothing() {
let rule = SealRule::tumbling(ms(30_000), ms(45_000));
assert_eq!(rule.horizon_at(at_millis(0)), None);
assert_eq!(rule.horizon_at(at_millis(75_000)), None, "the horizon would be 0 - 1, not 0");
assert_eq!(rule.horizon_at(at_millis(75_001)), Some(at_millis(0)));
}
#[test]
fn rolling_admission_carries_lateness_and_rolling_eviction_does_not() {
let admission = SealRule::rolling(ms(1_000), ms(200));
let eviction = EvictionRule::rolling(ms(1_000));
assert_eq!(admission.seal_instant(at_millis(5_000)).at(), at_millis(6_201));
assert_eq!(eviction.eviction_instant(at_millis(5_000)).at(), at_millis(6_000));
}
#[test]
fn an_eviction_instant_never_carries_the_strict_gate_plus_one() {
let eviction = EvictionRule::rolling(ms(0));
assert_eq!(eviction.eviction_instant(at_millis(7_000)).at(), at_millis(7_000));
}
#[test]
fn every_kind_admits_its_own_base_span_plus_lateness() {
assert_eq!(SealRule::tumbling(ms(1_000), ms(50)).admissible().duration(), ms(1_050));
assert_eq!(SealRule::sliding(ms(1_000), ms(50)).admissible().duration(), ms(1_050));
assert_eq!(SealRule::session(ms(300), ms(50)).admissible().duration(), ms(350));
assert_eq!(SealRule::rolling(ms(2_000), ms(50)).admissible().duration(), ms(2_050));
}
#[test]
fn no_lateness_can_make_the_admissible_span_shorter_than_the_window() {
let enormous = Duration::from_nanoseconds_const(i64::MAX);
for lateness in [ms(0), ms(1), enormous] {
let rule = SealRule::tumbling(ms(1_000), lateness);
assert!(
rule.admissible().duration() >= ms(1_000),
"admissible {:?} fell below the 1000ms window for lateness {lateness:?}",
rule.admissible().duration()
);
}
}
}