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}