Skip to main content

moirai_executor/schedule/queue/
mod.rs

1//! Priority-aware worker queues.
2
3use std::sync::{
4    atomic::{AtomicUsize, Ordering},
5    Arc,
6};
7
8use moirai_core::Priority;
9use moirai_scheduler::{ChaseLevDeque, ChaseLevStealer, StealResult};
10use moirai_utils::CacheAligned;
11
12use super::job::ScheduledJob;
13
14/// One queue per priority level; indices come from [`Priority::index`] (SSOT).
15const PRIORITY_LEVELS: usize = Priority::Critical.index() + 1;
16/// Initial slot count for each worker's multi-producer injector queue.
17///
18/// The injector holds `(Priority, ScheduledJob)` elements (~256 B each); the
19/// previous `LockFreeQueue::new()` default of 65536 slots pre-allocated ~16 MiB
20/// per worker at startup regardless of load (~256 MiB idle on 16 workers). A
21/// 1024-slot injector bounds that to ~256 KiB per worker while the enqueue path
22/// still backs off and eventually succeeds when momentarily full (unblocked-
23/// sender contract), so throughput under burst load is unaffected.
24///
25/// This is a subtractive sizing fix; threading `ExecutorConfig::
26/// max_global_queue_size` to this construction site is a separate follow-up.
27pub(super) const INJECTOR_CAPACITY: usize = 1024;
28/// Pop scan order: highest [`Priority::index`] first.
29const PRIORITY_POP_ORDER: [usize; PRIORITY_LEVELS] = [
30    Priority::Critical.index(),
31    Priority::High.index(),
32    Priority::Normal.index(),
33    Priority::Low.index(),
34];
35
36/// Per-worker task queues partitioned by priority using lock-free Chase-Lev deques.
37///
38/// Local pop operations and push operations are lock-free. Local operations (from the owner thread)
39/// proceed directly on the private SPSC deques, while non-local schedules place tasks into the
40/// lock-free multi-producer injector queue. Steal operations are entirely lock-free and proceed
41/// without acquiring any locks.
42///
43/// Queue contents are synchronized by `state` (note: required contract comment).
44/// Worker queues are also used to coordinate scheduler quiescence.
45pub(crate) struct WorkerQueues<const CAPACITY: usize> {
46    local_stealers: [ChaseLevStealer<ScheduledJob>; PRIORITY_LEVELS],
47    injector: moirai_utils::queue::LockFreeQueue<(Priority, ScheduledJob)>,
48    /// Advisory fast-path count used to skip checking when the queues are visibly
49    /// empty. The owner writes it on every push/pop and thieves write it on every
50    /// `steal_batch`, so it is cache-line isolated to keep those cross-thread RMWs
51    /// from false-sharing with the multi-producer `injector` metadata above.
52    len: CacheAligned<AtomicUsize>,
53}
54
55/// Unique bottom-side queue capabilities owned by one worker thread.
56pub(crate) struct WorkerQueueOwner<const CAPACITY: usize> {
57    local_queues: [ChaseLevDeque<ScheduledJob>; PRIORITY_LEVELS],
58    shared: Arc<WorkerQueues<CAPACITY>>,
59}
60
61impl<const CAPACITY: usize> WorkerQueues<CAPACITY> {
62    /// Create empty queues for one worker.
63    pub(crate) fn new() -> (WorkerQueueOwner<CAPACITY>, Arc<Self>) {
64        let local_queues = std::array::from_fn(|_| ChaseLevDeque::new(CAPACITY));
65        let local_stealers = std::array::from_fn(|index| local_queues[index].stealer());
66        let shared = Arc::new(Self {
67            local_stealers,
68            injector: moirai_utils::queue::LockFreeQueue::with_capacity(INJECTOR_CAPACITY),
69            len: CacheAligned::new(AtomicUsize::new(0)),
70        });
71        (
72            WorkerQueueOwner {
73                local_queues,
74                shared: Arc::clone(&shared),
75            },
76            shared,
77        )
78    }
79
80    /// Push a job from an external thread (non-local push).
81    pub(crate) fn try_push_external(
82        &self,
83        priority: Priority,
84        job: ScheduledJob,
85    ) -> Option<ScheduledJob> {
86        match self.injector.try_enqueue((priority, job)) {
87            Ok(()) => {
88                self.len.fetch_add(1, Ordering::Relaxed);
89                None
90            }
91            Err((_priority, job)) => Some(job),
92        }
93    }
94
95    /// Steal one job without acquiring a bottom-side owner capability.
96    pub(crate) fn steal_one(&self) -> Option<ScheduledJob> {
97        if self.len.load(Ordering::Relaxed) == 0 {
98            return None;
99        }
100        for &index in &PRIORITY_POP_ORDER {
101            loop {
102                match self.local_stealers[index].steal() {
103                    StealResult::Success(job) => {
104                        self.len.fetch_sub(1, Ordering::Relaxed);
105                        return Some(job);
106                    }
107                    StealResult::Retry => continue,
108                    StealResult::Empty => break,
109                }
110            }
111        }
112        if let Some((_priority, job)) = self.injector.try_dequeue() {
113            self.len.fetch_sub(1, Ordering::Relaxed);
114            return Some(job);
115        }
116        None
117    }
118
119    /// Returns true when the queue has no visible jobs.
120    pub(crate) fn is_empty(&self) -> bool {
121        self.len.load(Ordering::Relaxed) == 0
122    }
123
124    /// Approximate queued job count.
125    #[cfg(test)]
126    pub(crate) fn len(&self) -> usize {
127        self.len.load(Ordering::Relaxed)
128    }
129
130    /// Initial slot count of the multi-producer injector queue.
131    #[cfg(test)]
132    pub(crate) fn injector_capacity(&self) -> usize {
133        self.injector.capacity()
134    }
135}
136
137impl<const CAPACITY: usize> WorkerQueueOwner<CAPACITY> {
138    pub(crate) fn pop_local(&mut self) -> Option<ScheduledJob> {
139        if self.shared.len.load(Ordering::Relaxed) == 0 {
140            return None;
141        }
142        for &index in &PRIORITY_POP_ORDER {
143            if let Some(job) = self.local_queues[index].pop() {
144                self.shared.len.fetch_sub(1, Ordering::Relaxed);
145                return Some(job);
146            }
147        }
148        while let Some((priority, job)) = self.shared.injector.try_dequeue() {
149            self.local_queues[priority.index()].push(job);
150        }
151        for &index in &PRIORITY_POP_ORDER {
152            if let Some(job) = self.local_queues[index].pop() {
153                self.shared.len.fetch_sub(1, Ordering::Relaxed);
154                return Some(job);
155            }
156        }
157        None
158    }
159
160    /// Steal multiple jobs from another worker, retaining all but one locally.
161    pub(crate) fn steal_batch(&mut self, target: &WorkerQueues<CAPACITY>) -> Option<ScheduledJob> {
162        if target.len.load(Ordering::Relaxed) == 0 {
163            return None;
164        }
165
166        // 1. Try to steal from target's local queues
167        for &index in &PRIORITY_POP_ORDER {
168            loop {
169                match target.local_stealers[index].steal_batch() {
170                    StealResult::Success(mut batch) => {
171                        let first_job = batch
172                            .next()
173                            .expect("invariant: successful batch contains one job");
174                        let mut pushed_count = 0;
175                        for job in batch {
176                            self.local_queues[index].push(job);
177                            pushed_count += 1;
178                        }
179                        if pushed_count > 0 {
180                            self.shared.len.fetch_add(pushed_count, Ordering::Relaxed);
181                        }
182                        target.len.fetch_sub(pushed_count + 1, Ordering::Relaxed);
183                        return Some(first_job);
184                    }
185                    StealResult::Retry => continue,
186                    StealResult::Empty => break,
187                }
188            }
189        }
190
191        // 2. Try to steal from target's injector
192        if let Some((_priority, first_job)) = target.injector.try_dequeue() {
193            let mut pushed_count = 0;
194            // Dequeue a batch (up to 15 more tasks to form a batch of 16)
195            while pushed_count < 15 {
196                if let Some((p, job)) = target.injector.try_dequeue() {
197                    self.local_queues[p.index()].push(job);
198                    pushed_count += 1;
199                } else {
200                    break;
201                }
202            }
203            if pushed_count > 0 {
204                self.shared.len.fetch_add(pushed_count, Ordering::Relaxed);
205            }
206            target.len.fetch_sub(pushed_count + 1, Ordering::Relaxed);
207            return Some(first_job);
208        }
209
210        None
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use std::sync::{Arc, Mutex};
217
218    use super::WorkerQueues;
219    use crate::schedule::job::ScheduledJob;
220    use moirai_core::Priority;
221
222    #[test]
223    fn worker_queue_pops_highest_priority_first() {
224        let observed = Arc::new(Mutex::new(Vec::new()));
225        let (mut owner, queues) = WorkerQueues::<256>::new();
226
227        for (priority, value) in [(Priority::Low, 1), (Priority::Critical, 2)] {
228            let observed = Arc::clone(&observed);
229            let () = queues
230                .try_push_external(
231                    priority,
232                    ScheduledJob::new(move |_| {
233                        observed.lock().unwrap().push(value);
234                    }),
235                )
236                .map_or((), |_| panic!("test queue has capacity"));
237        }
238
239        owner.pop_local().unwrap().execute(0);
240        owner.pop_local().unwrap().execute(0);
241
242        assert_eq!(*observed.lock().unwrap(), vec![2, 1]);
243        assert_eq!(queues.len(), 0);
244    }
245
246    #[test]
247    fn injector_uses_sane_default_capacity_not_65536() {
248        // The injector must not pre-allocate the LockFreeQueue 65536-slot
249        // default (~16 MiB/worker for (Priority, ScheduledJob)); it is sized to
250        // INJECTOR_CAPACITY instead.
251        let (_owner, queues) = WorkerQueues::<256>::new();
252        assert_eq!(queues.injector_capacity(), super::INJECTOR_CAPACITY);
253        assert_eq!(queues.injector_capacity(), 1024);
254        assert_ne!(queues.injector_capacity(), 65536);
255    }
256
257    #[test]
258    fn injector_round_trips_through_external_push() {
259        // The reduced-capacity injector still enqueues and drains: an external
260        // push lands in the injector and pops out via pop_local's drain path.
261        let observed = Arc::new(Mutex::new(Vec::new()));
262        let (mut owner, queues) = WorkerQueues::<256>::new();
263
264        for (priority, value) in [(Priority::Normal, 7), (Priority::Critical, 9)] {
265            let observed = Arc::clone(&observed);
266            let () = queues
267                .try_push_external(
268                    priority,
269                    ScheduledJob::new(move |_| {
270                        observed.lock().unwrap().push(value);
271                    }),
272                )
273                .map_or((), |_| panic!("test queue has capacity"));
274        }
275
276        // Critical drains ahead of Normal once moved into the local queues.
277        owner.pop_local().unwrap().execute(0);
278        owner.pop_local().unwrap().execute(0);
279
280        assert_eq!(*observed.lock().unwrap(), vec![9, 7]);
281        assert_eq!(queues.len(), 0);
282    }
283
284    #[test]
285    fn full_injector_returns_and_drops_rejected_job_once() {
286        let (_owner, queues) = WorkerQueues::<256>::new();
287        for _ in 0..super::INJECTOR_CAPACITY {
288            let () = queues
289                .try_push_external(Priority::Normal, ScheduledJob::new(|_| {}))
290                .map_or((), |_| panic!("capacity-sized admission must succeed"));
291        }
292
293        let capture = Arc::new(());
294        let rejected_capture = Arc::clone(&capture);
295        let rejected = queues
296            .try_push_external(
297                Priority::Normal,
298                ScheduledJob::new(move |_| drop(rejected_capture)),
299            )
300            .expect("one job beyond capacity must be rejected");
301
302        assert_eq!(queues.len(), super::INJECTOR_CAPACITY);
303        assert_eq!(Arc::strong_count(&capture), 2);
304        drop(rejected);
305        assert_eq!(Arc::strong_count(&capture), 1);
306    }
307}