Skip to main content

pdfrum_common/
deadline.rs

1//! When work must stop: a flag a host raises, and — where the target has a
2//! clock — a budget that raises it for you.
3//!
4//! The engine has no threads to interrupt and no signal to catch: a deadline
5//! is honoured by the loops that already have a natural granularity — the
6//! content interpreter per batch of operators, the rasterizer per object, the
7//! extractor and the page loader per page, the cross-reference rebuild per
8//! chunk of tokens, the script engine per run — each asking
9//! [`Deadline::passed`] at that boundary and stopping. The reads are cheap
10//! (one atomic load, and one `Instant::now()` only while a budget is set)
11//! and happen only when a deadline is set; the default path pays one branch.
12//!
13//! **The flag is the mechanism and the clock is a convenience**, because
14//! `std::time::Instant::now()` aborts at runtime on `wasm32-unknown-unknown`
15//! and the facade builds for that target. A host there stops the work from
16//! its own timer or event with [`Deadline::stop`]; a native host usually
17//! writes [`Deadline::after`], which is the same flag plus a clock read at
18//! every check.
19
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::Duration;
23use std::time::Instant;
24
25use crate::{LimitExceeded, PageIndex};
26
27/// When work must stop. Cloning shares the flag: a host keeps one clone to
28/// raise and hands the other to [`Limits`](crate::Limits).
29///
30/// ```
31/// use pdfrum_common::Deadline;
32///
33/// // The mechanism: a flag raised from anywhere, on every target.
34/// let stop = Deadline::manual();
35/// let limit = stop.clone();
36/// assert!(!limit.passed());
37/// stop.stop();
38/// assert!(limit.passed());
39/// ```
40///
41/// ```
42/// # #[cfg(not(target_arch = "wasm32"))] {
43/// use std::time::Duration;
44/// use pdfrum_common::Deadline;
45///
46/// // The convenience, where there is a clock: a budget from now.
47/// let deadline = Deadline::after(Duration::from_secs(5));
48/// assert!(!deadline.passed());
49/// assert_eq!(deadline.budget(), Some(Duration::from_secs(5)));
50///
51/// // A zero budget has passed before it is asked.
52/// assert!(Deadline::after(Duration::ZERO).passed());
53/// # }
54/// ```
55#[derive(Debug, Clone)]
56pub struct Deadline {
57    /// Raised by [`Deadline::stop`]. Shared by every clone.
58    stop: Arc<AtomicBool>,
59    /// The budget, where one was set. Always `None` on `wasm32`, whose
60    /// clock constructors do not exist, so no code path there reads a clock.
61    clock: Option<Clock>,
62}
63
64/// A budget counted from the moment it was made. Stored as a start and a
65/// budget rather than as one `Instant`, so the message can say how much
66/// time was allowed and so no arithmetic on `Instant` can overflow.
67#[derive(Debug, Clone, Copy)]
68struct Clock {
69    start: Instant,
70    budget: Duration,
71}
72
73impl Deadline {
74    /// A deadline nothing but [`Deadline::stop`] raises.
75    #[must_use]
76    pub fn manual() -> Deadline {
77        Deadline {
78            stop: Arc::new(AtomicBool::new(false)),
79            clock: None,
80        }
81    }
82
83    /// A deadline that answers to a flag the caller already owns.
84    ///
85    /// Raising `flag` (`store(true, ..)`, from any thread) makes this
86    /// deadline, and every clone of it, pass, exactly as [`Deadline::stop`]
87    /// does; [`Deadline::stop`] on it raises `flag` too. A host with one
88    /// cancellation flag per job hands that flag here rather than keeping a
89    /// second one in step with it. Add a budget with
90    /// [`Deadline::with_budget`].
91    ///
92    /// The flag is only read, with a relaxed load. Nothing lowers it again,
93    /// so a flag that is reused must be replaced with a fresh one.
94    ///
95    /// ```
96    /// use std::sync::Arc;
97    /// use std::sync::atomic::{AtomicBool, Ordering};
98    /// use pdfrum_common::Deadline;
99    ///
100    /// let flag = Arc::new(AtomicBool::new(false));
101    /// let deadline = Deadline::from_flag(Arc::clone(&flag));
102    /// assert!(!deadline.passed());
103    /// flag.store(true, Ordering::Relaxed);
104    /// assert!(deadline.passed());
105    /// ```
106    #[must_use]
107    pub fn from_flag(flag: Arc<AtomicBool>) -> Deadline {
108        Deadline {
109            stop: flag,
110            clock: None,
111        }
112    }
113
114    /// A deadline `budget` from now. It can still be raised early with
115    /// [`Deadline::stop`].
116    ///
117    /// Not available on `wasm32`, which has no monotonic clock to read: a
118    /// host there uses [`Deadline::manual`] and its own timer.
119    #[cfg(not(target_arch = "wasm32"))]
120    #[must_use]
121    pub fn after(budget: Duration) -> Deadline {
122        Deadline {
123            stop: Arc::new(AtomicBool::new(false)),
124            clock: Some(Clock {
125                start: Instant::now(),
126                budget,
127            }),
128        }
129    }
130
131    /// This deadline's flag, with a budget of `budget` from now.
132    ///
133    /// The returned deadline **shares the flag**, so [`Deadline::stop`] on
134    /// either raises both — it is the same deadline with a clock added, not a
135    /// second one beside it. That is what a host wants when it holds a cancel
136    /// flag *and* a wall-clock budget: one deadline that answers to whichever
137    /// arrives first. Building the two separately gives the engine only one of
138    /// them, because [`Limits::deadline`](crate::Limits::deadline) is a single
139    /// slot.
140    ///
141    /// An existing budget is replaced, not intersected.
142    ///
143    /// Not available on `wasm32`, as [`Deadline::after`].
144    ///
145    /// ```
146    /// # #[cfg(not(target_arch = "wasm32"))] {
147    /// use std::time::Duration;
148    /// use pdfrum_common::Deadline;
149    ///
150    /// let flag = Deadline::manual();
151    /// let limit = flag.with_budget(Duration::from_secs(30));
152    ///
153    /// // The budget is live on the copy the engine holds...
154    /// assert_eq!(limit.budget(), Some(Duration::from_secs(30)));
155    /// assert!(!limit.passed());
156    ///
157    /// // ...and the flag still reaches it.
158    /// flag.stop();
159    /// assert!(limit.passed());
160    /// # }
161    /// ```
162    #[cfg(not(target_arch = "wasm32"))]
163    #[must_use]
164    pub fn with_budget(&self, budget: Duration) -> Deadline {
165        Deadline {
166            stop: Arc::clone(&self.stop),
167            clock: Some(Clock {
168                start: Instant::now(),
169                budget,
170            }),
171        }
172    }
173
174    /// The deadline at `instant`; one already in the past has passed.
175    ///
176    /// Not available on `wasm32`, as [`Deadline::after`].
177    #[cfg(not(target_arch = "wasm32"))]
178    #[must_use]
179    pub fn at(instant: Instant) -> Deadline {
180        let start = Instant::now();
181        Deadline {
182            stop: Arc::new(AtomicBool::new(false)),
183            clock: Some(Clock {
184                start,
185                budget: instant.saturating_duration_since(start),
186            }),
187        }
188    }
189
190    /// Raises the flag: every check from now on, on every clone, answers
191    /// that the deadline has passed. Idempotent, and callable from any
192    /// thread.
193    pub fn stop(&self) {
194        self.stop.store(true, Ordering::Relaxed);
195    }
196
197    /// How much time was allowed, for a deadline made with a budget.
198    #[must_use]
199    pub fn budget(&self) -> Option<Duration> {
200        self.clock.map(|clock| clock.budget)
201    }
202
203    /// Whether the flag is raised or the budget spent. One atomic load, and
204    /// a clock read only while the flag is down and a budget is set.
205    #[must_use]
206    pub fn passed(&self) -> bool {
207        self.stop.load(Ordering::Relaxed) || self.spent_budget().is_some()
208    }
209
210    /// `Ok` while the budget lasts and the flag is down; past either, the
211    /// error that names what was being done. The page, when there is one,
212    /// is the caller's to add with [`LimitExceeded::on_page`].
213    ///
214    /// # Errors
215    ///
216    /// [`LimitExceeded::Stopped`] once [`Deadline::stop`] was called,
217    /// [`LimitExceeded::Time`] once a budget is spent.
218    pub fn check(&self, during: Operation) -> Result<(), LimitExceeded> {
219        if self.stop.load(Ordering::Relaxed) {
220            return Err(LimitExceeded::Stopped { during, page: None });
221        }
222        match self.spent_budget() {
223            Some(budget) => Err(LimitExceeded::Time {
224                budget,
225                during,
226                page: None,
227            }),
228            None => Ok(()),
229        }
230    }
231
232    /// The budget, if there is one and it is spent. On `wasm32` there is
233    /// never one, so only the flag can pass a deadline there.
234    fn spent_budget(&self) -> Option<Duration> {
235        self.clock
236            .filter(|clock| clock.start.elapsed() >= clock.budget)
237            .map(|clock| clock.budget)
238    }
239}
240
241/// Two deadlines are equal when they share a flag — clones of one another —
242/// which is the question a caller comparing two `Limits` is asking.
243impl PartialEq for Deadline {
244    fn eq(&self, other: &Deadline) -> bool {
245        Arc::ptr_eq(&self.stop, &other.stop)
246    }
247}
248
249impl Eq for Deadline {}
250
251/// What the engine was doing when a deadline passed — the noun in the
252/// message, and the boundary at which the check sits.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
254#[non_exhaustive]
255pub enum Operation {
256    /// Opening the document: the header and cross-reference read, including
257    /// the rebuild scan of a damaged file.
258    Open,
259    /// Loading one page's dictionary — the per-page boundary a walk over a
260    /// document crosses.
261    PageLoad,
262    /// Interpreting a content stream into page objects.
263    Interpret,
264    /// Rasterizing the page objects.
265    Render,
266    /// Extracting text.
267    Extract,
268    /// Running a document script.
269    Script,
270}
271
272impl Operation {
273    /// The message's verb phrase, with the page where one is known.
274    pub(crate) fn describe(self, page: Option<PageIndex>) -> String {
275        let what = match self {
276            Operation::Open => "opening the document",
277            Operation::PageLoad => "loading page",
278            Operation::Interpret => "interpreting page",
279            Operation::Render => "rendering page",
280            Operation::Extract => "extracting text from page",
281            Operation::Script => "running a script",
282        };
283        match (self, page) {
284            (Operation::Open | Operation::Script, _) | (_, None) => what.to_string(),
285            (_, Some(page)) => format!("{what} {page}"),
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::{Deadline, Operation};
293    use crate::{LimitExceeded, PageIndex};
294    use std::time::{Duration, Instant};
295
296    #[test]
297    fn a_manual_deadline_passes_only_when_stopped_and_every_clone_sees_it() {
298        let stop = Deadline::manual();
299        let held = stop.clone();
300        assert!(!held.passed());
301        assert_eq!(held.check(Operation::Open), Ok(()));
302        assert_eq!(held.budget(), None);
303        stop.stop();
304        assert!(held.passed());
305        assert_eq!(
306            held.check(Operation::Render),
307            Err(LimitExceeded::Stopped {
308                during: Operation::Render,
309                page: None,
310            })
311        );
312        // Idempotent.
313        stop.stop();
314        assert!(stop.passed());
315    }
316
317    #[test]
318    fn a_deadline_from_a_flag_passes_when_the_flag_is_raised_and_raises_it_when_stopped() {
319        use std::sync::Arc;
320        use std::sync::atomic::{AtomicBool, Ordering};
321        let flag = Arc::new(AtomicBool::new(false));
322        let deadline = Deadline::from_flag(Arc::clone(&flag));
323        assert!(!deadline.passed());
324        assert_eq!(deadline.budget(), None);
325        flag.store(true, Ordering::Relaxed);
326        assert!(matches!(
327            deadline.check(Operation::Render),
328            Err(LimitExceeded::Stopped { .. })
329        ));
330        let other = Arc::new(AtomicBool::new(false));
331        Deadline::from_flag(Arc::clone(&other)).stop();
332        assert!(other.load(Ordering::Relaxed));
333    }
334
335    #[test]
336    fn equality_is_sharing_a_flag() {
337        let a = Deadline::manual();
338        assert_eq!(a, a.clone());
339        assert_ne!(a, Deadline::manual());
340    }
341
342    #[test]
343    fn a_zero_budget_has_passed_and_an_hour_has_not() {
344        assert!(Deadline::after(Duration::ZERO).passed());
345        assert!(!Deadline::after(Duration::from_hours(1)).passed());
346    }
347
348    #[test]
349    fn a_budget_can_still_be_stopped_early() {
350        let deadline = Deadline::after(Duration::from_hours(1));
351        deadline.stop();
352        assert!(deadline.passed());
353        assert!(matches!(
354            deadline.check(Operation::Extract),
355            Err(LimitExceeded::Stopped { .. })
356        ));
357    }
358
359    #[test]
360    fn an_instant_already_reached_has_passed() {
361        let now = Instant::now();
362        let deadline = Deadline::at(now);
363        assert!(deadline.passed());
364        assert_eq!(deadline.budget(), Some(Duration::ZERO));
365        let later = Deadline::at(now + Duration::from_hours(1));
366        assert!(!later.passed());
367        assert!(later.budget() > Some(Duration::from_secs(3599)));
368    }
369
370    #[test]
371    fn the_check_names_the_operation_and_leaves_the_page_to_the_caller() {
372        let deadline = Deadline::after(Duration::from_secs(5));
373        assert_eq!(deadline.check(Operation::Render), Ok(()));
374        let spent = Deadline::after(Duration::ZERO);
375        let error = spent.check(Operation::Render).expect_err("spent");
376        assert_eq!(
377            error,
378            LimitExceeded::Time {
379                budget: Duration::ZERO,
380                during: Operation::Render,
381                page: None,
382            }
383        );
384        assert_eq!(
385            error.on_page(PageIndex::from(2u32)),
386            LimitExceeded::Time {
387                budget: Duration::ZERO,
388                during: Operation::Render,
389                page: Some(PageIndex::from(2u32)),
390            }
391        );
392    }
393
394    #[test]
395    fn descriptions_take_a_page_only_where_one_makes_sense() {
396        let page = Some(PageIndex::from(3u32));
397        assert_eq!(Operation::Open.describe(page), "opening the document");
398        assert_eq!(Operation::Script.describe(page), "running a script");
399        assert_eq!(Operation::Render.describe(page), "rendering page 3");
400        assert_eq!(Operation::Render.describe(None), "rendering page");
401        assert_eq!(Operation::PageLoad.describe(page), "loading page 3");
402        assert_eq!(Operation::Interpret.describe(page), "interpreting page 3");
403        assert_eq!(
404            Operation::Extract.describe(page),
405            "extracting text from page 3"
406        );
407    }
408}