rivet/executor.rs
1//! Priority-aware async executor with single-stack task polling.
2//!
3//! All cooperative tasks share one system stack. The executor polls tasks
4//! in priority order. When a task returns `Poll::Pending`, its state lives
5//! in a static `TaskCell` referenced by the task's `TaskReg.user_data` —
6//! no per-task stack allocation is needed.
7//!
8//! When all tasks are pending, the executor calls `port::arch::idle()` to enter
9//! a low-power state.
10
11use core::task::Poll;
12
13use crate::task::{iter_task_regs, TaskReg, TaskRegistry};
14use crate::waker;
15
16/// Identity of the task currently being polled, encoded as
17/// `(priority << 8) | index`. `u32::MAX` means "not currently polling a task".
18/// Set by the executor immediately before calling a task's `poll_fn` and
19/// read by sync primitives (`Semaphore`, `Channel`, `Sleep`) so they know
20/// which task to register as a waiter, without needing the user to pass
21/// priority/index manually.
22#[cfg(not(loom))]
23static CURRENT_TASK: crate::sync::atomic::AtomicU32 = crate::sync::atomic::AtomicU32::new(u32::MAX);
24#[cfg(loom)]
25loom::lazy_static! {
26 static ref CURRENT_TASK: crate::sync::atomic::AtomicU32 = crate::sync::atomic::AtomicU32::new(u32::MAX);
27}
28
29fn set_current(id: crate::task::TaskId) {
30 CURRENT_TASK.store(id.as_u16() as u32, crate::sync::atomic::Ordering::Release);
31}
32
33fn clear_current() {
34 CURRENT_TASK.store(u32::MAX, crate::sync::atomic::Ordering::Release);
35}
36
37/// Test-only: simulate being inside a task's poll, so `Semaphore::acquire()`,
38/// `Channel::send()/recv()`, and `Sleep` can be exercised directly from host
39/// tests without spinning up the full executor loop.
40#[doc(hidden)]
41pub fn set_current_for_test(priority: u8, index: u8) {
42 set_current(crate::task::TaskId::new(priority, index));
43}
44
45/// Test-only: clear the simulated task context set by [`set_current_for_test`].
46#[doc(hidden)]
47pub fn clear_current_for_test() {
48 clear_current();
49}
50
51/// The identity of the task currently being polled. `None` if called
52/// outside of a task's poll (e.g. from an ISR or before the executor starts).
53///
54/// Used by `Semaphore::acquire()`, `Channel::send()/recv()`, and `Sleep`
55/// to register themselves as waiters without requiring the caller to pass
56/// `(priority, index)` explicitly.
57pub fn current_task() -> Option<crate::task::TaskId> {
58 let v = CURRENT_TASK.load(crate::sync::atomic::Ordering::Acquire);
59 if v == u32::MAX {
60 None
61 } else {
62 Some(crate::task::TaskId::from_u16(v as u16))
63 }
64}
65
66/// The global executor singleton.
67pub struct Executor {
68 registry: TaskRegistry,
69 /// Number of tasks not yet completed (plan.md [B10]): decremented the
70 /// first time a task's poll returns `Ready`. Used to skip re-polling
71 /// completed tasks and to know when the cooperative tier is idle.
72 live_tasks: crate::sync::atomic::AtomicUsize,
73}
74
75impl Executor {
76 pub const fn new() -> Self {
77 Self {
78 registry: TaskRegistry::new(),
79 live_tasks: crate::sync::atomic::AtomicUsize::new(0),
80 }
81 }
82
83 /// Discover tasks from the `.rivet_tasks` linker section.
84 /// Assigns per-priority indices. Must be called once before `run()`.
85 pub fn init(&mut self) {
86 let mut counts: [u8; 32] = [0; 32];
87
88 for reg in iter_task_regs() {
89 let prio = reg.priority as usize;
90 if prio > (crate::task::MAX_PRIORITY as usize) {
91 panic!(
92 "rivet: task priority {} exceeds MAX_PRIORITY {} \
93 (check #[rivet::task(priority = ...)])",
94 reg.priority,
95 crate::task::MAX_PRIORITY
96 );
97 }
98 let idx = counts[prio] as usize;
99 if idx >= crate::task::MAX_TASKS {
100 // plan.md [B12]: overflow must be loud, not a silent drop.
101 panic!(
102 "rivet: too many #[rivet::task]s at priority {} \
103 (limit MAX_TASKS = {} per priority)",
104 reg.priority,
105 crate::task::MAX_TASKS
106 );
107 }
108
109 self.registry.tasks[prio][idx] = Some(reg as *const TaskReg);
110 counts[prio] += 1;
111 self.registry.total += 1;
112 self.live_tasks
113 .fetch_add(1, crate::sync::atomic::Ordering::Relaxed);
114 }
115
116 self.registry.count_per_priority[..=(crate::task::MAX_PRIORITY as usize)]
117 .copy_from_slice(&counts[..=(crate::task::MAX_PRIORITY as usize)]);
118
119 // Mark all tasks as initially ready so the executor polls them once
120 // to start their async state machines running.
121 for (p, &count) in counts[..=(crate::task::MAX_PRIORITY as usize)]
122 .iter()
123 .enumerate()
124 {
125 for i in 0..(count as usize) {
126 crate::waker::mark_ready(crate::task::TaskId::new(p as u8, i as u8));
127 }
128 }
129 }
130
131 /// Main executor loop. Never returns.
132 pub fn run(&self) -> ! {
133 loop {
134 waker::clear_pend();
135
136 // Poll all ready tasks, highest priority first.
137 while let Some(id) = waker::next_ready() {
138 let reg = match self.lookup_task(id.priority(), id.index()) {
139 Some(r) => r,
140 None => continue,
141 };
142
143 // Skip completed tasks woken by a stale registration
144 // (plan.md [B10]).
145 // SAFETY: `reg.completed_fn` was paired with this task's
146 // `TaskCell` by `#[rivet::task]`/`register_task!`.
147 unsafe {
148 if (reg.completed_fn)(reg.user_data) {
149 continue;
150 }
151 }
152
153 let task_waker = waker::task_waker(id);
154
155 set_current(id);
156 // SAFETY: `reg.poll_fn` is a type-erased poll function
157 // paired with `reg.user_data` (a `TaskCell` pointer) at
158 // registration time by `#[rivet::task]`/`register_task!`.
159 // The executor only ever polls a task that was registered
160 // with a matching (poll_fn, user_data) pair, and never
161 // re-enters a task while it's being polled.
162 let result = unsafe { (reg.poll_fn)(reg.user_data, &task_waker) };
163 clear_current();
164
165 if result == Poll::Ready(()) {
166 self.live_tasks
167 .fetch_sub(1, crate::sync::atomic::Ordering::Relaxed);
168 }
169 }
170
171 // All tasks pending — sleep until an interrupt wakes us.
172 if !waker::has_pending() {
173 crate::port::arch::idle();
174 }
175 }
176 }
177
178 fn lookup_task(&self, priority: u8, index: u8) -> Option<&'static TaskReg> {
179 let p = priority as usize;
180 let i = index as usize;
181 if p > (crate::task::MAX_PRIORITY as usize) || i >= crate::task::MAX_TASKS {
182 return None;
183 }
184 // SAFETY: the pointer came from the linker-section walk in `init()`
185 // and points at a `static TaskReg` that lives for the program's
186 // lifetime; the registry is written once at boot and only read
187 // afterwards.
188 self.registry.tasks[p][i].map(|ptr| unsafe { &*ptr })
189 }
190}
191
192/// Test-only: reset the simulated-task-context global. Part of the global
193/// reset done by [`crate::kernel_test!`].
194#[cfg(feature = "test-support")]
195pub(crate) fn reset_for_test() {
196 clear_current();
197}
198
199impl Default for Executor {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205/// Global executor singleton.
206pub static mut EXECUTOR: Executor = Executor::new();