use std::sync::Arc;
use std::time::Instant;
pub const DEFAULT_SESSION_WALL_SECS: u64 = 3600;
#[derive(Debug)]
pub struct SessionDeadline {
started: Instant,
max_wall_secs: Option<u64>,
}
impl SessionDeadline {
pub fn new(max_wall_secs: Option<u64>) -> Self {
Self {
started: Instant::now(),
max_wall_secs,
}
}
pub fn shared_default() -> Arc<Self> {
Arc::new(Self::new(Some(DEFAULT_SESSION_WALL_SECS)))
}
pub fn unlimited() -> Arc<Self> {
Arc::new(Self::new(None))
}
pub fn admit(&self) -> Option<String> {
let max = self.max_wall_secs?;
let elapsed = self.elapsed_secs();
if elapsed < max {
return None;
}
Some(format!(
"session budget exhausted: {elapsed}s elapsed of a {max}s ceiling"
))
}
pub fn remaining_secs(&self) -> Option<u64> {
self.max_wall_secs
.map(|max| max.saturating_sub(self.elapsed_secs()))
}
pub fn elapsed_secs(&self) -> u64 {
self.started.elapsed().as_secs()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_deadline_admits() {
assert!(SessionDeadline::new(Some(DEFAULT_SESSION_WALL_SECS))
.admit()
.is_none());
}
#[test]
fn an_exhausted_deadline_denies_with_both_numbers() {
let reason = SessionDeadline::new(Some(0))
.admit()
.expect("a 0s ceiling must deny");
assert!(reason.contains("session budget exhausted"), "{reason}");
assert!(reason.contains("0s ceiling"), "{reason}");
}
#[test]
fn no_ceiling_never_denies_and_has_no_remainder() {
let d = SessionDeadline::new(None);
assert!(d.admit().is_none());
assert_eq!(d.remaining_secs(), None);
}
#[test]
fn remaining_saturates_at_zero_rather_than_wrapping() {
assert_eq!(SessionDeadline::new(Some(0)).remaining_secs(), Some(0));
let plenty = SessionDeadline::new(Some(3600))
.remaining_secs()
.expect("bounded");
assert!(
plenty > 3500,
"a fresh hour should have nearly all of it left"
);
}
#[test]
fn the_default_ceiling_clears_existing_caller_bounds() {
assert!(
DEFAULT_SESSION_WALL_SECS > 900,
"must not truncate the A/B's native arm"
);
assert!(
DEFAULT_SESSION_WALL_SECS >= 1800,
"must not truncate one external invocation"
);
}
#[test]
fn a_shared_handle_reports_one_clock() {
let a = SessionDeadline::shared_default();
let b = Arc::clone(&a);
assert!(
Arc::ptr_eq(&a, &b),
"clones must share, not copy, the clock"
);
assert_eq!(a.elapsed_secs(), b.elapsed_secs());
}
}