Skip to main content

lumen_core/
task.rs

1//! The async seam: [`Spawn`] for running work off the main thread, [`Timer`]
2//! for waiting without blocking it, and the two service resources that carry
3//! the selected backend through the world.
4//!
5//! Lumen ships one implementation of both (`lumen-async-tokio`), and a crate
6//! that needs async work asks the world for [`SpawnService`] rather than
7//! naming that backend. Nothing installs an async backend by default, so a
8//! consumer must have a path that works when the resource is absent: a file
9//! dialog, for instance, falls back to a blocking call.
10//!
11//! Futures cross the seam boxed ([`BoxFuture`]) because both traits are used
12//! as trait objects.
13
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::Arc;
17use std::time::Duration;
18
19use bevy_ecs::prelude::Resource;
20
21/// A boxed, `Send` future with a `'static` lifetime: what the async seam
22/// passes across a trait-object boundary.
23pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
24
25/// An executor that runs futures away from the tick loop.
26///
27/// Implementations own their worker threads and outlive every future they are
28/// handed, so [`Self::spawn`] is fire-and-forget: a task that wants to report
29/// back to the world pushes a [`Command`](crate::command::Command) onto the
30/// [`CommandQueue`](crate::command::CommandQueue), which the next tick drains
31/// on the main thread.
32pub trait Spawn: Send + Sync + 'static {
33    /// Run `fut` to completion on the executor and return immediately.
34    ///
35    /// Dropping the returned unit does not cancel the task; there is no
36    /// handle, by design. Cancellation, timeouts, and result delivery are the
37    /// future's own business, which keeps the seam free of executor-specific
38    /// join-handle types.
39    fn spawn(&self, fut: BoxFuture<()>);
40
41    /// Run a blocking closure on the executor's blocking pool.
42    ///
43    /// For work that parks the calling thread (a synchronous file read, a
44    /// platform dialog with no async form). Implementations that have no
45    /// separate pool may run it on a plain thread.
46    fn spawn_blocking(&self, task: Box<dyn FnOnce() + Send + 'static>);
47}
48
49/// A source of futures that complete after a delay.
50///
51/// Separate from [`Spawn`] because waiting and executing are different
52/// capabilities: a caller that is already inside an async task needs the
53/// timer, not the executor.
54pub trait Timer: Send + Sync + 'static {
55    /// Returns a future that completes once `duration` has elapsed.
56    ///
57    /// Creating the future must not require an ambient executor context, so a
58    /// caller can build it anywhere and await it wherever it likes.
59    fn sleep(&self, duration: Duration) -> BoxFuture<()>;
60}
61
62/// The executor an app runs async work on, held as a `Resource` so any crate
63/// can reach it without naming a backend.
64///
65/// Install one in a plugin (`lumen-async-tokio`'s does), or from an app hook
66/// to swap in your own. Absence is meaningful: a consumer that finds no
67/// `SpawnService` takes its blocking path.
68#[derive(Resource, Clone)]
69pub struct SpawnService(Arc<dyn Spawn>);
70
71impl SpawnService {
72    /// Wrap an executor.
73    pub fn new<S: Spawn>(spawn: S) -> Self {
74        Self(Arc::new(spawn))
75    }
76
77    /// Borrow the executor as a trait object.
78    pub fn as_spawn(&self) -> &dyn Spawn {
79        self.0.as_ref()
80    }
81
82    /// Clone out the shared executor handle, for a consumer that stores it
83    /// past the borrow of the world.
84    pub fn handle(&self) -> Arc<dyn Spawn> {
85        Arc::clone(&self.0)
86    }
87}
88
89impl<S: Spawn> From<S> for SpawnService {
90    fn from(spawn: S) -> Self {
91        Self::new(spawn)
92    }
93}
94
95impl std::ops::Deref for SpawnService {
96    type Target = dyn Spawn;
97
98    fn deref(&self) -> &Self::Target {
99        self.0.as_ref()
100    }
101}
102
103/// The timer source an app waits on, held as a `Resource` alongside
104/// [`SpawnService`].
105#[derive(Resource, Clone)]
106pub struct TimerService(Arc<dyn Timer>);
107
108impl TimerService {
109    /// Wrap a timer.
110    pub fn new<T: Timer>(timer: T) -> Self {
111        Self(Arc::new(timer))
112    }
113
114    /// Borrow the timer as a trait object.
115    pub fn as_timer(&self) -> &dyn Timer {
116        self.0.as_ref()
117    }
118
119    /// Clone out the shared timer handle.
120    pub fn handle(&self) -> Arc<dyn Timer> {
121        Arc::clone(&self.0)
122    }
123}
124
125impl<T: Timer> From<T> for TimerService {
126    fn from(timer: T) -> Self {
127        Self::new(timer)
128    }
129}
130
131impl std::ops::Deref for TimerService {
132    type Target = dyn Timer;
133
134    fn deref(&self) -> &Self::Target {
135        self.0.as_ref()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use std::sync::atomic::{AtomicUsize, Ordering};
143
144    /// Executor that runs each future on its own thread and joins it before
145    /// returning. Enough to prove the trait objects dispatch.
146    #[derive(Default)]
147    struct ThreadSpawn {
148        spawned: Arc<AtomicUsize>,
149    }
150
151    impl Spawn for ThreadSpawn {
152        fn spawn(&self, fut: BoxFuture<()>) {
153            self.spawned.fetch_add(1, Ordering::SeqCst);
154            let handle = std::thread::spawn(move || block_on_unit(fut));
155            handle.join().expect("task thread joins");
156        }
157
158        fn spawn_blocking(&self, task: Box<dyn FnOnce() + Send + 'static>) {
159            self.spawned.fetch_add(1, Ordering::SeqCst);
160            std::thread::spawn(task).join().expect("task thread joins");
161        }
162    }
163
164    struct InstantTimer;
165
166    impl Timer for InstantTimer {
167        fn sleep(&self, _duration: Duration) -> BoxFuture<()> {
168            Box::pin(std::future::ready(()))
169        }
170    }
171
172    /// Minimal executor for a future that never yields pending forever: polls
173    /// with a no-op waker until it resolves. The test futures either resolve
174    /// on the first poll or are driven by another thread.
175    fn block_on_unit(mut fut: BoxFuture<()>) {
176        let waker = std::task::Waker::noop();
177        let mut cx = std::task::Context::from_waker(waker);
178        loop {
179            if fut.as_mut().poll(&mut cx).is_ready() {
180                return;
181            }
182            std::thread::yield_now();
183        }
184    }
185
186    #[test]
187    fn spawn_service_dispatches_to_the_installed_executor() {
188        let counter = Arc::new(AtomicUsize::new(0));
189        let service = SpawnService::new(ThreadSpawn {
190            spawned: Arc::clone(&counter),
191        });
192        let ran = Arc::new(AtomicUsize::new(0));
193        let ran_in_task = Arc::clone(&ran);
194        service.spawn(Box::pin(async move {
195            ran_in_task.fetch_add(1, Ordering::SeqCst);
196        }));
197        assert_eq!(counter.load(Ordering::SeqCst), 1);
198        assert_eq!(ran.load(Ordering::SeqCst), 1);
199    }
200
201    #[test]
202    fn spawn_service_dispatches_blocking_work() {
203        let service = SpawnService::from(ThreadSpawn::default());
204        let ran = Arc::new(AtomicUsize::new(0));
205        let ran_in_task = Arc::clone(&ran);
206        service.spawn_blocking(Box::new(move || {
207            ran_in_task.fetch_add(1, Ordering::SeqCst);
208        }));
209        assert_eq!(ran.load(Ordering::SeqCst), 1);
210    }
211
212    #[test]
213    fn timer_service_returns_a_future_from_the_installed_timer() {
214        let service = TimerService::from(InstantTimer);
215        block_on_unit(service.sleep(Duration::from_secs(3600)));
216    }
217
218    #[test]
219    fn services_are_cloneable_handles_on_one_backend() {
220        let counter = Arc::new(AtomicUsize::new(0));
221        let service = SpawnService::new(ThreadSpawn {
222            spawned: Arc::clone(&counter),
223        });
224        let clone = service.clone();
225        service.spawn(Box::pin(async {}));
226        clone.spawn(Box::pin(async {}));
227        clone.handle().spawn(Box::pin(async {}));
228        assert_eq!(counter.load(Ordering::SeqCst), 3);
229    }
230}