Skip to main content

guinea_core/
executor.rs

1//! Where guinea's own background work runs.
2//!
3//! Everything guinea schedules - a background task, an answer carried back to
4//! the UI thread - goes through here rather than straight to tokio, so that a
5//! test can put a [`Seeded`] executor on its thread and decide the order
6//! itself. Outside such a test this is `tokio::spawn`.
7
8use std::future::Future;
9use std::pin::Pin;
10use std::task::{Context, Poll};
11
12/// Runs `task` in the background.
13pub fn spawn<F>(task: F)
14where
15    F: Future<Output = ()> + Send + 'static,
16{
17    #[cfg(feature = "test-utils")]
18    let task = match seeded::spawn_here(Box::pin(task)) {
19        Ok(()) => return,
20        Err(task) => task,
21    };
22
23    tokio::spawn(task);
24}
25
26/// Gives way once: the task is woken and set aside, and whatever else is
27/// ready runs before it resumes.
28///
29/// Under a [`Seeded`] executor this is where two tasks can change places, so a
30/// fake that stands in for slow work yields where the real work would wait.
31pub fn yield_now() -> impl Future<Output = ()> {
32    YieldNow { yielded: false }
33}
34
35/// Gives way a random number of times - as many as the seed says under a
36/// [`Seeded`] executor, once outside one.
37///
38/// What a fake does where the real work takes as long as it takes. The number
39/// comes from the executor's own generator, so a seed replays the delays along
40/// with the order.
41pub fn random_delay() -> impl Future<Output = ()> {
42    RandomDelay { left: None }
43}
44
45#[cfg(feature = "test-utils")]
46const LONGEST_DELAY: usize = 10;
47
48struct RandomDelay {
49    left: Option<usize>,
50}
51
52impl Future for RandomDelay {
53    type Output = ();
54
55    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
56        let left = *self.left.get_or_insert_with(draw_delay);
57        if left == 0 {
58            return Poll::Ready(());
59        }
60
61        self.left = Some(left - 1);
62        cx.waker().wake_by_ref();
63        Poll::Pending
64    }
65}
66
67fn draw_delay() -> usize {
68    #[cfg(feature = "test-utils")]
69    if let Some(turns) = seeded::draw(LONGEST_DELAY + 1) {
70        return turns;
71    }
72
73    1
74}
75
76struct YieldNow {
77    yielded: bool,
78}
79
80impl Future for YieldNow {
81    type Output = ();
82
83    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
84        if self.yielded {
85            return Poll::Ready(());
86        }
87
88        self.yielded = true;
89        cx.waker().wake_by_ref();
90        Poll::Pending
91    }
92}
93
94#[cfg(feature = "test-utils")]
95pub use seeded::{Installed, Seeded, install};
96
97#[cfg(feature = "test-utils")]
98pub(crate) use seeded::queue_ui;
99
100#[cfg(feature = "test-utils")]
101mod seeded {
102    use std::cell::{Cell, RefCell};
103    use std::collections::{HashMap, VecDeque};
104    use std::future::Future;
105    use std::pin::Pin;
106    use std::rc::Rc;
107    use std::sync::{Arc, Mutex, MutexGuard};
108    use std::task::{Context, Poll, Wake, Waker};
109    use std::time::Duration;
110
111    use tokio::runtime::Runtime;
112    use tokio::time::{Instant, Sleep};
113
114    type Task = Pin<Box<dyn Future<Output = ()> + Send>>;
115    type Job = Box<dyn FnOnce() + Send>;
116
117    enum Work {
118        Poll(u64),
119        Ui(Job),
120    }
121
122    fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
123        mutex.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
124    }
125
126    /// What wakers reach, from whichever thread wakes them.
127    ///
128    /// Two kinds, kept apart. Tasks that are ready may go in any order - they
129    /// run on threads of their own in the application, and whichever finishes
130    /// first is what the seed stands for. Jobs for the UI thread may not: the
131    /// application's dispatcher queue runs them first in, first out, so two
132    /// posts from one place never swap. The seed decides only when the queue
133    /// moves, against the tasks.
134    #[derive(Default)]
135    struct Ready {
136        polls: Mutex<Vec<u64>>,
137        ui: Mutex<VecDeque<Job>>,
138        /// The clock waiting in [`Seeded::advance`] for something to become
139        /// ready, if it is.
140        waiting: Mutex<Option<Waker>>,
141    }
142
143    impl Ready {
144        fn push(&self, work: Work) {
145            match work {
146                Work::Poll(id) => lock(&self.polls).push(id),
147                Work::Ui(job) => lock(&self.ui).push_back(job),
148            }
149
150            if let Some(waiting) = lock(&self.waiting).take() {
151                waiting.wake();
152            }
153        }
154
155        fn is_empty(&self) -> bool {
156            lock(&self.polls).is_empty() && lock(&self.ui).is_empty()
157        }
158    }
159
160    /// Lets the clock run until `until`, or until work turns up first - the
161    /// answer is whether `until` was reached.
162    struct Until<'a> {
163        until: Pin<Box<Sleep>>,
164        ready: &'a Ready,
165    }
166
167    impl Future for Until<'_> {
168        type Output = bool;
169
170        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<bool> {
171            *lock(&self.ready.waiting) = Some(cx.waker().clone());
172
173            if !self.ready.is_empty() {
174                return Poll::Ready(false);
175            }
176
177            self.until.as_mut().poll(cx).map(|()| true)
178        }
179    }
180
181    struct Waking {
182        task: u64,
183        ready: Arc<Ready>,
184    }
185
186    impl Wake for Waking {
187        fn wake(self: Arc<Self>) {
188            self.wake_by_ref();
189        }
190
191        fn wake_by_ref(self: &Arc<Self>) {
192            self.ready.push(Work::Poll(self.task));
193        }
194    }
195
196    thread_local! {
197        static CURRENT: RefCell<Option<Rc<Seeded>>> = const { RefCell::new(None) };
198    }
199
200    /// One thread, one order: every background task and every answer bound
201    /// for the UI thread runs here, and which of the ready ones goes next is
202    /// picked by a generator started from `seed`.
203    ///
204    /// The same seed picks the same order every time, so an order that breaks
205    /// something can be had again by its number.
206    ///
207    /// Its clock is a paused tokio runtime: work runs inside its context, so
208    /// `tokio::time::sleep`, `interval` and `timeout` are made against it, and
209    /// its time moves only by [`Installed::advance`].
210    pub struct Seeded {
211        seed: u64,
212        state: Cell<u64>,
213        ready: Arc<Ready>,
214        tasks: RefCell<HashMap<u64, Task>>,
215        next: Cell<u64>,
216        running: Cell<bool>,
217        clock: Runtime,
218    }
219
220    /// Puts a [`Seeded`] executor on this thread until the guard drops.
221    pub fn install(seed: u64) -> Installed {
222        let clock = tokio::runtime::Builder::new_current_thread()
223            .enable_all()
224            .start_paused(true)
225            .build()
226            .expect("a paused runtime to keep the test's time");
227
228        let executor = Rc::new(Seeded {
229            seed,
230            state: Cell::new(seed),
231            ready: Arc::default(),
232            tasks: RefCell::default(),
233            next: Cell::new(1),
234            running: Cell::new(false),
235            clock,
236        });
237
238        CURRENT.with(|current| *current.borrow_mut() = Some(executor.clone()));
239        Installed(executor)
240    }
241
242    /// The executor on this thread, for as long as it is held.
243    pub struct Installed(Rc<Seeded>);
244
245    impl Installed {
246        pub fn seed(&self) -> u64 {
247            self.0.seed
248        }
249
250        /// Runs whatever is ready, one piece at a time in the seed's order,
251        /// until nothing is. Time does not move. Returns how many pieces ran.
252        pub fn run_until_parked(&self) -> usize {
253            self.0.run_until_parked()
254        }
255
256        /// Runs one piece of whatever is ready, the one the seed picks.
257        /// `false` when nothing was.
258        pub fn step(&self) -> bool {
259            self.0.step()
260        }
261
262        /// Moves the clock on by `by`, one timer at a time, running whatever
263        /// each timer sets off before the next one fires. Returns how many
264        /// pieces ran.
265        pub fn advance(&self, by: Duration) -> usize {
266            self.0.advance(by)
267        }
268
269        /// Moves the clock to the next timer due within `horizon` and lets it
270        /// fire; runs nothing. `false` when none was due - the clock is then
271        /// `horizon` further on.
272        pub fn wake_next(&self, horizon: Duration) -> bool {
273            self.0.wake_next(horizon)
274        }
275
276        /// Tasks that are neither finished nor ready - waiting on a timer the
277        /// clock has not reached, or on something nothing here will wake.
278        pub fn stuck(&self) -> usize {
279            self.0.tasks.borrow().len()
280        }
281    }
282
283    impl Drop for Installed {
284        fn drop(&mut self) {
285            CURRENT.with(|current| {
286                let mut current = current.borrow_mut();
287                if current.as_ref().is_some_and(|installed| Rc::ptr_eq(installed, &self.0)) {
288                    *current = None;
289                }
290            });
291        }
292    }
293
294    impl Seeded {
295        fn spawn(&self, task: Task) {
296            let id = self.next.get();
297            self.next.set(id + 1);
298
299            self.tasks.borrow_mut().insert(id, task);
300            self.ready.push(Work::Poll(id));
301        }
302
303        /// SplitMix64: a few lines, no dependency, and a good enough spread
304        /// for picking among a handful of tasks.
305        fn below(&self, bound: usize) -> usize {
306            let mut z = self.state.get().wrapping_add(0x9E37_79B9_7F4A_7C15);
307            self.state.set(z);
308
309            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
310            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
311            z ^= z >> 31;
312
313            (z % bound as u64) as usize
314        }
315
316        /// One of the ready tasks, or the job at the head of the UI queue -
317        /// which of those, the seed picks.
318        fn take(&self) -> Option<Work> {
319            let mut polls = lock(&self.ready.polls);
320            let mut ui = lock(&self.ready.ui);
321
322            let choices = polls.len() + usize::from(!ui.is_empty());
323            if choices == 0 {
324                return None;
325            }
326
327            let at = self.below(choices);
328            if at == polls.len() {
329                ui.pop_front().map(Work::Ui)
330            } else {
331                Some(Work::Poll(polls.swap_remove(at)))
332            }
333        }
334
335        fn step(&self) -> bool {
336            assert!(
337                !self.running.replace(true),
338                "the executor was asked to run work from inside work it is running"
339            );
340
341            let _clock = self.clock.enter();
342
343            let ran = match self.take() {
344                Some(Work::Ui(job)) => {
345                    job();
346                    true
347                }
348                Some(Work::Poll(id)) => {
349                    self.poll(id);
350                    true
351                }
352                None => false,
353            };
354
355            self.running.set(false);
356            ran
357        }
358
359        fn run_until_parked(&self) -> usize {
360            let mut ran = 0;
361            while self.step() {
362                ran += 1;
363            }
364            ran
365        }
366
367        fn now(&self) -> Instant {
368            self.clock.block_on(async { Instant::now() })
369        }
370
371        /// Lets the clock run until `target`, or until work turns up first;
372        /// whether `target` was reached.
373        fn run_clock_until(&self, target: Instant) -> bool {
374            self.clock.block_on(async {
375                Until {
376                    until: Box::pin(tokio::time::sleep_until(target)),
377                    ready: &self.ready,
378                }
379                .await
380            })
381        }
382
383        fn advance(&self, by: Duration) -> usize {
384            let target = self.now() + by;
385            let mut ran = self.run_until_parked();
386
387            loop {
388                let reached = self.run_clock_until(target);
389                ran += self.run_until_parked();
390
391                if reached {
392                    return ran;
393                }
394            }
395        }
396
397        fn wake_next(&self, horizon: Duration) -> bool {
398            let target = self.now() + horizon;
399            !self.run_clock_until(target)
400        }
401
402        fn poll(&self, id: u64) {
403            let Some(mut task) = self.tasks.borrow_mut().remove(&id) else {
404                return;
405            };
406
407            let waker = Waker::from(Arc::new(Waking {
408                task: id,
409                ready: self.ready.clone(),
410            }));
411
412            if task.as_mut().poll(&mut Context::from_waker(&waker)).is_pending() {
413                self.tasks.borrow_mut().insert(id, task);
414            }
415        }
416    }
417
418    fn current() -> Option<Rc<Seeded>> {
419        CURRENT.with(|current| current.borrow().clone())
420    }
421
422    /// Hands `task` to the executor on this thread, or back if there is none.
423    pub(super) fn spawn_here(task: Task) -> Result<(), Task> {
424        match current() {
425            Some(executor) => {
426                executor.spawn(task);
427                Ok(())
428            }
429            None => Err(task),
430        }
431    }
432
433    /// A number below `bound` from the executor on this thread, if there is
434    /// one.
435    pub(super) fn draw(bound: usize) -> Option<usize> {
436        current().map(|executor| executor.below(bound))
437    }
438
439    /// Queues `job` as work for the UI thread, or hands it back if no
440    /// executor is on this thread.
441    pub(crate) fn queue_ui<F>(job: F) -> Result<(), F>
442    where
443        F: FnOnce() + Send + 'static,
444    {
445        match current() {
446            Some(executor) => {
447                executor.ready.push(Work::Ui(Box::new(job)));
448                Ok(())
449            }
450            None => Err(job),
451        }
452    }
453
454    #[cfg(test)]
455    mod tests {
456        use super::*;
457        use std::sync::atomic::{AtomicUsize, Ordering};
458        use std::time::Duration;
459
460        fn order(seed: u64) -> Vec<&'static str> {
461            let executor = install(seed);
462            let seen = Arc::new(Mutex::new(Vec::new()));
463
464            for name in ["a", "b", "c"] {
465                let seen = seen.clone();
466                crate::executor::spawn(async move {
467                    crate::executor::yield_now().await;
468                    seen.lock().unwrap().push(name);
469                });
470            }
471
472            executor.run_until_parked();
473            let seen = seen.lock().unwrap().clone();
474            seen
475        }
476
477        #[test]
478        fn one_seed_is_one_order() {
479            for seed in 0..16 {
480                assert_eq!(order(seed), order(seed), "seed {seed}");
481            }
482        }
483
484        #[test]
485        fn the_seeds_between_them_reach_other_orders() {
486            let orders: std::collections::HashSet<Vec<&str>> = (0..64).map(order).collect();
487            assert!(orders.len() > 1, "every seed ran the tasks in one order: {orders:?}");
488        }
489
490        #[test]
491        fn work_for_the_ui_thread_runs_on_this_one() {
492            let executor = install(7);
493            let ran = Arc::new(AtomicUsize::new(0));
494
495            let counted = ran.clone();
496            crate::actor::invoke_on_ui(move || {
497                counted.fetch_add(1, Ordering::SeqCst);
498            });
499
500            executor.run_until_parked();
501            assert_eq!(ran.load(Ordering::SeqCst), 1);
502        }
503
504        /// Posts to the UI thread from one place arrive in the order they
505        /// were made, whatever the seed - the application's dispatcher queue
506        /// never swaps them - while tasks ready beside them still move around
507        /// them.
508        #[test]
509        fn posts_to_the_ui_thread_keep_their_order_in_every_seed() {
510            let mut interleavings = std::collections::HashSet::new();
511
512            for seed in 0..64 {
513                let executor = install(seed);
514                let seen = Arc::new(Mutex::new(Vec::new()));
515
516                for post in ["first", "second", "third"] {
517                    let seen = seen.clone();
518                    crate::actor::invoke_on_ui(move || seen.lock().unwrap().push(post));
519                }
520
521                let beside = seen.clone();
522                crate::executor::spawn(async move { beside.lock().unwrap().push("task") });
523
524                executor.run_until_parked();
525                let seen = seen.lock().unwrap().clone();
526
527                let posts: Vec<&str> = seen.iter().copied().filter(|name| *name != "task").collect();
528                assert_eq!(posts, ["first", "second", "third"], "seed {seed}");
529                interleavings.insert(seen);
530            }
531
532            assert!(interleavings.len() > 1, "the task always ran in one place: {interleavings:?}");
533        }
534
535        #[test]
536        fn a_sleeping_task_wakes_when_the_clock_reaches_it_and_not_before() {
537            let started = std::time::Instant::now();
538            let executor = install(3);
539            let woke = Arc::new(AtomicUsize::new(0));
540
541            let marked = woke.clone();
542            crate::executor::spawn(async move {
543                tokio::time::sleep(Duration::from_millis(800)).await;
544                marked.fetch_add(1, Ordering::SeqCst);
545            });
546
547            executor.run_until_parked();
548            assert_eq!(woke.load(Ordering::SeqCst), 0, "time moved without being asked");
549
550            executor.advance(Duration::from_millis(799));
551            assert_eq!(woke.load(Ordering::SeqCst), 0, "woke a millisecond early");
552
553            executor.advance(Duration::from_millis(1));
554            assert_eq!(woke.load(Ordering::SeqCst), 1);
555            assert!(started.elapsed() < Duration::from_millis(200), "waited in real time");
556        }
557
558        #[test]
559        fn a_timer_that_never_ends_still_lets_advance_return() {
560            let executor = install(5);
561            let ticks = Arc::new(AtomicUsize::new(0));
562
563            let counted = ticks.clone();
564            crate::executor::spawn(async move {
565                let mut every = tokio::time::interval(Duration::from_millis(100));
566                loop {
567                    every.tick().await;
568                    counted.fetch_add(1, Ordering::SeqCst);
569                }
570            });
571
572            executor.advance(Duration::from_secs(1));
573            assert_eq!(ticks.load(Ordering::SeqCst), 11, "the first tick and one per 100 ms");
574            assert_eq!(executor.stuck(), 1);
575        }
576
577        #[test]
578        fn a_task_waiting_on_nothing_is_counted_as_stuck() {
579            let executor = install(1);
580            crate::executor::spawn(std::future::pending());
581
582            executor.run_until_parked();
583            assert_eq!(executor.stuck(), 1);
584        }
585    }
586}