Skip to main content

moirai_async/executor/
core.rs

1use moirai_core::{Priority, TaskId};
2use moirai_pal::reactor::IoReactor;
3use std::future::Future;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::sync::Arc;
6use std::task::{Context, Poll, Waker};
7use std::time::Instant;
8
9use crate::executor::handle::AsyncHandle;
10use crate::executor::result_slot::AsyncResultSlot;
11use crate::executor::stats::{AsyncExecutorStats, ExecutorStats};
12use crate::executor::task::{AsyncTask, ErasedTaskFuture};
13use crate::executor::waker::ExecutorWaker;
14
15/// Native async executor with access to the PAL I/O reactor.
16pub struct AsyncExecutor {
17    /// Platform-specific I/O reactor
18    reactor: Arc<IoReactor>,
19    /// Run queue for ready tasks
20    run_queue: Arc<moirai_utils::queue::LockFreeQueue<Arc<AsyncTask>>>,
21    /// Runtime statistics
22    stats: AsyncExecutorStats,
23    /// Executor running state
24    running: Arc<AtomicBool>,
25    /// Monotonic task identifier source.
26    next_task_id: AtomicU64,
27}
28
29impl AsyncExecutor {
30    /// Create a new native async executor with a PAL I/O reactor handle.
31    pub fn new() -> std::io::Result<Self> {
32        let reactor = Arc::new(IoReactor::new()?);
33
34        Ok(Self {
35            reactor,
36            run_queue: Arc::new(moirai_utils::queue::LockFreeQueue::new()),
37            stats: AsyncExecutorStats::default(),
38            running: Arc::new(AtomicBool::new(false)),
39            next_task_id: AtomicU64::new(0),
40        })
41    }
42
43    /// Spawn an async task with default priority.
44    pub fn spawn<F, T>(&self, future: F) -> AsyncHandle<T>
45    where
46        F: Future<Output = T> + Send + 'static,
47        T: Send + 'static,
48    {
49        self.spawn_with_priority(future, Priority::Normal)
50    }
51
52    /// Spawn an async task with specified priority.
53    pub fn spawn_with_priority<F, T>(&self, future: F, priority: Priority) -> AsyncHandle<T>
54    where
55        F: Future<Output = T> + Send + 'static,
56        T: Send + 'static,
57    {
58        let task_id = TaskId::new(self.next_task_id.fetch_add(1, Ordering::Relaxed));
59        let result_slot = Arc::new(AsyncResultSlot::new());
60        let completion_slot = Arc::clone(&result_slot);
61
62        let wrapped_future = async move {
63            let result = future.await;
64            completion_slot.complete(result);
65        };
66
67        let task = Arc::new(AsyncTask {
68            task_id,
69            future: std::cell::UnsafeCell::new(ErasedTaskFuture::new(wrapped_future)),
70            future_lock: std::sync::Mutex::new(()),
71            is_queued: AtomicBool::new(true),
72            completed: AtomicBool::new(false),
73            priority,
74            created_at: Instant::now(),
75        });
76
77        self.run_queue.enqueue(Arc::clone(&task));
78
79        self.stats.tasks_spawned.fetch_add(1, Ordering::Relaxed);
80        self.stats.tasks_pending.fetch_add(1, Ordering::Relaxed);
81
82        let _ = self.reactor.wake();
83
84        AsyncHandle {
85            task_id,
86            result_slot,
87        }
88    }
89
90    /// Run the native async executor and poll the PAL reactor between task passes.
91    pub fn run(&self) -> std::io::Result<()> {
92        self.running.store(true, Ordering::SeqCst);
93
94        self.reactor.with_active(|| {
95            while self.running.load(Ordering::SeqCst) {
96                self.process_pending_tasks();
97
98                let has_tasks = self.stats.tasks_pending.load(Ordering::Acquire) > 0;
99
100                if !has_tasks {
101                    if !self.running.load(Ordering::SeqCst) {
102                        break;
103                    }
104                    self.reactor.run_iteration(None)?;
105                } else {
106                    let run_queue_empty = self.run_queue.is_empty();
107                    if run_queue_empty {
108                        self.reactor.run_iteration(None)?;
109                    } else {
110                        self.reactor
111                            .run_iteration(Some(std::time::Duration::from_millis(0)))?;
112                    }
113                }
114            }
115            Ok(())
116        })
117    }
118
119    /// Stop the async executor.
120    pub fn stop(&self) -> std::io::Result<()> {
121        self.running.store(false, Ordering::SeqCst);
122        self.reactor.stop()
123    }
124
125    /// Process all pending tasks.
126    pub(crate) fn process_pending_tasks(&self) {
127        while let Some(task) = self.run_queue.try_dequeue() {
128            task.is_queued.store(false, Ordering::SeqCst);
129
130            let waker = self.create_executor_waker(Arc::clone(&task));
131            let mut context = Context::from_waker(&waker);
132            let task_start = Instant::now();
133
134            // A wake can race a completion (the waker passed its `completed`
135            // check just before the task finished on another path), so this is
136            // the authoritative guard: never poll a future that already
137            // returned `Ready` — doing so panics with "resumed after
138            // completion".
139            //
140            // The check must happen *under* `future_lock`, together with the
141            // poll it guards. `is_queued` is cleared above so a self-wake during
142            // this poll can re-enqueue, which means a second polling thread can
143            // dequeue the same task while this one still holds the lock; if that
144            // thread tested `completed` before the lock, it would block, observe
145            // the completion only after acquiring the lock, and then poll the
146            // finished future anyway. Testing inside the critical section makes
147            // the guard and the poll atomic with respect to the completing
148            // writer below.
149            let _lock = task.future_lock.lock().unwrap();
150
151            if task.completed.load(Ordering::Acquire) {
152                continue;
153            }
154
155            let future_mut = unsafe { &mut *task.future.get() };
156            match future_mut.poll(&mut context) {
157                std::task::Poll::Ready(()) => {
158                    task.completed.store(true, Ordering::Release);
159                    self.stats.tasks_completed.fetch_add(1, Ordering::Relaxed);
160                    self.stats.tasks_pending.fetch_sub(1, Ordering::Relaxed);
161
162                    let execution_time = task_start.elapsed().as_nanos() as u64;
163                    self.stats
164                        .total_execution_time_ns
165                        .fetch_add(execution_time, Ordering::Relaxed);
166                }
167                std::task::Poll::Pending => {}
168            }
169        }
170    }
171
172    /// Create an executor-local waker for polling queued futures.
173    fn create_executor_waker(&self, task: Arc<AsyncTask>) -> Waker {
174        let waker = Arc::new(ExecutorWaker {
175            task,
176            run_queue: Arc::clone(&self.run_queue),
177            reactor: Arc::clone(&self.reactor),
178        });
179        Waker::from(waker)
180    }
181
182    /// Get current executor statistics.
183    pub fn stats(&self) -> ExecutorStats {
184        ExecutorStats {
185            tasks_spawned: self.stats.tasks_spawned.load(Ordering::Relaxed),
186            tasks_completed: self.stats.tasks_completed.load(Ordering::Relaxed),
187            tasks_pending: self.stats.tasks_pending.load(Ordering::Relaxed),
188            total_execution_time_ns: self.stats.total_execution_time_ns.load(Ordering::Relaxed),
189            waker_notifications: self.stats.waker_notifications.load(Ordering::Relaxed),
190            io_operations: self.stats.io_operations.load(Ordering::Relaxed),
191        }
192    }
193
194    /// Block on a future, running the executor until it completes.
195    pub fn block_on<F, T>(&self, future: F) -> T
196    where
197        F: Future<Output = T> + Send + 'static,
198        T: Send + 'static,
199    {
200        let handle = self.spawn(future);
201        let waker = futures::task::noop_waker();
202        let mut cx = Context::from_waker(&waker);
203        let mut pin_handle = Box::pin(handle);
204
205        self.running.store(true, Ordering::SeqCst);
206
207        loop {
208            self.process_pending_tasks();
209
210            match pin_handle.as_mut().poll(&mut cx) {
211                Poll::Ready(result) => {
212                    self.running.store(false, Ordering::SeqCst);
213                    return result;
214                }
215                Poll::Pending => {}
216            }
217
218            if self.run_queue.is_empty() {
219                self.reactor.run_iteration(None).ok();
220            } else {
221                self.reactor
222                    .run_iteration(Some(std::time::Duration::from_millis(0)))
223                    .ok();
224            }
225        }
226    }
227
228    /// Get access to the underlying I/O reactor for advanced operations.
229    pub fn reactor(&self) -> &IoReactor {
230        &self.reactor
231    }
232}
233
234impl Default for AsyncExecutor {
235    fn default() -> Self {
236        Self::new().expect("Failed to create default AsyncExecutor")
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_native_async_executor_creation() {
246        let executor = AsyncExecutor::new();
247        assert!(executor.is_ok());
248
249        let executor = executor.unwrap();
250        let stats = executor.stats();
251        assert_eq!(stats.tasks_spawned, 0);
252        assert_eq!(stats.tasks_completed, 0);
253        assert_eq!(stats.tasks_pending, 0);
254    }
255
256    #[test]
257    fn test_task_spawning() {
258        let executor = AsyncExecutor::new().unwrap();
259
260        let _handle = executor.spawn(async { 42 });
261
262        let stats = executor.stats();
263        assert_eq!(stats.tasks_spawned, 1);
264        assert_eq!(stats.tasks_pending, 1);
265    }
266
267    #[test]
268    fn test_task_ids_are_unique() {
269        let executor = AsyncExecutor::new().unwrap();
270
271        let first = executor.spawn(async { 1usize });
272        let second = executor.spawn(async { 2usize });
273
274        assert_ne!(first.id(), second.id());
275    }
276
277    #[test]
278    fn test_ready_task_completion_publishes_result() {
279        use std::task::Poll;
280
281        let executor = AsyncExecutor::new().unwrap();
282        let mut handle = Box::pin(executor.spawn(async { 7usize }));
283        let waker = futures::task::noop_waker();
284        let mut context = Context::from_waker(&waker);
285
286        assert!(matches!(handle.as_mut().poll(&mut context), Poll::Pending));
287
288        executor.process_pending_tasks();
289
290        assert_eq!(executor.stats().tasks_completed, 1);
291        assert!(matches!(handle.as_mut().poll(&mut context), Poll::Ready(7)));
292    }
293
294    #[test]
295    fn test_ready_task_completion_wakes_registered_handle() {
296        use futures::task::{waker_ref, ArcWake};
297        use std::sync::atomic::AtomicUsize;
298        use std::task::Poll;
299
300        struct WakeCounter(AtomicUsize);
301
302        impl ArcWake for WakeCounter {
303            fn wake_by_ref(arc_self: &Arc<Self>) {
304                arc_self.0.fetch_add(1, Ordering::SeqCst);
305            }
306        }
307
308        let executor = AsyncExecutor::new().unwrap();
309        let mut handle = Box::pin(executor.spawn(async { 11usize }));
310        let wake_counter = Arc::new(WakeCounter(AtomicUsize::new(0)));
311        let waker = waker_ref(&wake_counter);
312        let mut context = Context::from_waker(&waker);
313
314        assert!(matches!(handle.as_mut().poll(&mut context), Poll::Pending));
315
316        executor.process_pending_tasks();
317
318        assert_eq!(wake_counter.0.load(Ordering::SeqCst), 1);
319        assert!(matches!(
320            handle.as_mut().poll(&mut context),
321            Poll::Ready(11)
322        ));
323    }
324
325    #[test]
326    fn stale_waker_after_completion_does_not_repoll() {
327        // Regression: a completed task whose waker is fired again (as a live
328        // reactor waker would after a timeout race) must not be re-enqueued or
329        // re-polled — re-polling a finished `async` block panics with
330        // "resumed after completion".
331        let executor = AsyncExecutor::new().unwrap();
332        // Hold the handle so the result slot (and task) stay alive across passes.
333        let _handle = executor.spawn(async { 5usize });
334
335        // Drain the spawn: the task runs to completion.
336        executor.process_pending_tasks();
337        assert_eq!(executor.stats().tasks_completed, 1);
338
339        // Simulate a stale wake arriving after completion: fabricate the same
340        // executor waker the task carried and fire it. It must be a no-op.
341        let task = executor.run_queue.try_dequeue();
342        assert!(
343            task.is_none(),
344            "completed task must not be on the run queue"
345        );
346
347        // A second processing pass (a stale wake would have re-enqueued the
348        // finished task before this pass) must poll nothing and not panic.
349        executor.process_pending_tasks();
350        assert_eq!(
351            executor.stats().tasks_completed,
352            1,
353            "no re-poll of the completed task"
354        );
355    }
356
357    #[test]
358    fn completion_under_lock_blocks_a_concurrent_polling_thread() {
359        use std::sync::atomic::AtomicUsize;
360        use std::sync::{Arc, Barrier};
361
362        // Regression for a re-poll-after-completion race between two threads
363        // running `process_pending_tasks` on one executor (`run` takes `&self`,
364        // and the executor is shared as an `Arc` in-tree).
365        //
366        // `process_pending_tasks` clears `is_queued` before polling, so a wake
367        // during a poll re-enqueues the task and a second thread can dequeue it
368        // while the first still holds `future_lock`. If that thread tested
369        // `completed` *before* taking the lock, it would pass the test, block on
370        // the lock, and then poll a future the first thread had completed
371        // meanwhile — which panics with "resumed after completion".
372        //
373        // The interleaving is forced deterministically (no timing): this thread
374        // holds `future_lock` and marks the task completed while a second thread
375        // is provably parked inside `process_pending_tasks` on that same lock.
376        let executor = Arc::new(AsyncExecutor::new().unwrap());
377        let polls = Arc::new(AtomicUsize::new(0));
378
379        // A future that would panic if polled a second time, standing in for the
380        // "resumed after completion" panic of a finished `async` block.
381        let poll_counter = Arc::clone(&polls);
382        let _handle = executor.spawn(async move {
383            assert_eq!(
384                poll_counter.fetch_add(1, Ordering::SeqCst),
385                0,
386                "future must never be polled after completion"
387            );
388        });
389
390        // Take the queued task, hold its future lock, and put it back so the
391        // other thread dequeues the same task while the lock is held.
392        let task = executor
393            .run_queue
394            .try_dequeue()
395            .expect("spawned task must be queued");
396        let guard = task.future_lock.lock().unwrap();
397        executor.run_queue.enqueue(Arc::clone(&task));
398
399        let barrier = Arc::new(Barrier::new(2));
400        let poller = {
401            let executor = Arc::clone(&executor);
402            let barrier = Arc::clone(&barrier);
403            std::thread::spawn(move || {
404                barrier.wait();
405                // Dequeues the task, then parks on `future_lock`.
406                executor.process_pending_tasks();
407            })
408        };
409
410        barrier.wait();
411
412        // The poller is either about to block or already blocked on the lock we
413        // hold; completing the task now is exactly the race being guarded. The
414        // lock is released after the flag is set, so the poller observes a
415        // completed task the instant it acquires the lock.
416        task.completed.store(true, Ordering::Release);
417        drop(guard);
418
419        poller.join().expect("polling thread must not panic");
420
421        assert_eq!(
422            polls.load(Ordering::SeqCst),
423            0,
424            "a task completed while another thread waited on its lock must not be polled"
425        );
426    }
427
428    #[test]
429    fn test_priority_scheduling() {
430        let executor = AsyncExecutor::new().unwrap();
431
432        let _high_priority = executor.spawn_with_priority(async { "high" }, Priority::High);
433        let _normal_priority = executor.spawn_with_priority(async { "normal" }, Priority::Normal);
434        let _low_priority = executor.spawn_with_priority(async { "low" }, Priority::Low);
435
436        let stats = executor.stats();
437        assert_eq!(stats.tasks_spawned, 3);
438        assert_eq!(stats.tasks_pending, 3);
439    }
440}