use reifydb_codec::key::encoded::EncodedKey;
use reifydb_core::state::store::{TimerKind, TimerStore};
use reifydb_value::{
Result,
value::{datetime::DateTime, duration::Duration},
};
use crate::operator::state::seal::policy::{EvictionPolicy, SealPolicy, SealedThrough};
pub struct SealGate {
policy: SealPolicy,
frontier: DateTime,
}
impl SealGate {
pub fn new(policy: SealPolicy, ledger: Option<SealedThrough>, watermark: Option<DateTime>) -> Self {
let ledger = ledger.map_or_else(DateTime::default, SealedThrough::at);
Self {
policy,
frontier: watermark.map_or(ledger, |watermark| ledger.max(watermark)),
}
}
pub fn policy(&self) -> SealPolicy {
self.policy
}
pub fn frontier(&self) -> DateTime {
self.frontier
}
pub fn admits(&self, horizon: u64) -> bool {
self.policy.seal_instant_from_order(horizon).at() > self.frontier
}
pub fn arm(
&self,
store: &mut dyn TimerStore,
key: &EncodedKey,
prior_horizon: Option<u64>,
horizon: u64,
) -> Result<()> {
let at = self.policy.seal_instant_from_order(horizon);
if let Some(prior_horizon) = prior_horizon {
let prior_at = self.policy.seal_instant_from_order(prior_horizon);
if prior_at != at {
store.disarm_timer(prior_at.at(), TimerKind::Seal, key)?;
}
}
store.arm_timer(at.at(), TimerKind::Seal, key)
}
}
pub fn disarm_seal(store: &mut dyn TimerStore, policy: SealPolicy, key: &EncodedKey, horizon: u64) -> Result<()> {
store.disarm_timer(policy.seal_instant_from_order(horizon).at(), TimerKind::Seal, key)
}
pub struct EvictionGate {
policy: EvictionPolicy,
}
impl EvictionGate {
pub fn new(span: Duration) -> Self {
Self {
policy: EvictionPolicy::rolling(span),
}
}
pub fn rearm(
&self,
store: &mut dyn TimerStore,
key: &EncodedKey,
before: Option<u64>,
after: Option<u64>,
) -> Result<()> {
if before == after {
return Ok(());
}
if let Some(before) = before {
store.disarm_timer(self.policy.eviction_instant_from_order(before).at(), TimerKind::Seal, key)?;
}
if let Some(after) = after {
store.arm_timer(self.policy.eviction_instant_from_order(after).at(), TimerKind::Seal, key)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use reifydb_value::factory::time::at_millis;
use super::*;
use crate::operator::state::{
mock::{MockStore, RecordedTimer},
seal::coord::Coord,
};
fn ms(millis: u64) -> Duration {
Duration::from_milliseconds_const(millis as i64)
}
fn key() -> EncodedKey {
EncodedKey::new(b"window".as_slice())
}
fn policy() -> SealPolicy {
SealPolicy::tumbling(ms(1_000), ms(200))
}
fn order(millis: u64) -> u64 {
at_millis(millis).to_order()
}
fn sealed_through(millis: u64) -> SealedThrough {
SealedThrough::from_order(order(millis))
}
#[test]
fn the_frontier_is_the_ledger_and_the_watermark_merged_upward() {
let lagging = SealGate::new(policy(), Some(sealed_through(9_000)), Some(at_millis(3_000)));
let leading = SealGate::new(policy(), Some(sealed_through(3_000)), Some(at_millis(9_000)));
assert_eq!(lagging.frontier(), at_millis(9_000));
assert_eq!(leading.frontier(), at_millis(9_000));
}
#[test]
fn an_empty_ledger_and_no_watermark_leave_the_frontier_at_the_epoch() {
let gate = SealGate::new(policy(), None, None);
assert_eq!(gate.frontier(), DateTime::default());
assert!(gate.admits(order(0)));
}
#[test]
fn the_guest_reaches_the_same_frontier_as_the_host_from_the_same_two_inputs() {
let ledger = sealed_through(9_000);
let watermark = at_millis(3_000);
let host_side = SealGate::new(policy(), Some(ledger), Some(watermark));
let guest_side = SealGate::new(policy(), Some(ledger), Some(watermark));
assert_eq!(host_side.frontier(), guest_side.frontier());
assert_eq!(host_side.admits(order(5_000)), guest_side.admits(order(5_000)));
}
#[test]
fn a_window_is_admitted_until_the_frontier_passes_its_whole_admissible_span() {
let gate = SealGate::new(policy(), Some(sealed_through(6_201)), None);
assert!(!gate.admits(order(5_000)), "5_000 + 1_200 + 1 == the frontier, so it is sealed");
assert!(gate.admits(order(5_001)), "one millisecond later the seal instant is past the frontier");
}
#[test]
fn arming_a_moved_horizon_disarms_the_instant_it_replaces() {
let mut store = MockStore::recording_timers();
let gate = SealGate::new(policy(), None, None);
gate.arm(&mut store, &key(), Some(order(4_000)), order(5_000)).unwrap();
assert_eq!(
store.timers(),
&[
RecordedTimer::disarmed(at_millis(5_201), TimerKind::Seal, key()),
RecordedTimer::armed(at_millis(6_201), TimerKind::Seal, key()),
]
);
}
#[test]
fn arming_an_unmoved_horizon_leaves_the_existing_timer_alone() {
let mut store = MockStore::recording_timers();
let gate = SealGate::new(policy(), None, None);
gate.arm(&mut store, &key(), Some(order(5_000)), order(5_000)).unwrap();
assert_eq!(store.timers(), &[RecordedTimer::armed(at_millis(6_201), TimerKind::Seal, key())]);
}
#[test]
fn a_window_seen_for_the_first_time_arms_without_a_disarm() {
let mut store = MockStore::recording_timers();
let gate = SealGate::new(policy(), None, None);
gate.arm(&mut store, &key(), None, order(5_000)).unwrap();
assert_eq!(store.timers(), &[RecordedTimer::armed(at_millis(6_201), TimerKind::Seal, key())]);
}
#[test]
fn eviction_rearms_on_the_bare_span_and_never_on_the_seal_instant() {
let mut store = MockStore::recording_timers();
let gate = EvictionGate::new(ms(1_000));
gate.rearm(&mut store, &key(), Some(order(4_000)), Some(order(5_000))).unwrap();
assert_eq!(
store.timers(),
&[
RecordedTimer::disarmed(at_millis(5_000), TimerKind::Seal, key()),
RecordedTimer::armed(at_millis(6_000), TimerKind::Seal, key()),
]
);
}
#[test]
fn an_unchanged_oldest_coordinate_touches_no_timer_at_all() {
let mut store = MockStore::recording_timers();
let gate = EvictionGate::new(ms(1_000));
gate.rearm(&mut store, &key(), Some(order(4_000)), Some(order(4_000))).unwrap();
gate.rearm(&mut store, &key(), None, None).unwrap();
assert!(store.timers().is_empty());
}
#[test]
fn a_group_that_empties_disarms_without_arming_anything_new() {
let mut store = MockStore::recording_timers();
let gate = EvictionGate::new(ms(1_000));
gate.rearm(&mut store, &key(), Some(order(4_000)), None).unwrap();
assert_eq!(store.timers(), &[RecordedTimer::disarmed(at_millis(5_000), TimerKind::Seal, key())]);
}
#[test]
fn disarming_targets_the_seal_instant_the_horizon_resolves_to() {
let mut store = MockStore::recording_timers();
let gate = SealGate::new(policy(), None, None);
gate.arm(&mut store, &key(), None, order(5_000)).unwrap();
disarm_seal(&mut store, policy(), &key(), order(5_000)).unwrap();
assert_eq!(
store.timers(),
&[
RecordedTimer::armed(at_millis(6_201), TimerKind::Seal, key()),
RecordedTimer::disarmed(at_millis(6_201), TimerKind::Seal, key()),
]
);
}
}