use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub const REVALIDATE_AFTER: Duration = Duration::from_secs(2);
pub struct Freshness {
window: Duration,
verified: Mutex<Option<Instant>>,
building: AtomicBool,
writes: AtomicU64,
}
impl Default for Freshness {
fn default() -> Self {
Self::new(REVALIDATE_AFTER)
}
}
impl Freshness {
pub fn new(window: Duration) -> Self {
Self {
window,
verified: Mutex::new(None),
building: AtomicBool::new(false),
writes: AtomicU64::new(0),
}
}
pub fn building(&self) -> bool {
self.building.load(Ordering::SeqCst)
}
pub fn begin_build(&self) {
self.building.store(true, Ordering::SeqCst);
}
pub fn end_build(&self) {
self.building.store(false, Ordering::SeqCst);
}
pub fn fresh(&self) -> bool {
self.verified
.lock()
.ok()
.and_then(|at| *at)
.is_some_and(|at| at.elapsed() < self.window)
}
pub fn generation(&self) -> u64 {
self.writes.load(Ordering::SeqCst)
}
pub fn vouch(&self, generation: u64) {
if self.generation() != generation {
return;
}
if let Ok(mut at) = self.verified.lock() {
*at = Some(Instant::now());
}
}
pub fn note_write(&self) {
self.writes.fetch_add(1, Ordering::SeqCst);
if let Ok(mut at) = self.verified.lock() {
*at = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn freshness() -> Freshness {
Freshness::new(Duration::from_secs(60))
}
#[test]
fn nothing_is_trusted_before_a_refresh_has_vouched() {
assert!(!freshness().fresh());
}
#[test]
fn a_refresh_that_ran_alone_is_trusted() {
let f = freshness();
let generation = f.generation();
f.vouch(generation);
assert!(f.fresh());
}
#[test]
fn a_write_during_a_refresh_stops_it_vouching() {
let f = freshness();
let generation = f.generation();
f.note_write();
f.vouch(generation);
assert!(!f.fresh(), "the refresh vouched for mail it had not read");
}
#[test]
fn a_write_after_a_refresh_retracts_the_vouching() {
let f = freshness();
f.vouch(f.generation());
f.note_write();
assert!(!f.fresh());
}
#[test]
fn a_later_refresh_can_vouch_again_after_a_write() {
let f = freshness();
f.note_write();
f.vouch(f.generation());
assert!(f.fresh());
}
#[test]
fn the_window_expires() {
let f = Freshness::new(Duration::ZERO);
f.vouch(f.generation());
assert!(!f.fresh());
}
#[test]
fn building_is_reported_while_it_lasts() {
let f = freshness();
assert!(!f.building());
f.begin_build();
assert!(f.building());
f.end_build();
assert!(!f.building());
}
}