use std::cell::Cell;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug)]
pub struct Deadline {
expires_at: Option<Instant>,
}
impl Deadline {
pub fn from_timeout_secs(timeout_secs: u64) -> Self {
Deadline {
expires_at: Instant::now().checked_add(Duration::from_secs(timeout_secs)),
}
}
pub fn none() -> Self {
Deadline { expires_at: None }
}
pub fn check(&self, phase: &str) {
if let Some(expires_at) = self.expires_at {
check_expired(Instant::now(), expires_at, phase);
}
}
pub fn enter(&self) -> ScopedDeadline {
let prev = CURRENT.with(|c| c.replace(self.expires_at));
ScopedDeadline { prev }
}
}
thread_local! {
static CURRENT: Cell<Option<Instant>> = const { Cell::new(None) };
}
pub struct ScopedDeadline {
prev: Option<Instant>,
}
impl Drop for ScopedDeadline {
fn drop(&mut self) {
let prev = self.prev;
CURRENT.with(|c| c.set(prev));
}
}
pub fn check_current_every(i: usize, every: usize, phase: &str) {
if i % every != 0 {
return;
}
if let Some(expires_at) = CURRENT.with(|c| c.get()) {
check_expired(Instant::now(), expires_at, phase);
}
}
fn check_expired(now: Instant, expires_at: Instant, phase: &str) {
if now > expires_at {
panic!("diffctx compute deadline exceeded during {phase}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_expired_deadline_panics_with_the_phase_name() {
let deadline = Deadline::from_timeout_secs(0);
std::thread::sleep(Duration::from_millis(5));
let err = std::panic::catch_unwind(|| deadline.check("edge construction"))
.expect_err("deadline did not fire");
let msg = err.downcast_ref::<String>().cloned().unwrap_or_default();
assert!(msg.contains("edge construction"), "message was: {msg}");
}
#[test]
fn an_unexpired_deadline_does_not_fire() {
Deadline::from_timeout_secs(1000).check("edge construction");
Deadline::none().check("edge construction");
}
#[test]
fn concurrent_deadlines_do_not_affect_each_other() {
let short = Deadline::from_timeout_secs(0);
let long = Deadline::from_timeout_secs(1000);
std::thread::sleep(Duration::from_millis(5));
long.check("edge construction");
std::panic::catch_unwind(|| short.check("edge construction"))
.expect_err("short deadline did not fire");
long.check("edge construction");
}
#[test]
fn scoped_deadline_clears_on_drop_and_nests() {
let outer = Deadline::from_timeout_secs(1000);
let guard = outer.enter();
check_current_every(0, 1, "edge construction");
{
let expired = Deadline::from_timeout_secs(0);
let inner = expired.enter();
std::thread::sleep(Duration::from_millis(5));
std::panic::catch_unwind(|| check_current_every(0, 1, "edge construction"))
.expect_err("inner deadline did not fire");
drop(inner);
}
check_current_every(0, 1, "edge construction");
drop(guard);
check_current_every(0, 1, "edge construction");
}
}