Skip to main content

arris_math/
control.rs

1//! Cancellation: the argument every operation takes so its caller can stop
2//! it, and the counter the kernel's loops tick (ADR-0030).
3//!
4//! A [`Control`] is what the caller supplies: a poll it answers from
5//! whatever its platform has, and a budget of steps. A [`Meter`] is what
6//! an algorithm holds: it counts a step at each loop boundary of work
7//! that can grow past its input's size and asks the budget and then the
8//! poll. A stop is an [`Interrupted`], which the operation returns in its
9//! own error type; the transaction it runs in rolls the model back.
10
11use core::fmt;
12
13/// The poll a caller supplies: `true` asks the running operation to stop.
14/// `Sync` because the `parallel` passes poll from several threads.
15pub type Poll<'a> = &'a (dyn Fn() -> bool + Sync);
16
17/// What stopped an operation.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Stop {
20    /// The caller's poll answered `true`.
21    Poll,
22    /// The caller's budget of steps ran out.
23    Budget,
24}
25
26/// An operation that was stopped, by the caller and not by its input. The
27/// model is as it was before the call.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct Interrupted {
30    /// What stopped it.
31    pub by: Stop,
32    /// The steps taken before the stop. For [`Stop::Budget`] it is the
33    /// budget; the same input and budget stop at the same count on every
34    /// platform and with `parallel` on or off.
35    pub steps: u64,
36}
37
38impl fmt::Display for Interrupted {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self.by {
41            Stop::Poll => write!(
42                f,
43                "interrupted by the caller's poll after {} steps",
44                self.steps
45            ),
46            Stop::Budget => write!(
47                f,
48                "interrupted: the budget of {} steps is spent",
49                self.steps
50            ),
51        }
52    }
53}
54
55impl core::error::Error for Interrupted {}
56
57/// How a caller stops an operation: a poll, a budget of steps, both or
58/// neither. Every operation on a model, and every long query beside one,
59/// takes one last; [`Control::NONE`] is what a caller that does not care
60/// passes.
61///
62/// A step is one iteration of a loop whose trip count is not bounded by
63/// the input's entity count (ADR-0030 §5). Step counts are stable within
64/// one release, not across releases: a budget is a cap a caller tunes.
65///
66/// ```
67/// use arris_math::{Control, Meter, Stop};
68/// use core::sync::atomic::{AtomicBool, Ordering};
69///
70/// let stop = AtomicBool::new(false);
71/// let poll = || stop.load(Ordering::Relaxed);
72/// let control = Control::poll(&poll).with_budget(2);
73/// let mut meter = Meter::new(&control);
74/// assert!(meter.tick().is_ok() && meter.tick().is_ok());
75/// let spent = meter.tick().unwrap_err();
76/// assert_eq!((spent.by, spent.steps), (Stop::Budget, 2));
77///
78/// stop.store(true, Ordering::Relaxed);
79/// let stopped = Meter::new(&control).tick().unwrap_err();
80/// assert_eq!((stopped.by, stopped.steps), (Stop::Poll, 0));
81/// ```
82#[derive(Clone, Copy, Default)]
83pub struct Control<'a> {
84    poll: Option<Poll<'a>>,
85    budget: Option<u64>,
86}
87
88impl fmt::Debug for Control<'_> {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.debug_struct("Control")
91            .field("poll", &self.poll.map(|_| "…"))
92            .field("budget", &self.budget)
93            .finish()
94    }
95}
96
97impl Control<'static> {
98    /// No poll and no budget: the operation runs to its end.
99    pub const NONE: Control<'static> = Control {
100        poll: None,
101        budget: None,
102    };
103
104    /// A budget of `steps` and no poll: the `steps + 1`-th step is
105    /// [`Stop::Budget`].
106    pub const fn budget(steps: u64) -> Control<'static> {
107        Control {
108            poll: None,
109            budget: Some(steps),
110        }
111    }
112}
113
114impl<'a> Control<'a> {
115    /// A poll and no budget. The poll is asked at every step, and a
116    /// `true` stops the operation at that step.
117    pub const fn poll(poll: Poll<'a>) -> Control<'a> {
118        Control {
119            poll: Some(poll),
120            budget: None,
121        }
122    }
123
124    /// This control with a budget of `steps`.
125    #[must_use]
126    pub const fn with_budget(mut self, steps: u64) -> Self {
127        self.budget = Some(steps);
128        self
129    }
130
131    /// This control with `poll`.
132    #[must_use]
133    pub const fn with_poll(mut self, poll: Poll<'a>) -> Self {
134        self.poll = Some(poll);
135        self
136    }
137}
138
139/// The counter an algorithm ticks: a [`Control`] and the steps taken.
140///
141/// [`tick`](Meter::tick) is the step: it asks the budget, then the poll,
142/// and counts the step only if neither stops it. A parallel pass gives
143/// each item a [`split`](Meter::split) and afterwards
144/// [`charge`](Meter::charge)s the items' steps in the sequential order, so
145/// both builds stop on the same item with the same count (ADR-0030 §4).
146#[derive(Clone, Copy)]
147pub struct Meter<'a> {
148    poll: Option<Poll<'a>>,
149    cap: Option<u64>,
150    steps: u64,
151    stopped: Option<Interrupted>,
152}
153
154impl fmt::Debug for Meter<'_> {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        f.debug_struct("Meter")
157            .field("cap", &self.cap)
158            .field("steps", &self.steps)
159            .finish()
160    }
161}
162
163impl Default for Meter<'_> {
164    /// A meter that never stops: [`Control::NONE`]'s.
165    fn default() -> Self {
166        Meter::new(&Control::NONE)
167    }
168}
169
170impl<'a> Meter<'a> {
171    /// A meter at zero steps under `control`.
172    pub fn new(control: &Control<'a>) -> Self {
173        Meter {
174            poll: control.poll,
175            cap: control.budget,
176            steps: 0,
177            stopped: None,
178        }
179    }
180
181    /// One step. `Err` when the budget is spent (checked first, and the
182    /// poll is then not asked) or the poll answers `true`; the step is not
183    /// counted, so [`Interrupted::steps`] is the steps completed.
184    pub fn tick(&mut self) -> Result<(), Interrupted> {
185        if self.cap.is_some_and(|cap| self.steps >= cap) {
186            return Err(self.stopped_by(Stop::Budget));
187        }
188        if self.poll.is_some_and(|poll| poll()) {
189            return Err(self.stopped_by(Stop::Poll));
190        }
191        self.steps += 1;
192        Ok(())
193    }
194
195    /// The first stop [`tick`](Meter::tick) reported, if any. A caller
196    /// that runs code which may turn a failed step into a failure of its
197    /// own (an attempt that falls back to another) asks this at its end:
198    /// an operation that saw a stop is stopped, whatever else it
199    /// returned.
200    pub fn stopped(&self) -> Option<Interrupted> {
201        self.stopped
202    }
203
204    fn stopped_by(&mut self, by: Stop) -> Interrupted {
205        let stop = self.stop(by);
206        *self.stopped.get_or_insert(stop)
207    }
208
209    /// The steps taken so far.
210    pub fn steps(&self) -> u64 {
211        self.steps
212    }
213
214    /// The meter for one item of a parallel pass: the same poll, no steps
215    /// yet, capped at the budget this meter has left.
216    #[must_use]
217    pub fn split(&self) -> Meter<'a> {
218        Meter {
219            poll: self.poll,
220            cap: self.cap.map(|cap| cap.saturating_sub(self.steps)),
221            steps: 0,
222            stopped: None,
223        }
224    }
225
226    /// Takes the `steps` of one item of a pass this meter was
227    /// [`split`](Meter::split) for. Items are charged in the sequential
228    /// order; the first whose running total crosses the budget is
229    /// `Err(Interrupted { by: Stop::Budget, steps: budget })`, which is
230    /// what the sequential build's own tick reports there.
231    pub fn charge(&mut self, steps: u64) -> Result<(), Interrupted> {
232        let total = self.steps.saturating_add(steps);
233        if let Some(cap) = self.cap
234            && total > cap
235        {
236            self.steps = cap;
237            return Err(self.stop(Stop::Budget));
238        }
239        self.steps = total;
240        Ok(())
241    }
242
243    /// The [`Interrupted`] of an item that stopped itself, as this meter
244    /// reports it: the item's steps added to the ones already charged (and
245    /// no more than the budget), so the count is the sequential build's. An item that finished is
246    /// [`charge`](Meter::charge)d; one that stopped is charged here
247    /// instead, never both.
248    pub fn charge_stop(&mut self, item: Interrupted) -> Interrupted {
249        self.steps = self.steps.saturating_add(item.steps);
250        if let (Stop::Budget, Some(cap)) = (item.by, self.cap) {
251            // An item split off before its predecessors were charged ran
252            // to the budget the pass started with; the sequential build
253            // stopped it at what was left.
254            self.steps = self.steps.min(cap);
255        }
256        self.stop(item.by)
257    }
258
259    fn stop(&self, by: Stop) -> Interrupted {
260        Interrupted {
261            by,
262            steps: self.steps,
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use core::sync::atomic::{AtomicU64, Ordering};
271
272    #[test]
273    fn none_never_stops() {
274        let mut m = Meter::new(&Control::NONE);
275        for _ in 0..1000 {
276            assert!(m.tick().is_ok());
277        }
278        assert_eq!(m.steps(), 1000);
279    }
280
281    #[test]
282    fn a_meter_remembers_its_first_stop() {
283        let mut m = Meter::new(&Control::budget(1));
284        assert_eq!(m.stopped(), None);
285        m.tick().unwrap();
286        assert_eq!(m.stopped(), None);
287        let e = m.tick().unwrap_err();
288        assert_eq!(m.stopped(), Some(e));
289        m.tick().unwrap_err();
290        assert_eq!(m.stopped(), Some(e));
291    }
292
293    #[test]
294    fn a_budget_of_n_allows_n_steps() {
295        let mut m = Meter::new(&Control::budget(3));
296        assert!(m.tick().is_ok() && m.tick().is_ok() && m.tick().is_ok());
297        let e = m.tick().unwrap_err();
298        assert_eq!(
299            e,
300            Interrupted {
301                by: Stop::Budget,
302                steps: 3
303            }
304        );
305        // Stays stopped, and does not count past the budget.
306        assert_eq!(m.tick().unwrap_err(), e);
307        assert_eq!(Meter::new(&Control::budget(0)).tick().unwrap_err().steps, 0);
308    }
309
310    #[test]
311    fn a_poll_is_asked_at_every_step_and_stops_at_its_first_true() {
312        let calls = AtomicU64::new(0);
313        let poll = || calls.fetch_add(1, Ordering::Relaxed) + 1 == 4;
314        let mut m = Meter::new(&Control::poll(&poll));
315        assert!(m.tick().is_ok() && m.tick().is_ok() && m.tick().is_ok());
316        let e = m.tick().unwrap_err();
317        assert_eq!(
318            e,
319            Interrupted {
320                by: Stop::Poll,
321                steps: 3
322            }
323        );
324    }
325
326    #[test]
327    fn the_budget_is_asked_before_the_poll() {
328        let calls = AtomicU64::new(0);
329        let poll = || {
330            calls.fetch_add(1, Ordering::Relaxed);
331            false
332        };
333        let mut m = Meter::new(&Control::poll(&poll).with_budget(1));
334        m.tick().unwrap();
335        assert_eq!(m.tick().unwrap_err().by, Stop::Budget);
336        assert_eq!(calls.load(Ordering::Relaxed), 1);
337    }
338
339    #[test]
340    fn a_split_is_capped_at_what_is_left() {
341        let mut m = Meter::new(&Control::budget(5));
342        m.tick().unwrap();
343        m.tick().unwrap();
344        let mut item = m.split();
345        for _ in 0..3 {
346            item.tick().unwrap();
347        }
348        assert_eq!(item.tick().unwrap_err().steps, 3);
349        assert_eq!(Meter::new(&Control::NONE).split().cap, None);
350    }
351
352    /// The parallel rule against the sequential one: items charged in
353    /// order stop at the same item, with the same count, as one meter
354    /// ticking through all of them.
355    #[test]
356    fn charging_in_order_stops_where_sequential_ticks_stop() {
357        let items = [2u64, 3, 1, 4];
358        for budget in 0..=12u64 {
359            let mut sequential = Meter::new(&Control::budget(budget));
360            let seq = items
361                .iter()
362                .enumerate()
363                .find_map(|(i, &n)| (0..n).find_map(|_| sequential.tick().err()).map(|e| (i, e)));
364            let mut joined = Meter::new(&Control::budget(budget));
365            let par = items.iter().enumerate().find_map(|(i, &n)| {
366                let mut item = joined.split();
367                let done = (0..n).try_for_each(|_| item.tick());
368                match done {
369                    Ok(()) => joined.charge(item.steps()).err().map(|e| (i, e)),
370                    Err(e) => Some((i, joined.charge_stop(e))),
371                }
372            });
373            assert_eq!(par, seq, "budget {budget}");
374            assert_eq!(joined.steps(), sequential.steps(), "budget {budget}");
375        }
376    }
377
378    /// Every item of a pass split off before any is charged, as the
379    /// parallel build does: the stop and its count are the sequential
380    /// ones all the same.
381    #[test]
382    fn items_split_together_stop_where_sequential_ticks_stop() {
383        let items = [2u64, 3, 1, 4];
384        for budget in 0..=12u64 {
385            let mut sequential = Meter::new(&Control::budget(budget));
386            let seq = items
387                .iter()
388                .enumerate()
389                .find_map(|(i, &n)| (0..n).find_map(|_| sequential.tick().err()).map(|e| (i, e)));
390            let mut joined = Meter::new(&Control::budget(budget));
391            let base = joined.split();
392            let par = items.iter().enumerate().find_map(|(i, &n)| {
393                let mut item = base;
394                let done = (0..n).try_for_each(|_| item.tick());
395                match done {
396                    Ok(()) => joined.charge(item.steps()).err().map(|e| (i, e)),
397                    Err(e) => Some((i, joined.charge_stop(e))),
398                }
399            });
400            assert_eq!(par, seq, "budget {budget}");
401            assert_eq!(joined.steps(), sequential.steps(), "budget {budget}");
402        }
403    }
404}