use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GateState {
Fresh,
Stale,
Lost,
}
impl GateState {
pub fn is_usable(self) -> bool {
matches!(self, GateState::Fresh)
}
}
#[derive(Debug, Clone)]
pub struct FreshnessGate {
budget_ms: i64,
last_arrival_ms: Option<i64>,
link_up: bool,
}
impl FreshnessGate {
pub fn new(budget_ms: i64) -> Self {
Self {
budget_ms: budget_ms.max(0),
last_arrival_ms: None,
link_up: true,
}
}
pub fn record(&mut self, now_ms: i64) {
self.last_arrival_ms = Some(now_ms);
}
pub fn set_link(&mut self, up: bool) {
self.link_up = up;
}
pub fn state(&self, now_ms: i64) -> GateState {
if !self.link_up {
return GateState::Lost;
}
match self.last_arrival_ms {
None => GateState::Lost,
Some(arrival) if now_ms.saturating_sub(arrival) <= self.budget_ms => GateState::Fresh,
Some(_) => GateState::Stale,
}
}
pub fn is_usable(&self, now_ms: i64) -> bool {
self.state(now_ms).is_usable()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LockState {
Unlocked,
Locked,
Lost,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LockGate {
state: LockState,
ever_usable_since_lock: bool,
}
impl Default for LockGate {
fn default() -> Self {
Self {
state: LockState::Unlocked,
ever_usable_since_lock: false,
}
}
}
impl LockGate {
pub fn new() -> Self {
Self::default()
}
pub fn state(&self) -> LockState {
self.state
}
pub fn is_locked(&self) -> bool {
self.state == LockState::Locked
}
pub fn lock(&mut self) {
self.state = LockState::Locked;
self.ever_usable_since_lock = false;
}
pub fn unlock(&mut self) {
self.state = LockState::Unlocked;
self.ever_usable_since_lock = false;
}
pub fn update(&mut self, stream_usable: bool) {
if self.state != LockState::Locked {
return;
}
if stream_usable {
self.ever_usable_since_lock = true;
} else if self.ever_usable_since_lock {
self.state = LockState::Lost;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_gate_is_lost_until_a_result_arrives() {
let mut g = FreshnessGate::new(500);
assert_eq!(g.state(1000), GateState::Lost);
g.record(1000);
assert_eq!(g.state(1100), GateState::Fresh); }
#[test]
fn a_result_past_the_budget_is_stale() {
let mut g = FreshnessGate::new(500);
g.record(1000);
assert_eq!(g.state(1400), GateState::Fresh); assert_eq!(g.state(1501), GateState::Stale); assert!(!g.is_usable(1501));
}
#[test]
fn a_link_drop_is_lost_even_with_a_fresh_result() {
let mut g = FreshnessGate::new(500);
g.record(1000);
g.set_link(false);
assert_eq!(g.state(1050), GateState::Lost);
g.set_link(true);
assert_eq!(g.state(1050), GateState::Fresh);
}
#[test]
fn a_frozen_stream_goes_stale_on_local_elapsed_not_the_result_timestamp() {
let mut g = FreshnessGate::new(500);
g.record(1000); assert_eq!(g.state(1400), GateState::Fresh);
assert_eq!(g.state(1501), GateState::Stale);
assert_eq!(g.state(100_000), GateState::Stale); }
#[test]
fn the_budget_boundary_is_inclusive() {
let mut g = FreshnessGate::new(500);
g.record(1000);
assert_eq!(g.state(1500), GateState::Fresh); assert_eq!(g.state(1501), GateState::Stale); }
#[test]
fn a_negative_budget_is_clamped_not_permanently_stale() {
let mut g = FreshnessGate::new(-100);
g.record(1000);
assert_eq!(g.state(1000), GateState::Fresh);
assert_eq!(g.state(1001), GateState::Stale);
}
#[test]
fn the_lock_gate_starts_unlocked() {
assert_eq!(LockGate::new().state(), LockState::Unlocked);
}
#[test]
fn a_locked_gate_stays_locked_while_the_stream_is_usable() {
let mut g = LockGate::new();
g.lock();
g.update(true);
assert_eq!(g.state(), LockState::Locked);
assert!(g.is_locked());
}
#[test]
fn a_lock_still_acquiring_stays_locked_not_lost() {
let mut g = LockGate::new();
g.lock();
g.update(false);
g.update(false);
assert_eq!(g.state(), LockState::Locked);
}
#[test]
fn a_tracking_gate_drops_to_lost_when_the_stream_goes_stale() {
let mut g = LockGate::new();
g.lock();
g.update(true); g.update(false); assert_eq!(g.state(), LockState::Lost);
assert!(!g.is_locked());
}
#[test]
fn a_lost_gate_never_auto_re_acquires() {
let mut g = LockGate::new();
g.lock();
g.update(true); g.update(false); assert_eq!(g.state(), LockState::Lost);
g.update(true);
g.update(true);
assert_eq!(g.state(), LockState::Lost);
g.lock();
assert_eq!(g.state(), LockState::Locked);
}
#[test]
fn an_unlocked_gate_does_not_lock_itself_on_a_usable_stream() {
let mut g = LockGate::new();
g.update(true); assert_eq!(g.state(), LockState::Unlocked);
}
#[test]
fn unlock_returns_to_idle() {
let mut g = LockGate::new();
g.lock();
g.unlock();
assert_eq!(g.state(), LockState::Unlocked);
}
}