Skip to main content

scirs2_core/concurrent/
work_stealing.rs

1//! Chase-Lev work-stealing double-ended deque and scheduler.
2//!
3//! # Overview
4//!
5//! This module provides:
6//!
7//! - [`WorkStealingDeque`] — a single-producer, multi-consumer lock-free deque
8//!   following the classic Chase-Lev design.  The *owner* pushes and pops from
9//!   the **bottom**; *thieves* steal from the **top**.
10//! - [`WorkStealingScheduler`] — a thread-pool built on top of per-worker
11//!   [`WorkStealingDeque`]s that automatically balances load through stealing.
12//! - [`PriorityTaskQueue`] — a multi-priority bounded task queue that separates
13//!   high / normal / low work and serves them in order.
14//!
15//! # Safety note
16//!
17//! The Chase-Lev deque uses `unsafe` pointer arithmetic to achieve lock-free
18//! semantics; all unsafety is contained inside [`WorkStealingDeque`] and is
19//! guarded by the invariants described in the individual `unsafe` blocks.
20
21use std::cell::UnsafeCell;
22use std::sync::atomic::{fence, AtomicIsize, AtomicUsize, Ordering};
23use std::sync::{Arc, Condvar, Mutex};
24use std::thread;
25
26use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
27
28// ── constants ────────────────────────────────────────────────────────────────
29
30/// Initial capacity of the circular buffer (must be a power of two).
31const INITIAL_CAPACITY: usize = 64;
32
33// ── circular buffer ──────────────────────────────────────────────────────────
34
35/// A heap-allocated circular buffer of capacity `cap` (always a power of two).
36struct CircularBuf<T> {
37    cap: usize,
38    data: Box<[UnsafeCell<Option<T>>]>,
39}
40
41impl<T> CircularBuf<T> {
42    fn new(cap: usize) -> Self {
43        let data = (0..cap)
44            .map(|_| UnsafeCell::new(None))
45            .collect::<Vec<_>>()
46            .into_boxed_slice();
47        Self { cap, data }
48    }
49
50    fn mask(&self) -> usize {
51        self.cap - 1
52    }
53
54    /// Write `val` at logical index `i`.
55    ///
56    /// # Safety
57    /// The caller must ensure no concurrent reader accesses the same slot.
58    unsafe fn write(&self, i: usize, val: T) {
59        let slot = self.data[i & self.mask()].get();
60        // SAFETY: we have exclusive write access because the caller checked
61        // bottom/top atomics before calling.
62        unsafe { (*slot) = Some(val) };
63    }
64
65    /// Read the value at logical index `i`, replacing the slot with `None`.
66    ///
67    /// # Safety
68    /// The caller must ensure it has exclusive access to this slot via
69    /// the CAS on `top` (for stealers) or the pop logic (for the owner).
70    unsafe fn read(&self, i: usize) -> Option<T> {
71        let slot = self.data[i & self.mask()].get();
72        // SAFETY: guaranteed exclusive by the caller's atomic protocol.
73        unsafe { (*slot).take() }
74    }
75}
76
77// SAFETY: We uphold the single-owner / many-stealer invariant through the
78// atomic bottom/top protocol, making concurrent accesses disjoint.
79unsafe impl<T: Send> Send for CircularBuf<T> {}
80unsafe impl<T: Send> Sync for CircularBuf<T> {}
81
82// ── WorkStealingDeque ────────────────────────────────────────────────────────
83
84/// A Chase-Lev lock-free work-stealing deque.
85///
86/// The **owner** thread calls [`push`](WorkStealingDeque::push) and
87/// [`pop`](WorkStealingDeque::pop).  Any number of **stealer** threads call
88/// [`steal`](WorkStealingDeque::steal) concurrently.
89///
90/// The internal buffer grows automatically (doubling) when the owner fills it.
91/// Shrinking is *not* implemented to keep the implementation simple.
92pub struct WorkStealingDeque<T: Send + 'static> {
93    bottom: AtomicIsize,
94    top: AtomicIsize,
95    buf: Mutex<Arc<CircularBuf<T>>>,
96}
97
98/// Outcome of a [`WorkStealingDeque::steal`] attempt.
99#[derive(Debug)]
100pub enum StealResult<T> {
101    /// A task was successfully stolen.
102    Success(T),
103    /// The deque is empty — no task available.
104    Empty,
105    /// A concurrent stealer raced us — try again later.
106    Retry,
107}
108
109impl<T: Send + 'static> WorkStealingDeque<T> {
110    /// Create a new empty deque.
111    pub fn new() -> Self {
112        Self {
113            bottom: AtomicIsize::new(0),
114            top: AtomicIsize::new(0),
115            buf: Mutex::new(Arc::new(CircularBuf::new(INITIAL_CAPACITY))),
116        }
117    }
118
119    /// Number of elements currently in the deque (approximate).
120    pub fn len(&self) -> usize {
121        let b = self.bottom.load(Ordering::Relaxed);
122        let t = self.top.load(Ordering::Relaxed);
123        (b - t).max(0) as usize
124    }
125
126    /// Returns `true` if the deque contains no elements.
127    pub fn is_empty(&self) -> bool {
128        self.len() == 0
129    }
130
131    /// Push a task onto the bottom (owner side).
132    ///
133    /// May grow the internal buffer if needed.
134    pub fn push(&self, task: T) -> CoreResult<()> {
135        let b = self.bottom.load(Ordering::Relaxed);
136        let t = self.top.load(Ordering::Acquire);
137        let size = (b - t) as usize;
138
139        let buf: Arc<CircularBuf<T>> = {
140            let guard = self.buf.lock().map_err(|e| {
141                CoreError::SchedulerError(
142                    ErrorContext::new(format!("deque buf lock poisoned: {e}"))
143                        .with_location(ErrorLocation::new(file!(), line!())),
144                )
145            })?;
146            Arc::clone(&*guard)
147        };
148
149        // Grow if needed
150        let buf: Arc<CircularBuf<T>> = if size >= buf.cap - 1 {
151            let new_cap = buf.cap * 2;
152            let new_buf = Arc::new(CircularBuf::new(new_cap));
153            // Copy existing elements
154            for i in t..b {
155                // SAFETY: single owner, exclusive write slot on new_buf.
156                unsafe {
157                    let val = buf.read(i as usize);
158                    if let Some(v) = val {
159                        new_buf.write(i as usize, v);
160                    }
161                }
162            }
163            let mut guard = self.buf.lock().map_err(|e| {
164                CoreError::SchedulerError(
165                    ErrorContext::new(format!("deque buf lock poisoned during grow: {e}"))
166                        .with_location(ErrorLocation::new(file!(), line!())),
167                )
168            })?;
169            *guard = Arc::clone(&new_buf);
170            new_buf
171        } else {
172            buf
173        };
174
175        // SAFETY: `b` is the owner-exclusive write index; no stealer touches it.
176        unsafe { buf.write(b as usize, task) };
177        fence(Ordering::Release);
178        self.bottom.store(b + 1, Ordering::Relaxed);
179        Ok(())
180    }
181
182    /// Pop a task from the bottom (owner side).
183    ///
184    /// Returns `None` if the deque is empty.
185    pub fn pop(&self) -> CoreResult<Option<T>> {
186        let b = self.bottom.load(Ordering::Relaxed) - 1;
187        let buf: Arc<CircularBuf<T>> = {
188            let guard = self.buf.lock().map_err(|e| {
189                CoreError::SchedulerError(
190                    ErrorContext::new(format!("deque buf lock poisoned on pop: {e}"))
191                        .with_location(ErrorLocation::new(file!(), line!())),
192                )
193            })?;
194            Arc::clone(&*guard)
195        };
196        self.bottom.store(b, Ordering::Relaxed);
197        fence(Ordering::SeqCst);
198        let t = self.top.load(Ordering::Relaxed);
199
200        if t > b {
201            // Deque was empty — restore bottom.
202            self.bottom.store(b + 1, Ordering::Relaxed);
203            return Ok(None);
204        }
205
206        // SAFETY: slot at `b` is exclusively owned by the owner at this point.
207        let task = unsafe { buf.read(b as usize) };
208
209        if t == b {
210            // Last element — race stealers with a CAS on top.
211            let stolen = self
212                .top
213                .compare_exchange(t, t + 1, Ordering::SeqCst, Ordering::Relaxed)
214                .is_err();
215            self.bottom.store(b + 1, Ordering::Relaxed);
216            if stolen {
217                return Ok(None);
218            }
219        }
220
221        Ok(task)
222    }
223
224    /// Steal a task from the top (stealer side).
225    pub fn steal(&self) -> StealResult<T> {
226        let t = self.top.load(Ordering::Acquire);
227        fence(Ordering::SeqCst);
228        let b = self.bottom.load(Ordering::Acquire);
229
230        if t >= b {
231            return StealResult::Empty;
232        }
233
234        let buf = match self.buf.lock() {
235            Ok(g) => Arc::clone(&*g),
236            Err(_) => return StealResult::Retry,
237        };
238
239        // SAFETY: `t` was acquired before checking `b`; the slot is valid.
240        let task = unsafe { buf.read(t as usize) };
241
242        match self
243            .top
244            .compare_exchange(t, t + 1, Ordering::SeqCst, Ordering::Relaxed)
245        {
246            Ok(_) => match task {
247                Some(v) => StealResult::Success(v),
248                None => StealResult::Retry,
249            },
250            Err(_) => StealResult::Retry,
251        }
252    }
253}
254
255impl<T: Send + 'static> Default for WorkStealingDeque<T> {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261// ── PriorityTaskQueue ────────────────────────────────────────────────────────
262
263/// Priority level for tasks in a [`PriorityTaskQueue`].
264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
265pub enum Priority {
266    /// Must run before `Normal` and `Low` tasks.
267    High = 2,
268    /// Default priority.
269    Normal = 1,
270    /// Background tasks — run only when no higher-priority work exists.
271    Low = 0,
272}
273
274type BoxTask = Box<dyn FnOnce() + Send + 'static>;
275
276struct PriorityItem {
277    priority: Priority,
278    seq: u64, // tie-break: lower seq = earlier submitted
279    task: BoxTask,
280}
281
282impl PartialEq for PriorityItem {
283    fn eq(&self, other: &Self) -> bool {
284        self.priority == other.priority && self.seq == other.seq
285    }
286}
287impl Eq for PriorityItem {}
288
289impl PartialOrd for PriorityItem {
290    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
291        Some(self.cmp(other))
292    }
293}
294
295impl Ord for PriorityItem {
296    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
297        // Higher priority first; equal priority → lower seq first (FIFO).
298        // BinaryHeap is a max-heap, so larger items are popped first.
299        // We want High > Normal > Low, so use self.priority > other.priority.
300        self.priority
301            .cmp(&other.priority)
302            .then_with(|| other.seq.cmp(&self.seq))
303    }
304}
305
306/// A bounded multi-priority task queue.
307///
308/// Tasks with [`Priority::High`] are dequeued before [`Priority::Normal`],
309/// which are dequeued before [`Priority::Low`].  Within a priority level
310/// tasks are served FIFO.
311pub struct PriorityTaskQueue {
312    inner: Mutex<PriorityQueueInner>,
313    not_empty: Condvar,
314    not_full: Condvar,
315    capacity: usize,
316    seq: AtomicUsize,
317}
318
319struct PriorityQueueInner {
320    heap: std::collections::BinaryHeap<PriorityItem>,
321    closed: bool,
322}
323
324impl PriorityTaskQueue {
325    /// Create a queue that holds at most `capacity` pending tasks.
326    pub fn new(capacity: usize) -> Self {
327        let cap = capacity.max(1);
328        Self {
329            inner: Mutex::new(PriorityQueueInner {
330                heap: std::collections::BinaryHeap::with_capacity(cap),
331                closed: false,
332            }),
333            not_empty: Condvar::new(),
334            not_full: Condvar::new(),
335            capacity: cap,
336            seq: AtomicUsize::new(0),
337        }
338    }
339
340    /// Submit a task at the given `priority`.
341    ///
342    /// Blocks if the queue is at capacity, or returns `Err` if the queue is
343    /// closed.
344    pub fn submit<F>(&self, priority: Priority, f: F) -> CoreResult<()>
345    where
346        F: FnOnce() + Send + 'static,
347    {
348        let seq = self.seq.fetch_add(1, Ordering::Relaxed) as u64;
349        let item = PriorityItem {
350            priority,
351            seq,
352            task: Box::new(f),
353        };
354        let mut guard = self.inner.lock().map_err(|e| {
355            CoreError::SchedulerError(
356                ErrorContext::new(format!("priority queue lock poisoned on submit: {e}"))
357                    .with_location(ErrorLocation::new(file!(), line!())),
358            )
359        })?;
360        loop {
361            if guard.closed {
362                return Err(CoreError::InvalidInput(ErrorContext::new(
363                    "PriorityTaskQueue: queue is closed",
364                )));
365            }
366            if guard.heap.len() < self.capacity {
367                break;
368            }
369            guard = self.not_full.wait(guard).map_err(|e| {
370                CoreError::SchedulerError(
371                    ErrorContext::new(format!("condvar wait poisoned: {e}"))
372                        .with_location(ErrorLocation::new(file!(), line!())),
373                )
374            })?;
375        }
376        guard.heap.push(item);
377        self.not_empty.notify_one();
378        Ok(())
379    }
380
381    /// Try to submit without blocking.  Returns `Err` if full or closed.
382    pub fn try_submit<F>(&self, priority: Priority, f: F) -> CoreResult<()>
383    where
384        F: FnOnce() + Send + 'static,
385    {
386        let seq = self.seq.fetch_add(1, Ordering::Relaxed) as u64;
387        let item = PriorityItem {
388            priority,
389            seq,
390            task: Box::new(f),
391        };
392        let mut guard = self.inner.lock().map_err(|e| {
393            CoreError::SchedulerError(
394                ErrorContext::new(format!("priority queue lock poisoned on try_submit: {e}"))
395                    .with_location(ErrorLocation::new(file!(), line!())),
396            )
397        })?;
398        if guard.closed {
399            return Err(CoreError::InvalidInput(ErrorContext::new(
400                "PriorityTaskQueue: queue is closed",
401            )));
402        }
403        if guard.heap.len() >= self.capacity {
404            return Err(CoreError::InvalidInput(ErrorContext::new(
405                "PriorityTaskQueue: queue is full",
406            )));
407        }
408        guard.heap.push(item);
409        self.not_empty.notify_one();
410        Ok(())
411    }
412
413    /// Block until a task is available, then return it.
414    ///
415    /// Returns `None` when the queue is closed and drained.
416    pub fn dequeue(&self) -> CoreResult<Option<BoxTask>> {
417        let mut guard = self.inner.lock().map_err(|e| {
418            CoreError::SchedulerError(
419                ErrorContext::new(format!("priority queue lock poisoned on dequeue: {e}"))
420                    .with_location(ErrorLocation::new(file!(), line!())),
421            )
422        })?;
423        loop {
424            if let Some(item) = guard.heap.pop() {
425                self.not_full.notify_one();
426                return Ok(Some(item.task));
427            }
428            if guard.closed {
429                return Ok(None);
430            }
431            guard = self.not_empty.wait(guard).map_err(|e| {
432                CoreError::SchedulerError(
433                    ErrorContext::new(format!("condvar wait poisoned on dequeue: {e}"))
434                        .with_location(ErrorLocation::new(file!(), line!())),
435                )
436            })?;
437        }
438    }
439
440    /// Try to dequeue without blocking.  Returns `None` if empty.
441    pub fn try_dequeue(&self) -> CoreResult<Option<BoxTask>> {
442        let mut guard = self.inner.lock().map_err(|e| {
443            CoreError::SchedulerError(
444                ErrorContext::new(format!("priority queue lock poisoned on try_dequeue: {e}"))
445                    .with_location(ErrorLocation::new(file!(), line!())),
446            )
447        })?;
448        match guard.heap.pop() {
449            Some(item) => {
450                self.not_full.notify_one();
451                Ok(Some(item.task))
452            }
453            None => Ok(None),
454        }
455    }
456
457    /// Close the queue.  Pending tasks may still be dequeued; new submits fail.
458    pub fn close(&self) {
459        if let Ok(mut g) = self.inner.lock() {
460            g.closed = true;
461        }
462        self.not_empty.notify_all();
463        self.not_full.notify_all();
464    }
465
466    /// Number of pending tasks.
467    pub fn pending(&self) -> usize {
468        self.inner.lock().map(|g| g.heap.len()).unwrap_or(0)
469    }
470}
471
472// ── WorkStealingScheduler ────────────────────────────────────────────────────
473
474/// Configuration for [`WorkStealingScheduler`].
475#[derive(Debug, Clone)]
476pub struct SchedulerConfig {
477    /// Number of worker threads (0 = use hardware concurrency).
478    pub num_workers: usize,
479    /// How many steal attempts before a worker sleeps.
480    pub steal_attempts: usize,
481    /// Duration to sleep when idle (microseconds).
482    pub idle_sleep_us: u64,
483}
484
485impl Default for SchedulerConfig {
486    fn default() -> Self {
487        Self {
488            num_workers: 0,
489            steal_attempts: 32,
490            idle_sleep_us: 100,
491        }
492    }
493}
494
495/// Statistics collected by the scheduler.
496#[derive(Debug, Default, Clone)]
497pub struct SchedulerStats {
498    /// Total tasks completed.
499    pub tasks_completed: u64,
500    /// Total successful steals.
501    pub steal_successes: u64,
502    /// Total failed steal attempts.
503    pub steal_failures: u64,
504}
505
506type StatsCell = Arc<Mutex<SchedulerStats>>;
507
508/// A work-stealing thread-pool scheduler.
509///
510/// Each worker has its own [`WorkStealingDeque`].  When idle, workers attempt
511/// to steal from neighbours in round-robin order before sleeping.
512pub struct WorkStealingScheduler {
513    deques: Arc<Vec<Arc<WorkStealingDeque<BoxTask>>>>,
514    handles: Vec<thread::JoinHandle<()>>,
515    stop: Arc<std::sync::atomic::AtomicBool>,
516    stats: StatsCell,
517    next_push: AtomicUsize,
518}
519
520impl WorkStealingScheduler {
521    /// Create a scheduler with the given configuration.
522    pub fn new(cfg: SchedulerConfig) -> CoreResult<Self> {
523        let n = if cfg.num_workers == 0 {
524            thread::available_parallelism()
525                .map(|p| p.get())
526                .unwrap_or(4)
527        } else {
528            cfg.num_workers
529        };
530        if n == 0 {
531            return Err(CoreError::InvalidInput(ErrorContext::new(
532                "WorkStealingScheduler: num_workers must be >= 1",
533            )));
534        }
535
536        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
537        let stats: StatsCell = Arc::new(Mutex::new(SchedulerStats::default()));
538        let deques: Arc<Vec<Arc<WorkStealingDeque<BoxTask>>>> =
539            Arc::new((0..n).map(|_| Arc::new(WorkStealingDeque::new())).collect());
540
541        let mut handles = Vec::with_capacity(n);
542        for id in 0..n {
543            let deques2 = Arc::clone(&deques);
544            let stop2 = Arc::clone(&stop);
545            let stats2 = Arc::clone(&stats);
546            let steal_attempts = cfg.steal_attempts;
547            let idle_sleep_us = cfg.idle_sleep_us;
548
549            let handle = thread::Builder::new()
550                .name(format!("ws-worker-{id}"))
551                .spawn(move || {
552                    worker_loop(id, n, deques2, stop2, stats2, steal_attempts, idle_sleep_us);
553                })
554                .map_err(|e| {
555                    CoreError::SchedulerError(
556                        ErrorContext::new(format!("failed to spawn worker {id}: {e}"))
557                            .with_location(ErrorLocation::new(file!(), line!())),
558                    )
559                })?;
560            handles.push(handle);
561        }
562
563        Ok(Self {
564            deques,
565            handles,
566            stop,
567            stats,
568            next_push: AtomicUsize::new(0),
569        })
570    }
571
572    /// Submit a closure for execution.
573    pub fn submit<F>(&self, f: F) -> CoreResult<()>
574    where
575        F: FnOnce() + Send + 'static,
576    {
577        let idx = self.next_push.fetch_add(1, Ordering::Relaxed) % self.deques.len();
578        self.deques[idx].push(Box::new(f))
579    }
580
581    /// Number of worker threads.
582    pub fn num_workers(&self) -> usize {
583        self.deques.len()
584    }
585
586    /// Snapshot of accumulated statistics.
587    pub fn stats(&self) -> SchedulerStats {
588        self.stats.lock().map(|g| g.clone()).unwrap_or_default()
589    }
590
591    /// Shut down all worker threads and wait for them to finish.
592    pub fn shutdown(self) -> CoreResult<()> {
593        self.stop.store(true, Ordering::SeqCst);
594        for h in self.handles {
595            h.join().map_err(|_| {
596                CoreError::SchedulerError(
597                    ErrorContext::new("worker thread panicked during shutdown")
598                        .with_location(ErrorLocation::new(file!(), line!())),
599                )
600            })?;
601        }
602        Ok(())
603    }
604}
605
606/// Worker event loop.
607fn worker_loop(
608    id: usize,
609    n: usize,
610    deques: Arc<Vec<Arc<WorkStealingDeque<BoxTask>>>>,
611    stop: Arc<std::sync::atomic::AtomicBool>,
612    stats: StatsCell,
613    steal_attempts: usize,
614    idle_sleep_us: u64,
615) {
616    let mut local_completed = 0u64;
617    let mut local_steals = 0u64;
618    let mut local_failures = 0u64;
619
620    loop {
621        // 1. Try own deque first — use `steal` (not `pop`) because `push` is
622        //    called from the submitting thread, not from this worker.  In the
623        //    Chase-Lev model `push`/`pop` are owner-side, but our scheduler
624        //    pushes from the main thread and workers consume.  Using `steal`
625        //    is safe for any thread.
626        let own = match deques[id].steal() {
627            StealResult::Success(task) => {
628                task();
629                local_completed += 1;
630                true
631            }
632            _ => false,
633        };
634
635        if own {
636            continue;
637        }
638
639        // 2. Try to steal from neighbours.
640        let mut stole = false;
641        'steal: for attempt in 0..steal_attempts {
642            let victim = (id + 1 + attempt) % n;
643            if victim == id {
644                continue;
645            }
646            match deques[victim].steal() {
647                StealResult::Success(task) => {
648                    task();
649                    local_completed += 1;
650                    local_steals += 1;
651                    stole = true;
652                    break 'steal;
653                }
654                StealResult::Empty => {}
655                StealResult::Retry => {
656                    local_failures += 1;
657                }
658            }
659        }
660
661        if stole {
662            continue;
663        }
664
665        // 3. Re-check own deque (a task may have been pushed during stealing).
666        if let StealResult::Success(task) = deques[id].steal() {
667            task();
668            local_completed += 1;
669            continue;
670        }
671
672        // 4. Check stop flag.
673        if stop.load(Ordering::Relaxed) {
674            break;
675        }
676
677        // 5. Sleep briefly before retrying.
678        thread::sleep(std::time::Duration::from_micros(idle_sleep_us));
679    }
680
681    // Flush local stats.
682    if let Ok(mut g) = stats.lock() {
683        g.tasks_completed += local_completed;
684        g.steal_successes += local_steals;
685        g.steal_failures += local_failures;
686    }
687}
688
689// ── Tests ────────────────────────────────────────────────────────────────────
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use std::sync::atomic::AtomicU64;
695
696    #[test]
697    fn deque_push_pop_single_thread() {
698        let dq: WorkStealingDeque<i32> = WorkStealingDeque::new();
699        assert!(dq.is_empty());
700        dq.push(1).expect("push 1");
701        dq.push(2).expect("push 2");
702        dq.push(3).expect("push 3");
703        assert_eq!(dq.len(), 3);
704        assert_eq!(dq.pop().expect("pop"), Some(3));
705        assert_eq!(dq.pop().expect("pop"), Some(2));
706        assert_eq!(dq.pop().expect("pop"), Some(1));
707        assert_eq!(dq.pop().expect("pop"), None);
708    }
709
710    #[test]
711    fn deque_steal_basic() {
712        let dq = Arc::new(WorkStealingDeque::<i32>::new());
713        dq.push(10).expect("push");
714        dq.push(20).expect("push");
715
716        let dq2 = Arc::clone(&dq);
717        let stealer = thread::spawn(move || loop {
718            match dq2.steal() {
719                StealResult::Success(v) => return v,
720                StealResult::Empty => return -1,
721                StealResult::Retry => {}
722            }
723        });
724        let stolen = stealer.join().expect("stealer thread");
725        assert!(stolen == 10 || stolen == 20 || stolen == -1);
726    }
727
728    #[test]
729    fn deque_grows_automatically() {
730        let dq: WorkStealingDeque<usize> = WorkStealingDeque::new();
731        for i in 0..200 {
732            dq.push(i).expect("push");
733        }
734        let mut collected = Vec::new();
735        while let Ok(Some(v)) = dq.pop() {
736            collected.push(v);
737        }
738        assert_eq!(collected.len(), 200);
739    }
740
741    #[test]
742    fn priority_queue_ordering() {
743        let q = Arc::new(PriorityTaskQueue::new(16));
744        let results = Arc::new(Mutex::new(Vec::new()));
745
746        let r1 = Arc::clone(&results);
747        q.submit(Priority::Low, move || {
748            r1.lock().expect("lock").push("low");
749        })
750        .expect("submit low");
751
752        let r2 = Arc::clone(&results);
753        q.submit(Priority::High, move || {
754            r2.lock().expect("lock").push("high");
755        })
756        .expect("submit high");
757
758        let r3 = Arc::clone(&results);
759        q.submit(Priority::Normal, move || {
760            r3.lock().expect("lock").push("normal");
761        })
762        .expect("submit normal");
763
764        q.close();
765
766        // Drain in priority order
767        while let Ok(Some(task)) = q.dequeue() {
768            task();
769        }
770
771        let res = results.lock().expect("lock");
772        assert_eq!(*res, vec!["high", "normal", "low"]);
773    }
774
775    #[test]
776    fn priority_queue_fifo_within_level() {
777        let q = Arc::new(PriorityTaskQueue::new(32));
778        let results = Arc::new(Mutex::new(Vec::new()));
779
780        for i in 0..5u32 {
781            let r = Arc::clone(&results);
782            q.submit(Priority::Normal, move || {
783                r.lock().expect("lock").push(i);
784            })
785            .expect("submit");
786        }
787        q.close();
788
789        while let Ok(Some(task)) = q.dequeue() {
790            task();
791        }
792
793        let res = results.lock().expect("lock");
794        assert_eq!(*res, vec![0, 1, 2, 3, 4]);
795    }
796
797    #[test]
798    fn scheduler_runs_tasks() {
799        let cfg = SchedulerConfig {
800            num_workers: 4,
801            steal_attempts: 16,
802            idle_sleep_us: 100,
803        };
804        let sched = WorkStealingScheduler::new(cfg).expect("new scheduler");
805        let counter = Arc::new(AtomicU64::new(0));
806        let n_tasks = 100usize;
807
808        for _ in 0..n_tasks {
809            let c = Arc::clone(&counter);
810            sched
811                .submit(move || {
812                    c.fetch_add(1, Ordering::Relaxed);
813                })
814                .expect("submit");
815        }
816
817        // Wait for tasks to drain
818        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
819        while counter.load(Ordering::Relaxed) < n_tasks as u64 {
820            if std::time::Instant::now() > deadline {
821                break;
822            }
823            thread::sleep(std::time::Duration::from_millis(1));
824        }
825
826        assert_eq!(counter.load(Ordering::Relaxed), n_tasks as u64);
827        sched.shutdown().expect("shutdown");
828    }
829}