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 `budget` from now. It can still be raised early with
84    /// [`Deadline::stop`].
85    ///
86    /// Not available on `wasm32`, which has no monotonic clock to read: a
87    /// host there uses [`Deadline::manual`] and its own timer.
88    #[cfg(not(target_arch = "wasm32"))]
89    #[must_use]
90    pub fn after(budget: Duration) -> Deadline {
91        Deadline {
92            stop: Arc::new(AtomicBool::new(false)),
93            clock: Some(Clock {
94                start: Instant::now(),
95                budget,
96            }),
97        }
98    }
99
100    /// This deadline's flag, with a budget of `budget` from now.
101    ///
102    /// The returned deadline **shares the flag**, so [`Deadline::stop`] on
103    /// either raises both — it is the same deadline with a clock added, not a
104    /// second one beside it. That is what a host wants when it holds a cancel
105    /// flag *and* a wall-clock budget: one deadline that answers to whichever
106    /// arrives first. Building the two separately gives the engine only one of
107    /// them, because [`Limits::deadline`](crate::Limits::deadline) is a single
108    /// slot.
109    ///
110    /// An existing budget is replaced, not intersected.
111    ///
112    /// Not available on `wasm32`, as [`Deadline::after`].
113    ///
114    /// ```
115    /// # #[cfg(not(target_arch = "wasm32"))] {
116    /// use std::time::Duration;
117    /// use pdfrum_common::Deadline;
118    ///
119    /// let flag = Deadline::manual();
120    /// let limit = flag.with_budget(Duration::from_secs(30));
121    ///
122    /// // The budget is live on the copy the engine holds...
123    /// assert_eq!(limit.budget(), Some(Duration::from_secs(30)));
124    /// assert!(!limit.passed());
125    ///
126    /// // ...and the flag still reaches it.
127    /// flag.stop();
128    /// assert!(limit.passed());
129    /// # }
130    /// ```
131    #[cfg(not(target_arch = "wasm32"))]
132    #[must_use]
133    pub fn with_budget(&self, budget: Duration) -> Deadline {
134        Deadline {
135            stop: Arc::clone(&self.stop),
136            clock: Some(Clock {
137                start: Instant::now(),
138                budget,
139            }),
140        }
141    }
142
143    /// The deadline at `instant`; one already in the past has passed.
144    ///
145    /// Not available on `wasm32`, as [`Deadline::after`].
146    #[cfg(not(target_arch = "wasm32"))]
147    #[must_use]
148    pub fn at(instant: Instant) -> Deadline {
149        let start = Instant::now();
150        Deadline {
151            stop: Arc::new(AtomicBool::new(false)),
152            clock: Some(Clock {
153                start,
154                budget: instant.saturating_duration_since(start),
155            }),
156        }
157    }
158
159    /// Raises the flag: every check from now on, on every clone, answers
160    /// that the deadline has passed. Idempotent, and callable from any
161    /// thread.
162    pub fn stop(&self) {
163        self.stop.store(true, Ordering::Relaxed);
164    }
165
166    /// How much time was allowed, for a deadline made with a budget.
167    #[must_use]
168    pub fn budget(&self) -> Option<Duration> {
169        self.clock.map(|clock| clock.budget)
170    }
171
172    /// Whether the flag is raised or the budget spent. One atomic load, and
173    /// a clock read only while the flag is down and a budget is set.
174    #[must_use]
175    pub fn passed(&self) -> bool {
176        self.stop.load(Ordering::Relaxed) || self.spent_budget().is_some()
177    }
178
179    /// `Ok` while the budget lasts and the flag is down; past either, the
180    /// error that names what was being done. The page, when there is one,
181    /// is the caller's to add with [`LimitExceeded::on_page`].
182    ///
183    /// # Errors
184    ///
185    /// [`LimitExceeded::Stopped`] once [`Deadline::stop`] was called,
186    /// [`LimitExceeded::Time`] once a budget is spent.
187    pub fn check(&self, during: Operation) -> Result<(), LimitExceeded> {
188        if self.stop.load(Ordering::Relaxed) {
189            return Err(LimitExceeded::Stopped { during, page: None });
190        }
191        match self.spent_budget() {
192            Some(budget) => Err(LimitExceeded::Time {
193                budget,
194                during,
195                page: None,
196            }),
197            None => Ok(()),
198        }
199    }
200
201    /// The budget, if there is one and it is spent. On `wasm32` there is
202    /// never one, so only the flag can pass a deadline there.
203    fn spent_budget(&self) -> Option<Duration> {
204        self.clock
205            .filter(|clock| clock.start.elapsed() >= clock.budget)
206            .map(|clock| clock.budget)
207    }
208}
209
210/// Two deadlines are equal when they share a flag — clones of one another —
211/// which is the question a caller comparing two `Limits` is asking.
212impl PartialEq for Deadline {
213    fn eq(&self, other: &Deadline) -> bool {
214        Arc::ptr_eq(&self.stop, &other.stop)
215    }
216}
217
218impl Eq for Deadline {}
219
220/// What the engine was doing when a deadline passed — the noun in the
221/// message, and the boundary at which the check sits.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223#[non_exhaustive]
224pub enum Operation {
225    /// Opening the document: the header and cross-reference read, including
226    /// the rebuild scan of a damaged file.
227    Open,
228    /// Loading one page's dictionary — the per-page boundary a walk over a
229    /// document crosses.
230    PageLoad,
231    /// Interpreting a content stream into page objects.
232    Interpret,
233    /// Rasterizing the page objects.
234    Render,
235    /// Extracting text.
236    Extract,
237    /// Running a document script.
238    Script,
239}
240
241impl Operation {
242    /// The message's verb phrase, with the page where one is known.
243    pub(crate) fn describe(self, page: Option<PageIndex>) -> String {
244        let what = match self {
245            Operation::Open => "opening the document",
246            Operation::PageLoad => "loading page",
247            Operation::Interpret => "interpreting page",
248            Operation::Render => "rendering page",
249            Operation::Extract => "extracting text from page",
250            Operation::Script => "running a script",
251        };
252        match (self, page) {
253            (Operation::Open | Operation::Script, _) | (_, None) => what.to_string(),
254            (_, Some(page)) => format!("{what} {page}"),
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::{Deadline, Operation};
262    use crate::{LimitExceeded, PageIndex};
263    use std::time::{Duration, Instant};
264
265    #[test]
266    fn a_manual_deadline_passes_only_when_stopped_and_every_clone_sees_it() {
267        let stop = Deadline::manual();
268        let held = stop.clone();
269        assert!(!held.passed());
270        assert_eq!(held.check(Operation::Open), Ok(()));
271        assert_eq!(held.budget(), None);
272        stop.stop();
273        assert!(held.passed());
274        assert_eq!(
275            held.check(Operation::Render),
276            Err(LimitExceeded::Stopped {
277                during: Operation::Render,
278                page: None,
279            })
280        );
281        // Idempotent.
282        stop.stop();
283        assert!(stop.passed());
284    }
285
286    #[test]
287    fn equality_is_sharing_a_flag() {
288        let a = Deadline::manual();
289        assert_eq!(a, a.clone());
290        assert_ne!(a, Deadline::manual());
291    }
292
293    #[test]
294    fn a_zero_budget_has_passed_and_an_hour_has_not() {
295        assert!(Deadline::after(Duration::ZERO).passed());
296        assert!(!Deadline::after(Duration::from_hours(1)).passed());
297    }
298
299    #[test]
300    fn a_budget_can_still_be_stopped_early() {
301        let deadline = Deadline::after(Duration::from_hours(1));
302        deadline.stop();
303        assert!(deadline.passed());
304        assert!(matches!(
305            deadline.check(Operation::Extract),
306            Err(LimitExceeded::Stopped { .. })
307        ));
308    }
309
310    #[test]
311    fn an_instant_already_reached_has_passed() {
312        let now = Instant::now();
313        let deadline = Deadline::at(now);
314        assert!(deadline.passed());
315        assert_eq!(deadline.budget(), Some(Duration::ZERO));
316        let later = Deadline::at(now + Duration::from_hours(1));
317        assert!(!later.passed());
318        assert!(later.budget() > Some(Duration::from_secs(3599)));
319    }
320
321    #[test]
322    fn the_check_names_the_operation_and_leaves_the_page_to_the_caller() {
323        let deadline = Deadline::after(Duration::from_secs(5));
324        assert_eq!(deadline.check(Operation::Render), Ok(()));
325        let spent = Deadline::after(Duration::ZERO);
326        let error = spent.check(Operation::Render).expect_err("spent");
327        assert_eq!(
328            error,
329            LimitExceeded::Time {
330                budget: Duration::ZERO,
331                during: Operation::Render,
332                page: None,
333            }
334        );
335        assert_eq!(
336            error.on_page(PageIndex::from(2u32)),
337            LimitExceeded::Time {
338                budget: Duration::ZERO,
339                during: Operation::Render,
340                page: Some(PageIndex::from(2u32)),
341            }
342        );
343    }
344
345    #[test]
346    fn descriptions_take_a_page_only_where_one_makes_sense() {
347        let page = Some(PageIndex::from(3u32));
348        assert_eq!(Operation::Open.describe(page), "opening the document");
349        assert_eq!(Operation::Script.describe(page), "running a script");
350        assert_eq!(Operation::Render.describe(page), "rendering page 3");
351        assert_eq!(Operation::Render.describe(None), "rendering page");
352        assert_eq!(Operation::PageLoad.describe(page), "loading page 3");
353        assert_eq!(Operation::Interpret.describe(page), "interpreting page 3");
354        assert_eq!(
355            Operation::Extract.describe(page),
356            "extracting text from page 3"
357        );
358    }
359}