use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Freshness {
Fresh,
Unknown,
Stale,
}
impl Default for Freshness {
fn default() -> Self {
Freshness::Unknown
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FreshnessEvidence {
Offline,
SourceAge(Duration),
FresherTip {
latest_seq: u128,
slice_as_of: u128,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FreshnessPolicy {
pub max_age: Duration,
pub trust_unknown: bool,
}
impl Default for FreshnessPolicy {
fn default() -> Self {
Self {
max_age: Duration::from_secs(24 * 60 * 60),
trust_unknown: true,
}
}
}
impl FreshnessPolicy {
pub fn strict(max_age: Duration) -> Self {
Self {
max_age,
trust_unknown: false,
}
}
pub fn classify(&self, evidence: FreshnessEvidence) -> Freshness {
match evidence {
FreshnessEvidence::Offline => Freshness::Unknown,
FreshnessEvidence::SourceAge(age) if age <= self.max_age => Freshness::Fresh,
FreshnessEvidence::SourceAge(_) => Freshness::Stale,
FreshnessEvidence::FresherTip {
latest_seq,
slice_as_of,
} if latest_seq > slice_as_of => Freshness::Stale,
FreshnessEvidence::FresherTip { .. } => Freshness::Fresh,
}
}
pub fn trusts(&self, freshness: Freshness) -> bool {
match freshness {
Freshness::Fresh => true,
Freshness::Unknown => self.trust_unknown,
Freshness::Stale => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_names_the_oracle_offline_is_unknown() {
let p = FreshnessPolicy::default(); assert_eq!(p.classify(FreshnessEvidence::Offline), Freshness::Unknown);
assert_eq!(
p.classify(FreshnessEvidence::SourceAge(Duration::from_secs(3600))),
Freshness::Fresh
);
assert_eq!(
p.classify(FreshnessEvidence::SourceAge(Duration::from_secs(25 * 3600))),
Freshness::Stale
);
}
#[test]
fn classify_positional_slice_behind_a_fresher_tip_is_stale() {
let p = FreshnessPolicy::default();
assert_eq!(
p.classify(FreshnessEvidence::FresherTip {
latest_seq: 5,
slice_as_of: 3,
}),
Freshness::Stale
);
assert_eq!(
p.classify(FreshnessEvidence::FresherTip {
latest_seq: 3,
slice_as_of: 3,
}),
Freshness::Fresh
);
}
#[test]
fn strict_denies_unknown_and_stale_default_tolerates_unknown() {
let strict = FreshnessPolicy::strict(Duration::from_secs(3600));
assert!(strict.trusts(Freshness::Fresh));
assert!(
!strict.trusts(Freshness::Unknown),
"strict denies offline-unknown"
);
assert!(!strict.trusts(Freshness::Stale));
assert!(FreshnessPolicy::default().trusts(Freshness::Unknown));
assert!(!FreshnessPolicy::default().trusts(Freshness::Stale));
}
#[test]
fn the_default_window_is_24h() {
assert_eq!(
FreshnessPolicy::default().max_age,
Duration::from_secs(86_400)
);
}
}