use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use std::time::Instant;
use crate::{LimitExceeded, PageIndex};
#[derive(Debug, Clone)]
pub struct Deadline {
stop: Arc<AtomicBool>,
clock: Option<Clock>,
}
#[derive(Debug, Clone, Copy)]
struct Clock {
start: Instant,
budget: Duration,
}
impl Deadline {
#[must_use]
pub fn manual() -> Deadline {
Deadline {
stop: Arc::new(AtomicBool::new(false)),
clock: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn after(budget: Duration) -> Deadline {
Deadline {
stop: Arc::new(AtomicBool::new(false)),
clock: Some(Clock {
start: Instant::now(),
budget,
}),
}
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn with_budget(&self, budget: Duration) -> Deadline {
Deadline {
stop: Arc::clone(&self.stop),
clock: Some(Clock {
start: Instant::now(),
budget,
}),
}
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn at(instant: Instant) -> Deadline {
let start = Instant::now();
Deadline {
stop: Arc::new(AtomicBool::new(false)),
clock: Some(Clock {
start,
budget: instant.saturating_duration_since(start),
}),
}
}
pub fn stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
#[must_use]
pub fn budget(&self) -> Option<Duration> {
self.clock.map(|clock| clock.budget)
}
#[must_use]
pub fn passed(&self) -> bool {
self.stop.load(Ordering::Relaxed) || self.spent_budget().is_some()
}
pub fn check(&self, during: Operation) -> Result<(), LimitExceeded> {
if self.stop.load(Ordering::Relaxed) {
return Err(LimitExceeded::Stopped { during, page: None });
}
match self.spent_budget() {
Some(budget) => Err(LimitExceeded::Time {
budget,
during,
page: None,
}),
None => Ok(()),
}
}
fn spent_budget(&self) -> Option<Duration> {
self.clock
.filter(|clock| clock.start.elapsed() >= clock.budget)
.map(|clock| clock.budget)
}
}
impl PartialEq for Deadline {
fn eq(&self, other: &Deadline) -> bool {
Arc::ptr_eq(&self.stop, &other.stop)
}
}
impl Eq for Deadline {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Operation {
Open,
PageLoad,
Interpret,
Render,
Extract,
Script,
}
impl Operation {
pub(crate) fn describe(self, page: Option<PageIndex>) -> String {
let what = match self {
Operation::Open => "opening the document",
Operation::PageLoad => "loading page",
Operation::Interpret => "interpreting page",
Operation::Render => "rendering page",
Operation::Extract => "extracting text from page",
Operation::Script => "running a script",
};
match (self, page) {
(Operation::Open | Operation::Script, _) | (_, None) => what.to_string(),
(_, Some(page)) => format!("{what} {page}"),
}
}
}
#[cfg(test)]
mod tests {
use super::{Deadline, Operation};
use crate::{LimitExceeded, PageIndex};
use std::time::{Duration, Instant};
#[test]
fn a_manual_deadline_passes_only_when_stopped_and_every_clone_sees_it() {
let stop = Deadline::manual();
let held = stop.clone();
assert!(!held.passed());
assert_eq!(held.check(Operation::Open), Ok(()));
assert_eq!(held.budget(), None);
stop.stop();
assert!(held.passed());
assert_eq!(
held.check(Operation::Render),
Err(LimitExceeded::Stopped {
during: Operation::Render,
page: None,
})
);
stop.stop();
assert!(stop.passed());
}
#[test]
fn equality_is_sharing_a_flag() {
let a = Deadline::manual();
assert_eq!(a, a.clone());
assert_ne!(a, Deadline::manual());
}
#[test]
fn a_zero_budget_has_passed_and_an_hour_has_not() {
assert!(Deadline::after(Duration::ZERO).passed());
assert!(!Deadline::after(Duration::from_hours(1)).passed());
}
#[test]
fn a_budget_can_still_be_stopped_early() {
let deadline = Deadline::after(Duration::from_hours(1));
deadline.stop();
assert!(deadline.passed());
assert!(matches!(
deadline.check(Operation::Extract),
Err(LimitExceeded::Stopped { .. })
));
}
#[test]
fn an_instant_already_reached_has_passed() {
let now = Instant::now();
let deadline = Deadline::at(now);
assert!(deadline.passed());
assert_eq!(deadline.budget(), Some(Duration::ZERO));
let later = Deadline::at(now + Duration::from_hours(1));
assert!(!later.passed());
assert!(later.budget() > Some(Duration::from_secs(3599)));
}
#[test]
fn the_check_names_the_operation_and_leaves_the_page_to_the_caller() {
let deadline = Deadline::after(Duration::from_secs(5));
assert_eq!(deadline.check(Operation::Render), Ok(()));
let spent = Deadline::after(Duration::ZERO);
let error = spent.check(Operation::Render).expect_err("spent");
assert_eq!(
error,
LimitExceeded::Time {
budget: Duration::ZERO,
during: Operation::Render,
page: None,
}
);
assert_eq!(
error.on_page(PageIndex::from(2u32)),
LimitExceeded::Time {
budget: Duration::ZERO,
during: Operation::Render,
page: Some(PageIndex::from(2u32)),
}
);
}
#[test]
fn descriptions_take_a_page_only_where_one_makes_sense() {
let page = Some(PageIndex::from(3u32));
assert_eq!(Operation::Open.describe(page), "opening the document");
assert_eq!(Operation::Script.describe(page), "running a script");
assert_eq!(Operation::Render.describe(page), "rendering page 3");
assert_eq!(Operation::Render.describe(None), "rendering page");
assert_eq!(Operation::PageLoad.describe(page), "loading page 3");
assert_eq!(Operation::Interpret.describe(page), "interpreting page 3");
assert_eq!(
Operation::Extract.describe(page),
"extracting text from page 3"
);
}
}