Skip to main content

fast_steal/
task.rs

1//! Cancellable, lock-free units of work.
2//!
3//! A [`Task`] stores its remaining work as a single atomic `u128` packing the
4//! `start` (high 64 bits) and `end` (low 64 bits) bounds, which lets multiple
5//! worker threads advance the same task without locking. Because the crate is
6//! `no_std`, only `alloc` is required for the reference-counted state.
7
8extern crate alloc;
9use alloc::sync::{Arc, Weak};
10use core::{fmt, ops::Range, sync::atomic::Ordering};
11use portable_atomic::AtomicU128;
12
13/// A cancellable, concurrent-safe unit of work that tracks a `start..end` range.
14///
15/// `Task` is a reference-counted *handle*: each worker holds a strong `Task`
16/// (an `Arc<TaskInner>`), giving every worker a **distinct identity** even when
17/// two workers speculatively share the same progress cursor. This identity is
18/// what `set_threads`'s liveness sweep keys off (via `WeakTask`), so a dead
19/// worker is reclaimed regardless of how many twins still reference its cursor.
20///
21/// The range is stored as a single atomic `u128` inside `TaskInner`, allowing
22/// lock-free reads and fine-grained progress updates. Multiple workers can
23/// safely steal sub-ranges from the same task via [`split_two`](Task::split_two).
24///
25/// Two `Task`s are equal iff they point to the same underlying state (see the
26/// `PartialEq` impl, which uses `Arc::ptr_eq`).
27#[derive(Debug, Clone)]
28pub struct Task(Arc<TaskInner>);
29
30/// The identity-bearing inner of `Task`.
31///
32/// Kept separate from `state` so the liveness refcount counts *worker identity*,
33/// not the shared progress cursor. Only the `state` field is shared between
34/// speculative twins; each twin still owns its own `TaskInner` allocation.
35#[derive(Debug)]
36struct TaskInner {
37    /// Atomic state packing `start` (high 64 bits) and `end` (low 64 bits).
38    ///
39    /// Prefer the safe accessors ([`Task::start`], [`Task::end`], [`Task::get`],
40    /// [`Task::safe_add_start`]); this field is exposed for advanced use.
41    state: Arc<AtomicU128>,
42}
43
44/// A weak reference to a `Task`, obtained via [`Task::downgrade`].
45///
46/// Does not prevent the task from being deallocated. Use [`upgrade`](WeakTask::upgrade)
47/// to attempt to obtain a strong `Task` reference.
48///
49/// A `WeakTask` points at a worker's *identity* (`TaskInner`), not at the shared
50/// progress cursor — so [`strong_count`](WeakTask::strong_count) and
51/// [`is_alive`](WeakTask::is_alive) report whether that worker is still around.
52#[derive(Debug, Clone)]
53pub struct WeakTask(Weak<TaskInner>);
54
55impl WeakTask {
56    /// Attempts to upgrade to a strong [`Task`].
57    ///
58    /// Returns `None` if all strong references to the underlying task identity
59    /// have already been dropped (i.e. the worker that owned it has exited).
60    #[must_use]
61    pub fn upgrade(&self) -> Option<Task> {
62        self.0.upgrade().map(Task)
63    }
64    /// Returns the number of strong [`Task`] references to the underlying task
65    /// identity. Used by [`is_alive`](WeakTask::is_alive), which the liveness
66    /// sweep in `set_threads` relies on.
67    #[must_use]
68    pub fn strong_count(&self) -> usize {
69        self.0.strong_count()
70    }
71    /// Returns the number of weak [`WeakTask`] references to the underlying task
72    /// identity.
73    #[must_use]
74    pub fn weak_count(&self) -> usize {
75        self.0.weak_count()
76    }
77    /// Returns `true` if at least one strong [`Task`] reference to this worker's
78    /// identity still exists. This is the exact "is this worker alive?" test the
79    /// liveness sweep relies on — it is independent of how many twins share the
80    /// worker's progress cursor.
81    #[must_use]
82    pub fn is_alive(&self) -> bool {
83        self.0.strong_count() > 0
84    }
85}
86
87/// Error returned when a task range invariant is violated (`start > end`), or
88/// when [`safe_add_start`](Task::safe_add_start) cannot make forward progress.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct RangeError;
91
92impl fmt::Display for RangeError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "Range invariant violated: start > end")
95    }
96}
97
98impl core::error::Error for RangeError {}
99
100impl Task {
101    #[allow(clippy::inline_always)]
102    #[inline(always)]
103    const fn pack(range: Range<u64>) -> u128 {
104        ((range.start as u128) << 64) | (range.end as u128)
105    }
106    #[allow(clippy::inline_always)]
107    #[inline(always)]
108    const fn unpack(state: u128) -> Range<u64> {
109        #[allow(clippy::cast_possible_truncation)]
110        let end = state as u64;
111        (state >> 64) as u64..end
112    }
113
114    /// # Panics
115    /// Panics when `range.start > range.end`
116    #[must_use]
117    pub fn new(range: Range<u64>) -> Self {
118        assert!(range.start <= range.end);
119        Self(Arc::new(TaskInner {
120            state: Arc::new(AtomicU128::new(Self::pack(range))),
121        }))
122    }
123    /// Returns the current `start..end` range, loaded atomically with `Acquire`
124    /// ordering.
125    #[must_use]
126    pub fn get(&self) -> Range<u64> {
127        let state = self.0.state.load(Ordering::Acquire);
128        Self::unpack(state)
129    }
130    /// Returns the current start of the work range (the high 64 bits of the atomic
131    /// state).
132    ///
133    /// Loads independently of [`end`](Task::end): combining the two
134    /// (`task.end() - task.start()`) can observe a torn snapshot under
135    /// concurrency. Use [`get`](Task::get) or [`remain`](Task::remain) when a
136    /// consistent view of both bounds is required.
137    #[must_use]
138    pub fn start(&self) -> u64 {
139        (self.0.state.load(Ordering::Acquire) >> 64) as u64
140    }
141    /// Atomically advances `start` to `min(start + bias, end)`, but only if that
142    /// makes forward progress; then returns the slice that was claimed.
143    ///
144    /// `start` must be a cursor value the caller actually observed via
145    /// [`start`](Task::start) or [`get`](Task::get); a `start` that runs *ahead*
146    /// of the task's real cursor is rejected, because honouring it would skip
147    /// work no worker ever executed. A *stale* `start` is still accepted, but the
148    /// returned span then begins at the real cursor and is shorter than `bias` —
149    /// always consume the returned span, never assume `start..start + bias`.
150    ///
151    /// # Errors
152    /// Returns [`RangeError`] when `start + bias` would not exceed the current
153    /// `start` (no progress, a non-positive bias), or when
154    /// `start` runs ahead of the task's cursor.
155    pub fn safe_add_start(&self, start: u64, bias: u64) -> Result<Range<u64>, RangeError> {
156        let new_start = start.saturating_add(bias);
157        let mut old_state = self.0.state.load(Ordering::Acquire);
158        loop {
159            let mut range = Self::unpack(old_state);
160            if start > range.start {
161                // The caller's cursor runs ahead of reality: accepting it would
162                // jump over `range.start..start`, silently discarding work.
163                break Err(RangeError);
164            }
165            let new_start = new_start.min(range.end);
166            if new_start <= range.start {
167                break Err(RangeError);
168            }
169            let span = range.start..new_start;
170            range.start = new_start;
171            let new_state = Self::pack(range);
172            match self.0.state.compare_exchange_weak(
173                old_state,
174                new_state,
175                Ordering::AcqRel,
176                Ordering::Acquire,
177            ) {
178                Ok(_) => break Ok(span),
179                Err(x) => old_state = x,
180            }
181        }
182    }
183    /// Returns the current end of the work range (the low 64 bits of the atomic
184    /// state).
185    ///
186    /// Loads independently of [`start`](Task::start); see that method for the
187    /// torn-snapshot caveat when combining the two.
188    #[must_use]
189    pub fn end(&self) -> u64 {
190        let state = self.0.state.load(Ordering::Acquire);
191        #[allow(clippy::cast_possible_truncation)]
192        let end = state as u64;
193        end
194    }
195    /// Returns `end - start` (saturating), i.e. how much work is left.
196    #[must_use]
197    pub fn remain(&self) -> u64 {
198        let range = self.get();
199        range.end.saturating_sub(range.start)
200    }
201    /// Splits the work range in half, handing `mid..end` back to the caller as a
202    /// new task and keeping `start..mid` in `self`.
203    ///
204    /// The split only proceeds when both halves are at least `min_chunk_size`,
205    /// i.e. `remain >= min_chunk_size * 2`. That test runs *inside* the
206    /// compare-and-swap loop against the same atomic snapshot the commit
207    /// observes, so a concurrent cursor-sharer (`share_state`) advancing `start`
208    /// between the call and the commit cannot leak a half smaller than
209    /// `min_chunk_size`. When the range is too small to split under
210    /// `min_chunk_size`, returns `Ok(None)` without modifying `self`.
211    ///
212    /// # Errors
213    /// 1. Returns [`RangeError`] when `start > end`
214    /// 2. Returns `None` when `remain < min_chunk_size * 2` without modifying itself
215    pub fn split_two(&self, min_chunk_size: u64) -> Result<Option<Range<u64>>, RangeError> {
216        let mut old_state = self.0.state.load(Ordering::Acquire);
217        loop {
218            let range = Self::unpack(old_state);
219            if range.start > range.end {
220                return Err(RangeError);
221            }
222            if range.end - range.start < min_chunk_size.saturating_mul(2) {
223                return Ok(None);
224            }
225            let mid = range.start.midpoint(range.end);
226            let new_state = Self::pack(range.start..mid);
227            match self.0.state.compare_exchange_weak(
228                old_state,
229                new_state,
230                Ordering::AcqRel,
231                Ordering::Acquire,
232            ) {
233                Ok(_) => return Ok(Some(mid..range.end)),
234                Err(x) => old_state = x,
235            }
236        }
237    }
238    /// Atomically claims and returns the entire remaining range `start..end`,
239    /// emptying this task (sets `start = end`).
240    ///
241    /// Shares its error contract with [`split_two`](Task::split_two): both
242    /// consume remaining work, so both report a violated range invariant instead
243    /// of normalising it away.
244    ///
245    /// # Errors
246    /// 1. Returns [`RangeError`] when `start > end`
247    /// 2. Returns `Ok(None)` when the task is already empty, without modifying it
248    pub fn take(&self) -> Result<Option<Range<u64>>, RangeError> {
249        let mut old_state = self.0.state.load(Ordering::Acquire);
250        loop {
251            let range = Self::unpack(old_state);
252            if range.start > range.end {
253                return Err(RangeError);
254            }
255            if range.start == range.end {
256                return Ok(None);
257            }
258            let new_state = Self::pack(range.start..range.start);
259            match self.0.state.compare_exchange_weak(
260                old_state,
261                new_state,
262                Ordering::AcqRel,
263                Ordering::Acquire,
264            ) {
265                Ok(_) => return Ok(Some(range)),
266                Err(x) => old_state = x,
267            }
268        }
269    }
270    /// Creates a [`WeakTask`] that does not keep the task's state alive.
271    #[must_use]
272    pub fn downgrade(&self) -> WeakTask {
273        WeakTask(Arc::downgrade(&self.0))
274    }
275    /// Returns the number of strong [`Task`] references to this worker's *identity*
276    /// (`TaskInner`).
277    ///
278    /// `Task` and [`WeakTask`] are a paired strong/weak view of the same identity
279    /// allocation, so this equals [`WeakTask::strong_count`] on a matching
280    /// `WeakTask` — exactly like `Arc`/`Weak`. The (different) cursor-sharer count
281    /// that `steal` caps on is exposed separately as the crate-internal
282    /// `sharer_count` accessor.
283    #[must_use]
284    pub fn strong_count(&self) -> usize {
285        Arc::strong_count(&self.0)
286    }
287    /// Returns the number of weak [`WeakTask`] references to this worker's *identity*
288    /// (`TaskInner`). Pairs with [`strong_count`](Task::strong_count), and equals
289    /// [`WeakTask::weak_count`] on a matching `WeakTask`.
290    #[must_use]
291    pub fn weak_count(&self) -> usize {
292        Arc::weak_count(&self.0)
293    }
294    /// Returns the number of [`Task`] references currently sharing this task's
295    /// progress *cursor* (the `state` `Arc<AtomicU128>`).
296    ///
297    /// This is a *different* quantity from [`strong_count`](Task::strong_count):
298    /// the latter counts worker identities, whereas this counts how many workers
299    /// have aliased the same cursor via [`share_state`](Task::share_state)
300    /// (speculative sharing). Only `steal` consults it, to cap how many workers
301    /// share one cursor.
302    #[must_use]
303    pub(crate) fn sharer_count(&self) -> usize {
304        Arc::strong_count(&self.0.state)
305    }
306    /// Rebinds this task to share `other`'s progress cursor while keeping its own
307    /// distinct identity.
308    ///
309    /// Used by speculative sharing in `steal`: the caller's task aliases the
310    /// victim's cursor without copying it, and — unlike a plain `clone` — remains
311    /// a *separate* worker identity. That separation is what lets the liveness
312    /// sweep in `set_threads` still track each worker independently even after
313    /// sharing.
314    pub(crate) fn share_state(&mut self, other: &Self) {
315        *self = Self(Arc::new(TaskInner {
316            state: other.0.state.clone(),
317        }));
318    }
319    /// Builds a `Task` from a raw state, bypassing the range invariant checked by
320    /// [`new`](Task::new). For tests only: fabricates a corrupted (inverted-range)
321    /// state that `new` would refuse.
322    #[cfg(test)]
323    #[must_use]
324    pub(crate) fn from_raw_state(state: Arc<AtomicU128>) -> Self {
325        Self(Arc::new(TaskInner { state }))
326    }
327}
328/// Creates a [`Task`] from a `start..end` range.
329///
330/// # Panics
331/// Panics (via [`Task::new`]) if `range.start > range.end`.
332impl From<Range<u64>> for Task {
333    fn from(value: Range<u64>) -> Self {
334        Self::new(value)
335    }
336}
337
338impl PartialEq for Task {
339    fn eq(&self, other: &Self) -> bool {
340        Arc::ptr_eq(&self.0.state, &other.0.state)
341    }
342}
343impl Eq for Task {}
344
345#[cfg(test)]
346mod tests {
347    #![allow(clippy::unwrap_used)]
348    extern crate std;
349    use super::*;
350    use std::sync::Arc;
351    use std::thread;
352    use std::vec::Vec;
353
354    #[test]
355    fn test_new_task() {
356        let task = Task::new(10..20);
357        assert_eq!(task.start(), 10);
358        assert_eq!(task.end(), 20);
359        assert_eq!(task.remain(), 10);
360    }
361
362    #[test]
363    fn test_remain() {
364        let task = Task::new(10..25);
365        assert_eq!(task.remain(), 15);
366    }
367
368    #[test]
369    fn test_split_two() {
370        let task = Task::new(1..6); // 1, 2, 3, 4, 5
371        let range = task.split_two(1).unwrap().unwrap();
372        assert_eq!(task.start(), 1);
373        assert_eq!(task.end(), 3);
374        assert_eq!(range.start, 3);
375        assert_eq!(range.end, 6);
376    }
377
378    #[test]
379    fn test_split_empty() {
380        let task = Task::new(1..1);
381        let range = task.split_two(1).unwrap();
382        assert_eq!(task.start(), 1);
383        assert_eq!(task.end(), 1);
384        assert_eq!(range, None);
385    }
386
387    #[test]
388    fn test_split_one() {
389        let task = Task::new(1..2);
390        let range = task.split_two(1).unwrap();
391        assert_eq!(task.start(), 1);
392        assert_eq!(task.end(), 2);
393        assert_eq!(range, None);
394    }
395
396    #[test]
397    fn test_safe_add_start_no_progress() {
398        let task = Task::new(10..20);
399        // bias 0 -> start does not advance
400        assert_eq!(task.safe_add_start(10, 0), Err(RangeError));
401        // bias would not exceed current start
402        assert_eq!(task.safe_add_start(8, 2), Err(RangeError));
403    }
404
405    #[test]
406    fn test_safe_add_start_claims_span() {
407        let task = Task::new(10..20);
408        let span = task.safe_add_start(10, 5).unwrap();
409        assert_eq!(span, 10..15);
410        assert_eq!(task.start(), 15);
411        assert_eq!(task.remain(), 5);
412    }
413
414    #[test]
415    fn test_safe_add_start_capped_at_end() {
416        let task = Task::new(10..12);
417        let span = task.safe_add_start(10, 100).unwrap();
418        assert_eq!(span, 10..12);
419        assert_eq!(task.remain(), 0);
420    }
421
422    #[test]
423    fn test_take_empties() {
424        let task = Task::new(5..9);
425        assert_eq!(task.take(), Ok(Some(5..9)));
426        assert_eq!(task.take(), Ok(None));
427        assert_eq!(task.remain(), 0);
428    }
429
430    #[test]
431    fn test_downgrade_upgrade() {
432        let task = Task::new(1..10);
433        let weak = task.downgrade();
434        assert_eq!(weak.strong_count(), 1);
435        assert_eq!(weak.upgrade().unwrap().get(), 1..10);
436        drop(task);
437        assert_eq!(weak.upgrade(), None);
438    }
439
440    #[test]
441    fn test_partial_eq_by_ptr() {
442        let a = Task::new(1..10);
443        let b = a.clone();
444        assert_eq!(a, b);
445        let c = Task::new(1..10);
446        assert_ne!(a, c);
447    }
448
449    #[test]
450    fn test_split_two_halves() {
451        let task = Task::new(0..100);
452        let range = task.split_two(1).unwrap().unwrap();
453        assert_eq!(range, 50..100);
454        assert_eq!(task.get(), 0..50);
455    }
456
457    #[test]
458    fn split_two_respects_min_chunk_size() {
459        // `remain == 2 * min - 1` cannot yield two halves each >= min.
460        let task = Task::new(0..(2 * 8 - 1)); // remain = 15, min = 8
461        assert_eq!(task.split_two(8), Ok(None));
462        assert_eq!(task.get(), 0..15); // unchanged on refusal
463
464        // `remain == 2 * min` splits into exactly two `min`-sized halves.
465        let task = Task::new(0..16);
466        let range = task.split_two(8).unwrap().unwrap();
467        assert_eq!(range, 8..16);
468        assert_eq!(task.get(), 0..8);
469
470        // A chunk smaller than `min` is never handed out.
471        let task = Task::new(0..10);
472        assert_eq!(task.split_two(8), Ok(None));
473    }
474
475    #[test]
476    fn weak_task_reports_strong_and_weak_counts() {
477        // Covers WeakTask::strong_count (task.rs 51-52) and WeakTask::weak_count
478        // (task.rs 55-56).
479        let task = Task::new(1..10);
480        let weak = task.downgrade();
481        assert_eq!(weak.strong_count(), 1);
482        assert_eq!(weak.weak_count(), 1);
483        // A second weak reference bumps the weak count.
484        let weak2 = task.downgrade();
485        assert_eq!(weak2.weak_count(), 2);
486        drop(weak2);
487        assert_eq!(weak.weak_count(), 1);
488    }
489
490    #[test]
491    fn task_weak_count_reflects_weak_refs() {
492        // Covers Task::weak_count: it counts weak refs to the worker identity (the
493        // WeakTasks spawned by `downgrade`), not cursor weak refs.
494        let task = Task::new(1..10);
495        assert_eq!(task.weak_count(), 0);
496        let _w1 = task.downgrade();
497        assert_eq!(task.weak_count(), 1);
498        let _w2 = task.downgrade();
499        assert_eq!(task.weak_count(), 2);
500    }
501
502    #[test]
503    fn safe_add_start_survives_contention() {
504        // Many threads advancing the same task forces the CAS in
505        // `safe_add_start` to fail and retry, exercising the `Err(x) => old_state = x`
506        // branch (`task.rs` line 135).
507        let task = Arc::new(Task::new(0..2_000));
508        let mut handles = Vec::new();
509        for _ in 0..4 {
510            let t = task.clone();
511            handles.push(thread::spawn(move || {
512                loop {
513                    let s = t.start();
514                    if s >= 2_000 {
515                        break;
516                    }
517                    if t.safe_add_start(s, 1).is_err() {
518                        // Lost the CAS race; loop and retry (exercises the err path).
519                    }
520                }
521            }));
522        }
523        for h in handles {
524            h.join().unwrap();
525        }
526        assert_eq!(task.get(), 2_000..2_000);
527        assert_eq!(task.remain(), 0);
528    }
529
530    #[test]
531    fn split_two_survives_contention() {
532        // Contended `split_two` exercises its CAS-failure branch (`task.rs` line 176).
533        let task = Arc::new(Task::new(0..2_000));
534        let mut handles = Vec::new();
535        for _ in 0..4 {
536            let t = task.clone();
537            handles.push(thread::spawn(
538                move || {
539                    while t.split_two(1).unwrap().is_some() {}
540                },
541            ));
542        }
543        for h in handles {
544            h.join().unwrap();
545        }
546        // split_two leaves a single (un-splittable) remaining element.
547        assert_eq!(task.remain(), 1);
548    }
549
550    #[test]
551    fn take_survives_contention() {
552        // Contended `take` exercises its CAS-failure branch (`task.rs` line 200).
553        let task = Arc::new(Task::new(0..2_000));
554        let mut handles = Vec::new();
555        for _ in 0..4 {
556            let t = task.clone();
557            handles.push(thread::spawn(
558                move || {
559                    while matches!(t.take(), Ok(Some(_))) {}
560                },
561            ));
562        }
563        for h in handles {
564            h.join().unwrap();
565        }
566        assert_eq!(task.remain(), 0);
567    }
568
569    #[test]
570    fn split_two_reports_invariant_violation_when_start_gt_end() {
571        // `Task::new` panics on start > end, so a corrupted/invalid state can
572        // only be built through the `from_raw_state` test helper. This pins the
573        // `range.start > range.end` guard in `split_two`.
574        let bad = Task::from_raw_state(std::sync::Arc::new(portable_atomic::AtomicU128::new(
575            (20u128 << 64) | 0xA,
576        )));
577        assert_eq!(bad.split_two(1), Err(RangeError));
578    }
579
580    /// `RangeError`'s rendered text is part of the public API (it surfaces in
581    /// `?`-propagated error chains), yet nothing pinned it. Also checks the
582    /// `core::error::Error` impl resolves and reports no source.
583    #[test]
584    fn range_error_display_and_error_impl() {
585        use std::{error::Error, format, string::ToString};
586        let e = RangeError;
587        assert_eq!(e.to_string(), "Range invariant violated: start > end");
588        assert_eq!(format!("{e}"), "Range invariant violated: start > end");
589        let as_dyn: &dyn Error = &e;
590        assert!(as_dyn.source().is_none());
591    }
592
593    /// `Task::new` documents a panic on `start > end` but no test pinned it.
594    #[test]
595    #[should_panic(expected = "assertion failed")]
596    fn new_panics_when_start_gt_end() {
597        // Struct literal: an inline `10..5` trips `clippy::reversed_empty_ranges`
598        // (a correctness lint), which is exactly the input under test here.
599        let _ = Task::new(core::ops::Range { start: 10, end: 5 });
600    }
601
602    /// The `From` impl inherits `new`'s panic contract; pinned separately
603    /// because `TaskQueue::new` funnels user ranges through this path.
604    #[test]
605    #[should_panic(expected = "assertion failed")]
606    fn from_range_panics_when_start_gt_end() {
607        let _ = Task::from(core::ops::Range { start: 10, end: 5 });
608    }
609
610    /// `From<Range<u64>>` was only ever exercised indirectly via
611    /// `TaskQueue::new`; pin the happy path directly.
612    #[test]
613    fn from_range_builds_equivalent_task() {
614        let t = Task::from(3..9);
615        assert_eq!(t.get(), 3..9);
616        let t2: Task = (0..0).into();
617        assert_eq!(t2.get(), 0..0);
618        assert_eq!(t2.remain(), 0);
619    }
620
621    /// The whole crate rests on packing two `u64`s into one `AtomicU128`.
622    /// Nothing tested the extremes, where a sloppy shift/truncate would
623    /// silently corrupt the range.
624    #[test]
625    fn pack_unpack_round_trips_at_u64_bounds() {
626        for range in [
627            0..0,
628            0..u64::MAX,
629            u64::MAX..u64::MAX,
630            (u64::MAX - 1)..u64::MAX,
631        ] {
632            let t = Task::new(range.clone());
633            assert_eq!(t.get(), range, "round-trip lost bits");
634            assert_eq!(t.start(), range.start);
635            assert_eq!(t.end(), range.end);
636        }
637        assert_eq!(Task::new(0..u64::MAX).remain(), u64::MAX);
638    }
639
640    /// `start + bias` overflow no longer errors: it saturates to `u64::MAX` and
641    /// the claim is clamped by `min(end)`, so an overflowing bias claims the
642    /// entire remaining range instead of failing.
643    #[test]
644    fn safe_add_start_saturates_on_u64_overflow() {
645        let task = Task::new(0..u64::MAX);
646        let span = task.safe_add_start(0, u64::MAX).unwrap();
647        assert_eq!(span, 0..u64::MAX);
648        assert_eq!(task.get(), u64::MAX..u64::MAX);
649        assert_eq!(task.remain(), 0);
650
651        // Saturation at a non-zero cursor still clamps at `end`, never beyond.
652        let task = Task::new((u64::MAX - 5)..u64::MAX);
653        let span = task.safe_add_start(u64::MAX - 5, u64::MAX).unwrap();
654        assert_eq!(span, (u64::MAX - 5)..u64::MAX);
655        assert_eq!(task.remain(), 0);
656    }
657
658    /// `safe_add_start` never verifies that the caller-supplied `start` matches
659    /// the task's real cursor. It only rejects a *stale* start
660    /// (via `new_start <= range.start`); a start that runs *ahead* of reality
661    /// is trusted blindly, jumping the cursor over work nobody executed and
662    /// returning a span far wider than `bias`.
663    ///
664    /// This test pins the hardening: the ahead-of-cursor start is now rejected
665    /// and the task is left completely untouched.
666    #[test]
667    fn safe_add_start_rejects_caller_start_ahead_of_cursor() {
668        let task = Task::new(0..100);
669        // Caller asks to advance 1 step "from 50", but the cursor is at 0.
670        // Before hardening this returned `Ok(0..51)`, skipping items 0..50.
671        assert_eq!(task.safe_add_start(50, 1), Err(RangeError));
672        assert_eq!(task.get(), 0..100, "a rejected call must not mutate state");
673        // Even a start beyond `end` is refused rather than clamped by `min`.
674        assert_eq!(task.safe_add_start(500, 1), Err(RangeError));
675        assert_eq!(task.get(), 0..100);
676        // One step ahead is still ahead.
677        task.safe_add_start(0, 10).unwrap(); // cursor -> 10
678        assert_eq!(task.safe_add_start(11, 1), Err(RangeError));
679        assert_eq!(task.get(), 10..100);
680        // ...while the exact cursor keeps working.
681        assert_eq!(task.safe_add_start(10, 1), Ok(10..11));
682    }
683
684    /// The mirror case, which *is* the intended usage: a caller whose `start`
685    /// is stale still succeeds if `start + bias` overtakes the real cursor, but
686    /// the returned span begins at the real cursor and is therefore shorter
687    /// than `bias`. Callers must use the returned span, never assume
688    /// `start..start + bias`.
689    #[test]
690    fn safe_add_start_span_shrinks_when_caller_start_is_stale() {
691        let task = Task::new(0..100);
692        task.safe_add_start(0, 5).unwrap(); // cursor -> 5
693        let span = task.safe_add_start(3, 5).unwrap(); // stale 3, target 8
694        assert_eq!(
695            span,
696            5..8,
697            "span must start at the real cursor, not the stale one"
698        );
699        assert_eq!(task.start(), 8);
700    }
701
702    /// `take` used to guard with `start == end` while
703    /// `split_two` used `start > end -> Err`, so a corrupted (inverted) state
704    /// made `take` hand back a *reversed* Range that iterates as empty -- work
705    /// silently dropped, evidence erased by the normalising CAS. Both methods
706    /// now share the `start > end -> Err` contract.
707    #[test]
708    fn take_reports_invariant_violation_on_corrupted_state() {
709        let bad = Task::from_raw_state(Arc::new(portable_atomic::AtomicU128::new(
710            (20u128 << 64) | 0xA,
711        )));
712        // Built via the raw-state helper on purpose: writing `20..10` inline trips
713        // `clippy::reversed_empty_ranges`, yet a reversed range is precisely
714        // what the corrupted state yields.
715        let inverted = core::ops::Range {
716            start: 20u64,
717            end: 10u64,
718        };
719        assert_eq!(bad.get(), inverted);
720        assert_eq!(bad.take(), Err(RangeError));
721        // The corrupted state is preserved, not normalised away, so it stays
722        // diagnosable -- and `take` agrees with `split_two` on the same input.
723        assert_eq!(bad.get(), inverted);
724        assert_eq!(bad.split_two(1), Err(RangeError));
725    }
726
727    /// `remain` uses `saturating_sub`, so an inverted range reports 0 instead
728    /// of underflowing. Pinned because it is the reason a corrupted task looks
729    /// "finished" rather than "broken" to `TaskQueue::steal`'s `max_by_key`.
730    #[test]
731    fn remain_saturates_to_zero_on_corrupted_state() {
732        let bad = Task::from_raw_state(Arc::new(portable_atomic::AtomicU128::new(
733            (20u128 << 64) | 0xA,
734        )));
735        assert_eq!(bad.remain(), 0);
736    }
737
738    /// `WeakTask::weak_count` delegates to `Weak::weak_count`, which collapses to
739    /// 0 once the last strong ref to the identity dies -- it does NOT report
740    /// surviving weak refs. The paired [`Task::weak_count`] behaves identically
741    /// (both are scoped to the worker identity), so `Task`/`WeakTask` stay a
742    /// consistent strong/weak pair.
743    #[test]
744    fn weak_task_weak_count_collapses_to_zero_without_strong_refs() {
745        let task = Task::new(0..10);
746        let w1 = task.downgrade();
747        let _w2 = task.downgrade();
748        assert_eq!(w1.weak_count(), 2);
749        assert_eq!(w1.strong_count(), 1);
750        drop(task);
751        assert_eq!(w1.strong_count(), 0);
752        assert_eq!(
753            w1.weak_count(),
754            0,
755            "weak_count collapses once the strong count hits 0"
756        );
757        assert!(w1.upgrade().is_none());
758    }
759
760    /// `Task::sharer_count` had no direct test, yet `TaskQueue::steal` gates
761    /// speculative sharing on it (the sharer cap). It counts strong references to
762    /// the *shared cursor*, so a speculative sharer (via `share_state`) bumps it
763    /// while a plain `clone` — which only shares the worker identity — does not.
764    #[test]
765    fn task_sharer_count_tracks_cursor_sharers() {
766        let task = Task::new(0..10);
767        assert_eq!(task.sharer_count(), 1);
768
769        // A speculative sharer aliases the same cursor and holds its own strong
770        // ref to it.
771        let mut twin = Task::new(0..0);
772        twin.share_state(&task);
773        assert_eq!(
774            task.sharer_count(),
775            2,
776            "the twin holds a strong ref to the cursor"
777        );
778        assert_eq!(twin, task, "twins compare equal via the shared cursor");
779        drop(twin);
780        assert_eq!(
781            task.sharer_count(),
782            1,
783            "dropping the twin releases its cursor ref"
784        );
785
786        // A plain `clone` shares the worker identity, not an extra cursor ref, so
787        // it must NOT change `sharer_count`.
788        let _alias = task.clone();
789        assert_eq!(
790            task.sharer_count(),
791            1,
792            "clone shares identity, not a cursor ref"
793        );
794
795        // An upgraded `WeakTask` holds a strong ref to the *identity* (TaskInner),
796        // not to the cursor, so it also leaves `sharer_count` unchanged.
797        let w = task.downgrade();
798        let _up = w.upgrade().unwrap();
799        assert_eq!(
800            task.sharer_count(),
801            1,
802            "upgrade keeps the cursor count unchanged"
803        );
804    }
805
806    /// Pins the post-cleanup invariant: `Task::strong_count` counts *worker
807    /// identity* (like `WeakTask::strong_count`), NOT the shared cursor -- and the
808    /// two paired accessors stay equal. This guards against the count-API
809    /// inconsistency introduced when identity was split from the shared cursor.
810    #[test]
811    fn task_strong_count_counts_identity_not_cursor() {
812        let task = Task::new(0..10);
813        let weak = task.downgrade();
814
815        // Paired strong/weak views of the same identity: equal counts.
816        assert_eq!(task.strong_count(), 1);
817        assert_eq!(weak.strong_count(), 1);
818
819        // A plain `clone` shares the identity, so it bumps `strong_count`...
820        let _alias = task.clone();
821        assert_eq!(task.strong_count(), 2);
822        // ...and the paired `WeakTask` accessor tracks the very same identity.
823        assert_eq!(weak.strong_count(), 2);
824
825        // A `clone` does NOT touch the cursor, so `sharer_count` is unchanged.
826        assert_eq!(task.sharer_count(), 1, "clone does not alias the cursor");
827
828        // `share_state` aliases the cursor (new identity) and bumps `sharer_count`,
829        // but leaves `strong_count` (identity) untouched.
830        let mut twin = Task::new(0..0);
831        twin.share_state(&task);
832        assert_eq!(task.sharer_count(), 2, "share_state aliases the cursor");
833        assert_eq!(task.strong_count(), 2, "share_state keeps its own identity");
834    }
835
836    /// `WeakTask::is_alive` reports whether the worker *identity* is still held,
837    /// independent of how many twins share the cursor.
838    #[test]
839    fn weak_task_is_alive_tracks_identity_not_cursor() {
840        let task = Task::new(0..10);
841        let weak = task.downgrade();
842        assert!(weak.is_alive());
843        drop(task);
844        assert!(!weak.is_alive(), "identity dropped -> not alive");
845
846        // A speculative twin holds its OWN identity, so the victim's death does
847        // not flip the twin's `is_alive`.
848        let victim = Task::new(0..10);
849        let mut twin = Task::new(0..0);
850        twin.share_state(&victim);
851        let twin_weak = twin.downgrade();
852        assert!(twin_weak.is_alive());
853        drop(victim);
854        assert!(
855            twin_weak.is_alive(),
856            "twin keeps its own identity alive after the victim dies"
857        );
858    }
859
860    /// `mid = start + (end - start) / 2` must not overflow at the top of the
861    /// u64 range, where the naive `(start + end) / 2` would wrap.
862    #[test]
863    fn split_two_handles_u64_extremes_without_overflow() {
864        let task = Task::new(0..u64::MAX);
865        let hi = task.split_two(1).unwrap().unwrap();
866        assert_eq!(hi, (u64::MAX / 2)..u64::MAX);
867        assert_eq!(task.get(), 0..(u64::MAX / 2));
868
869        let task = Task::new((u64::MAX - 3)..u64::MAX);
870        let hi = task.split_two(1).unwrap().unwrap();
871        assert_eq!(hi, (u64::MAX - 2)..u64::MAX);
872        assert_eq!(task.get(), (u64::MAX - 3)..(u64::MAX - 2));
873    }
874}