Skip to main content

ax_task/executor/
mod.rs

1//! Single-owner coroutine execution with interrupt-safe wake publication.
2//!
3//! Futures are polled and destroyed only by the owner thread. Wakers may cross
4//! CPUs and may run in hard interrupt context; their operations only touch
5//! atomics, publish intrusive nodes, and invoke a direct thread wake header.
6//! Zero-reference allocations are freed immediately in task context; hard IRQ
7//! transfers only the typed coroutine header to the task-work reaper.
8
9mod coroutine;
10mod inbox;
11mod waker;
12
13use alloc::{boxed::Box, rc::Rc, sync::Arc};
14use core::{
15    cell::{Cell, RefCell},
16    fmt,
17    future::Future,
18    marker::PhantomData,
19    ptr,
20    sync::atomic::{AtomicUsize, Ordering},
21    task::{Context, Poll, Waker},
22};
23
24pub use coroutine::{CoroutineHeader, CoroutineId};
25
26use self::{
27    coroutine::{COMPLETE, Coroutine, POLLING, RUN_QUEUED, release_reference, retain_reference},
28    inbox::{InboxKind, IntrusiveInbox},
29    waker::coroutine_waker,
30};
31use crate::{
32    runtime::{cpu::IrqGuardToken, task_runtime},
33    thread::{TaskError, ThreadId, ThreadWakeHandle},
34};
35
36/// Maximum number of futures polled by one executor turn.
37pub const DEFAULT_POLL_BATCH: usize = 64;
38
39/// Wakes a coroutine with Linux `WF_SYNC` scheduling semantics.
40///
41/// The hint is honored only for this executor's Wakers. Other Waker
42/// implementations receive an ordinary wake, preserving generic readiness
43/// interoperability. Callers must be in task context and expect to block
44/// shortly after publishing the readiness transition.
45pub fn wake_waker_sync(waker: Waker) {
46    debug_assert!(!task_runtime::in_hard_irq());
47    waker::wake_sync(waker);
48}
49
50const NOTIFIED: usize = 1 << 0;
51const PARKING: usize = 1 << 1;
52const PARKED: usize = 1 << 2;
53
54/// A single-thread executor whose owner may migrate between CPUs.
55///
56/// The executor is deliberately `!Send` and `!Sync`: only its scheduler thread
57/// may spawn, poll, park, or shut down futures. Its heap-pinned shared header is
58/// retained by coroutine allocations, so late wakers never address the local
59/// owner object after it has been dropped.
60pub struct LocalExecutor {
61    shared: Arc<SharedExecutor>,
62    ready_pending: Cell<*mut CoroutineHeader>,
63    active: Cell<*mut CoroutineHeader>,
64    next_generation: Cell<u64>,
65    _owner_thread_only: PhantomData<Rc<()>>,
66}
67
68impl LocalExecutor {
69    /// Creates an executor for the calling scheduler thread.
70    ///
71    /// The owner identity is derived from the direct wake header rather than a
72    /// caller-supplied integer, and is checked against the currently running
73    /// scheduler thread.
74    ///
75    /// # Errors
76    ///
77    /// Returns a scheduler facade error before runtime initialization, or
78    /// [`TaskError::ExecutorOwnerMismatch`] when `owner_wake` belongs to another
79    /// thread.
80    pub fn new(owner_wake: ThreadWakeHandle) -> Result<Self, TaskError> {
81        if crate::runtime::task_runtime::in_hard_irq() {
82            return Err(TaskError::UnsafeContext);
83        }
84        let expected = owner_wake.thread_id();
85        let actual = crate::thread::current::current_thread_id()?;
86        if actual != expected {
87            return Err(TaskError::ExecutorOwnerMismatch {
88                expected: expected.as_u64(),
89                actual: actual.as_u64(),
90            });
91        }
92        Ok(Self {
93            shared: Arc::new(SharedExecutor::new(owner_wake)),
94            ready_pending: Cell::new(ptr::null_mut()),
95            active: Cell::new(ptr::null_mut()),
96            next_generation: Cell::new(1),
97            _owner_thread_only: PhantomData,
98        })
99    }
100
101    /// Returns the scheduler thread that owns this executor.
102    pub fn owner_thread(&self) -> ThreadId {
103        self.shared.owner_thread
104    }
105
106    /// Allocates and schedules a `'static` coroutine for the owner thread.
107    ///
108    /// `future` need not implement `Send`; it is polled and dropped only by the
109    /// executor owner. Allocation happens here in ordinary thread context, never
110    /// in a waker operation.
111    pub fn spawn<F>(&self, future: F) -> CoroutineId
112    where
113        F: Future<Output = ()> + 'static,
114    {
115        self.assert_owner_context();
116        unsafe {
117            // A static future may remain active until explicit executor shutdown.
118            self.spawn_scoped(future).1
119        }
120    }
121
122    /// Runs one possibly borrowing future to completion on the owner thread.
123    ///
124    /// `park` is called only after the executor-side lost-wake handshake has
125    /// completed. It must pass the supplied [`ExecutorParkCondition`] into the
126    /// OS scheduler's predicate-aware park operation. Checking the condition
127    /// before an unconditional park is insufficient because scheduler wake
128    /// consumption may race between those two operations. The future is dropped
129    /// on the owner before this method returns or unwinds.
130    pub fn run<F, P>(&self, future: F, mut park: P) -> F::Output
131    where
132        F: Future,
133        P: FnMut(&ExecutorParkCondition<'_>),
134    {
135        self.assert_owner_context();
136        let output = RefCell::new(None);
137        let root = async {
138            output.replace(Some(future.await));
139        };
140        let (header, _) = unsafe {
141            // `ScopedRunGuard` cancels and empties this borrowing future before
142            // `output` and the caller's borrowed data can leave this stack.
143            self.spawn_scoped(root)
144        };
145        retain_reference(unsafe {
146            // The fresh allocation is live through its permanent owner reference.
147            &*header
148        });
149        let guard = ScopedRunGuard {
150            executor: self,
151            header,
152        };
153
154        while output.borrow().is_none() {
155            let batch = self.run_ready_batch();
156            if output.borrow().is_some() || batch.has_more() {
157                continue;
158            }
159            let Some(token) = self.prepare_park() else {
160                continue;
161            };
162            let condition = ExecutorParkCondition { executor: self };
163            park(&condition);
164            let _owner_work = token.finish();
165            unsafe {
166                // Returning from the OS park path is also a reason to recheck a
167                // root future for signal or non-executor readiness changes.
168                coroutine::schedule(header);
169            }
170        }
171
172        let result = output
173            .borrow_mut()
174            .take()
175            .unwrap_or_else(|| unreachable!("completed root future must publish output"));
176        drop(guard);
177        result
178    }
179
180    /// Polls at most 64 ready coroutines from one queue snapshot.
181    ///
182    /// Wakes produced while a future is being polled are published to the next
183    /// snapshot, so a self-waking future cannot consume the current batch.
184    pub fn run_ready_batch(&self) -> PollBatch {
185        self.assert_owner_context();
186        self.shared
187            .park_state
188            .fetch_and(!NOTIFIED, Ordering::AcqRel);
189        let mut cursor = self.take_ready_snapshot();
190        let mut polled = 0;
191        let mut completed = 0;
192
193        while !cursor.is_null() && polled < DEFAULT_POLL_BATCH {
194            let header = cursor;
195            cursor = unsafe {
196                // The ready queue reference keeps `header` alive. Only the owner
197                // consumes this detached list and may rewrite its next pointer.
198                IntrusiveInbox::take_next(header, InboxKind::Ready)
199            };
200            let did_complete = unsafe {
201                // Detached ready nodes are uniquely owned by this executor turn.
202                self.poll_ready_coroutine(header)
203            };
204            polled += usize::from(did_complete.was_polled());
205            completed += usize::from(did_complete.was_completed());
206        }
207
208        self.ready_pending.set(cursor);
209        PollBatch {
210            polled,
211            completed,
212            has_more: self.has_ready(),
213        }
214    }
215
216    /// Reports whether this executor has a coroutine ready for a future batch.
217    pub fn has_ready(&self) -> bool {
218        !self.ready_pending.get().is_null() || !self.shared.ready.is_empty()
219    }
220
221    /// Begins the `NOTIFIED/PARKING/PARKED` lost-wake handshake.
222    ///
223    /// The caller must hold the returned token across the task-system park
224    /// operation. A wake after this function succeeds observes `PARKED`, publishes
225    /// `NOTIFIED`, and wakes the owner through its direct thread wake header.
226    pub fn prepare_park(&self) -> Option<ParkToken<'_>> {
227        self.assert_owner_context();
228        if self.has_owner_work() {
229            return None;
230        }
231
232        let previous = self.shared.park_state.fetch_or(PARKING, Ordering::AcqRel);
233        if previous & (NOTIFIED | PARKING | PARKED) != 0 || self.has_owner_work() {
234            self.cancel_park_attempt();
235            return None;
236        }
237
238        if self
239            .shared
240            .park_state
241            .compare_exchange(PARKING, PARKED, Ordering::AcqRel, Ordering::Acquire)
242            .is_err()
243            || self.has_owner_work()
244        {
245            self.cancel_park_attempt();
246            return None;
247        }
248
249        Some(ParkToken {
250            executor: self,
251            active: true,
252            _owner_thread_only: PhantomData,
253        })
254    }
255
256    fn has_owner_work(&self) -> bool {
257        self.has_ready()
258    }
259
260    fn cancel_park_attempt(&self) {
261        self.shared
262            .park_state
263            .fetch_and(!(PARKING | PARKED | NOTIFIED), Ordering::AcqRel);
264    }
265
266    fn finish_park(&self) -> bool {
267        let state = self.shared.park_state.swap(0, Ordering::AcqRel);
268        state & NOTIFIED != 0 || self.has_owner_work()
269    }
270
271    fn assert_owner_context(&self) {
272        if crate::runtime::task_runtime::in_hard_irq() {
273            crate::runtime::task_runtime::fatal_invariant(
274                0x4558_0005,
275                self.owner_thread().as_u64() as usize,
276            );
277        }
278        match crate::thread::current::current_thread_id() {
279            Ok(actual) if actual == self.owner_thread() => {}
280            Ok(actual) => {
281                crate::runtime::task_runtime::fatal_invariant(0x4558_0003, actual.as_u64() as usize)
282            }
283            Err(_) => crate::runtime::task_runtime::fatal_invariant(
284                0x4558_0006,
285                self.owner_thread().as_u64() as usize,
286            ),
287        }
288    }
289}
290
291/// Borrowed executor predicate for one OS scheduler park attempt.
292///
293/// The OS adapter must evaluate [`Self::should_abort`] from inside its own
294/// generation-checked park handshake. The predicate performs only owner-local
295/// reads and atomic observations; it does not poll futures or invoke callbacks.
296pub struct ExecutorParkCondition<'executor> {
297    executor: &'executor LocalExecutor,
298}
299
300impl ExecutorParkCondition<'_> {
301    /// Reports whether executor work or a wake publication must cancel the OS park.
302    ///
303    /// This operation is bounded, non-blocking, and scheduler-non-reentrant, so
304    /// an OS adapter may call it from an IRQ-disabled wait-queue predicate.
305    pub fn should_abort(&self) -> bool {
306        self.executor.shared.park_state.load(Ordering::Acquire) & NOTIFIED != 0
307            || self.executor.has_owner_work()
308    }
309}
310
311impl fmt::Debug for ExecutorParkCondition<'_> {
312    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313        formatter
314            .debug_struct("ExecutorParkCondition")
315            .field("owner_thread", &self.executor.owner_thread())
316            .field("should_abort", &self.should_abort())
317            .finish()
318    }
319}
320
321impl Drop for LocalExecutor {
322    fn drop(&mut self) {
323        self.assert_owner_context();
324        self.shutdown();
325    }
326}
327
328/// Outcome of one bounded executor turn.
329#[derive(Clone, Copy, Debug, Eq, PartialEq)]
330pub struct PollBatch {
331    polled: usize,
332    completed: usize,
333    has_more: bool,
334}
335
336impl PollBatch {
337    /// Returns how many futures were polled.
338    pub const fn polled(self) -> usize {
339        self.polled
340    }
341
342    /// Returns how many futures completed.
343    pub const fn completed(self) -> usize {
344        self.completed
345    }
346
347    /// Reports whether another bounded turn has ready work.
348    pub const fn has_more(self) -> bool {
349        self.has_more
350    }
351}
352
353/// Proof that the owner completed the executor-side park handshake.
354///
355/// Hold this value while attempting to block the owner in the task system. Its
356/// drop closes the handshake even when the park attempt is cancelled.
357#[must_use = "the token must be held across the task-system park operation"]
358pub struct ParkToken<'executor> {
359    executor: &'executor LocalExecutor,
360    active: bool,
361    _owner_thread_only: PhantomData<Rc<()>>,
362}
363
364impl ParkToken<'_> {
365    /// Finishes a park attempt and reports whether a wake or work was observed.
366    pub fn finish(mut self) -> bool {
367        self.active = false;
368        self.executor.finish_park()
369    }
370}
371
372impl Drop for ParkToken<'_> {
373    fn drop(&mut self) {
374        if self.active {
375            self.executor.cancel_park_attempt();
376        }
377    }
378}
379
380pub(super) struct SharedExecutor {
381    owner_thread: ThreadId,
382    owner_wake: ThreadWakeHandle,
383    ready: IntrusiveInbox,
384    park_state: AtomicUsize,
385    ready_publication: AtomicUsize,
386}
387
388const READY_PUBLICATION_CLOSED: usize = 1usize << (usize::BITS - 1);
389const READY_PUBLISHER_COUNT_MASK: usize = READY_PUBLICATION_CLOSED - 1;
390
391impl SharedExecutor {
392    fn new(owner_wake: ThreadWakeHandle) -> Self {
393        Self {
394            owner_thread: owner_wake.thread_id(),
395            owner_wake,
396            ready: IntrusiveInbox::new(InboxKind::Ready),
397            park_state: AtomicUsize::new(0),
398            ready_publication: AtomicUsize::new(0),
399        }
400    }
401
402    pub(super) fn publish_ready(
403        &self,
404        header: *mut CoroutineHeader,
405        intent: crate::thread::WakeIntent,
406    ) -> bool {
407        let Some(_publisher) = self.begin_ready_publish_guard() else {
408            return false;
409        };
410        unsafe {
411            // RUN_QUEUED gives this node exclusive ready-list membership and its
412            // queue reference keeps the allocation alive until consumption.
413            self.ready.push(header);
414        }
415        self.notify_owner(intent);
416        true
417    }
418
419    fn begin_ready_publish_guard(&self) -> Option<ReadyPublishGuard<'_>> {
420        let irq_token = crate::runtime::enter_irq_guard(crate::runtime::IrqGuardSource::Executor);
421        if self.begin_ready_publish() {
422            Some(ReadyPublishGuard {
423                executor: self,
424                irq_token,
425                _not_send: PhantomData,
426            })
427        } else {
428            // SAFETY: this consumes the token created above on the same CPU;
429            // no publication guard escaped the failed closed-state check.
430            unsafe { task_runtime::irq_guard_exit(irq_token) };
431            None
432        }
433    }
434
435    fn begin_ready_publish(&self) -> bool {
436        let mut state = self.ready_publication.load(Ordering::Acquire);
437        loop {
438            if state & READY_PUBLICATION_CLOSED != 0 {
439                return false;
440            }
441            if state & READY_PUBLISHER_COUNT_MASK == READY_PUBLISHER_COUNT_MASK {
442                crate::runtime::task_runtime::fatal_invariant(
443                    0x4558_0007,
444                    self.owner_thread.as_u64() as usize,
445                );
446            }
447            match self.ready_publication.compare_exchange_weak(
448                state,
449                state + 1,
450                Ordering::AcqRel,
451                Ordering::Acquire,
452            ) {
453                Ok(_) => return true,
454                Err(updated) => state = updated,
455            }
456        }
457    }
458
459    fn finish_ready_publish(&self) {
460        let previous = self.ready_publication.fetch_sub(1, Ordering::Release);
461        debug_assert_ne!(previous & READY_PUBLISHER_COUNT_MASK, 0);
462    }
463
464    fn notify_owner(&self, intent: crate::thread::WakeIntent) {
465        let previous = self.park_state.fetch_or(NOTIFIED, Ordering::AcqRel);
466        if previous & PARKED != 0 {
467            let _result = if intent.is_sync() {
468                self.owner_wake.wake_sync()
469            } else {
470                self.owner_wake.wake()
471            };
472        }
473    }
474
475    fn close_and_wait_for_publishers(&self) {
476        self.ready_publication
477            .fetch_or(READY_PUBLICATION_CLOSED, Ordering::AcqRel);
478        while self.ready_publication.load(Ordering::Acquire) != READY_PUBLICATION_CLOSED {
479            core::hint::spin_loop();
480        }
481    }
482}
483
484struct ReadyPublishGuard<'executor> {
485    executor: &'executor SharedExecutor,
486    irq_token: IrqGuardToken,
487    _not_send: PhantomData<*mut ()>,
488}
489
490impl Drop for ReadyPublishGuard<'_> {
491    fn drop(&mut self) {
492        self.executor.finish_ready_publish();
493        // SAFETY: construction received this token on the current CPU, the
494        // !Send marker prevents migration, and Drop consumes it exactly once.
495        unsafe { task_runtime::irq_guard_exit(self.irq_token) };
496    }
497}
498
499struct ScopedRunGuard<'executor> {
500    executor: &'executor LocalExecutor,
501    header: *mut CoroutineHeader,
502}
503
504struct ReadyQueueReference {
505    header: *mut CoroutineHeader,
506    polling: bool,
507}
508
509struct OwnedCoroutineReference {
510    header: *mut CoroutineHeader,
511}
512
513impl OwnedCoroutineReference {
514    /// Takes ownership of one existing allocation reference.
515    ///
516    /// # Safety
517    ///
518    /// The caller must transfer exactly one live reference and must not release
519    /// that reference through another path after construction.
520    unsafe fn new(header: *mut CoroutineHeader) -> Self {
521        Self { header }
522    }
523}
524
525impl Drop for OwnedCoroutineReference {
526    fn drop(&mut self) {
527        unsafe {
528            // This guard owns exactly the reference transferred at construction.
529            release_reference(self.header);
530        }
531    }
532}
533
534impl ReadyQueueReference {
535    const fn new(header: *mut CoroutineHeader) -> Self {
536        Self {
537            header,
538            polling: false,
539        }
540    }
541
542    fn mark_polling(&mut self) {
543        self.polling = true;
544    }
545
546    fn finish_polling(&mut self) {
547        unsafe {
548            // Only the owner mutates POLLING, and the queue reference keeps the
549            // header live until this guard is dropped.
550            (*self.header).state.fetch_and(!POLLING, Ordering::AcqRel);
551        }
552        self.polling = false;
553    }
554}
555
556impl Drop for ReadyQueueReference {
557    fn drop(&mut self) {
558        if self.polling {
559            unsafe {
560                // A panicking poll has fully unwound before this guard runs; clear
561                // the owner-only poll marker before scoped cancellation proceeds.
562                (*self.header).state.fetch_and(!POLLING, Ordering::AcqRel);
563            }
564        }
565        unsafe {
566            // This guard owns the reference transferred by ready publication,
567            // including on poll panic and early-complete skip paths.
568            release_reference(self.header);
569        }
570    }
571}
572
573impl Drop for ScopedRunGuard<'_> {
574    fn drop(&mut self) {
575        let _scoped_reference = unsafe {
576            // The scoped guard's independent reference must survive cancellation
577            // and be released even if the future destructor unwinds.
578            OwnedCoroutineReference::new(self.header)
579        };
580        self.executor.cancel_coroutine(self.header);
581    }
582}
583
584#[derive(Clone, Copy)]
585enum PollDisposition {
586    Skipped,
587    Pending,
588    Completed,
589}
590
591impl PollDisposition {
592    const fn was_polled(self) -> bool {
593        !matches!(self, Self::Skipped)
594    }
595
596    const fn was_completed(self) -> bool {
597        matches!(self, Self::Completed)
598    }
599}
600
601mod polling;
602
603mod membership;
604
605mod shutdown;
606
607mod block_on;
608pub use block_on::{BlockOnError, block_on, block_on_timeout};