Skip to main content

nautilus_live/
task.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Async task ownership and bounded shutdown for live components.
17//!
18//! This module keeps spawned work attached to its owner until shutdown observes a terminal result.
19//! Use [`TaskGroup`] for related unit-output tasks and [`TaskSlot`] or [`SharedTaskSlot`] when one
20//! task's identity or typed result belongs to the owning component. A shutdown timeout retains
21//! ownership so the caller can drain the task again instead of detaching it.
22//!
23//! # Task groups
24//!
25//! [`TaskGroup`] owns related `Future<Output = ()>` tasks for one lifecycle generation.
26//! [`TaskGroup::spawn`] registers each task before its future can poll. [`TaskGroup::spawn_named`]
27//! additionally returns a read-only [`TaskRef`] that preserves the task's logical name, instance
28//! identity, and terminal state without transferring ownership. Once
29//! [`TaskGroup::begin_shutdown`] closes admission, concurrent and later spawn attempts return
30//! [`TaskSpawnError`].
31//!
32//! # Generation-bound spawning
33//!
34//! [`TaskSpawner`] lets an admitted task create children in the same generation. Its cancellation
35//! token signals shutdown but does not grant task ownership: child work must still pass through
36//! [`TaskSpawner::spawn`]. A spawner from a closed generation cannot admit work into a replacement
37//! generation.
38//!
39//! # Group shutdown
40//!
41//! [`TaskGroup::begin_shutdown`] synchronously closes admission and requests graceful cancellation.
42//! [`TaskGroup::abort`] closes admission and requests forced cancellation immediately when a
43//! synchronous owner cannot offer a graceful completion phase.
44//! [`TaskGroup::finish_shutdown`] waits for admitted tasks, requests forced cancellation after the
45//! graceful deadline, and waits again within the abort deadline. Panics, unexpected cancellation,
46//! and tasks that outlive both deadlines remain observable through [`TaskShutdownError`].
47//!
48//! A timed-out generation stays closed and retains its tasks for another drain attempt.
49//! [`TaskGroup::start_generation`] opens a replacement only after the prior generation fully
50//! drains. Dropping a group requests forced cancellation but cannot await task termination, so
51//! owners that need a proven shutdown must call [`TaskGroup::finish_shutdown`].
52//!
53//! # Partial setup rollback
54//!
55//! [`TaskGroupGuard`] closes its task groups and runs a synchronous rollback callback if setup
56//! exits while the guard remains armed. Disarm it after setup succeeds. The owner still performs
57//! the asynchronous bounded drain after a rollback.
58//!
59//! # Singular tasks
60//!
61//! [`TaskSlot`] owns one task and preserves its output type. [`SharedTaskSlot`] provides the same
62//! ownership for clients that share the task across clones and serializes concurrent drain
63//! attempts. [`finish_task`] and [`SharedTaskSlot::finish`] wait gracefully, abort within a second
64//! bound, and retain an unfinished task if the finish future is canceled or the abort deadline
65//! expires.
66//!
67//! # Outcomes and errors
68//!
69//! [`TaskJoinOutcome`] distinguishes normal completion, owner-requested abort, join failure, and an
70//! incomplete task that remains owned. [`TaskSpawnError`] reports failed admission or start,
71//! [`TaskGenerationError`] prevents a replacement generation from opening too early, and
72//! [`TaskShutdownError`] reports group shutdown state, failures, and remaining tasks.
73
74use std::{
75    any::Any,
76    error::Error,
77    fmt::Display,
78    future::Future,
79    panic::AssertUnwindSafe,
80    sync::{
81        Arc,
82        atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
83    },
84    time::Duration,
85};
86
87use arc_swap::ArcSwap;
88use futures_util::FutureExt;
89use nautilus_common::live::dst::{
90    task::{JoinError, JoinHandle},
91    time,
92};
93use parking_lot::{Mutex, MutexGuard};
94use tokio_util::{
95    sync::CancellationToken,
96    task::{TaskTracker, task_tracker::TaskTrackerToken},
97};
98
99type TaskState = AtomicU8;
100
101static NEXT_TASK_ID: AtomicU64 = AtomicU64::new(1);
102
103/// A process-unique live task instance identifier.
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
105pub struct TaskId(u64);
106
107impl TaskId {
108    fn next() -> Self {
109        let id = NEXT_TASK_ID
110            .try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
111            .expect("task ID space exhausted");
112        Self(id)
113    }
114}
115
116impl Display for TaskId {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        self.0.fmt(f)
119    }
120}
121
122/// A read-only reference to one task admitted by a [`TaskGroup`].
123///
124/// The group retains cancellation and join ownership. The task is active from admission until its
125/// wrapper reaches a terminal path; active does not imply that the user future has received its
126/// first poll. A task may finish before [`TaskGroup::spawn_named`] returns.
127#[derive(Clone, Debug)]
128pub struct TaskRef {
129    identity: Arc<TaskIdentity>,
130}
131
132impl TaskRef {
133    /// Returns the task's process-unique instance identifier.
134    #[must_use]
135    pub fn id(&self) -> TaskId {
136        self.identity.id
137    }
138
139    /// Returns the task's logical name.
140    #[must_use]
141    pub fn name(&self) -> &'static str {
142        self.identity.name
143    }
144
145    /// Returns whether the task was admitted and has not reached a terminal path.
146    #[must_use]
147    pub fn is_active(&self) -> bool {
148        !self.is_finished()
149    }
150
151    /// Returns whether the task reached a terminal path.
152    #[must_use]
153    pub fn is_finished(&self) -> bool {
154        self.identity.finished.load(Ordering::Acquire)
155    }
156}
157
158#[derive(Debug)]
159struct TaskIdentity {
160    id: TaskId,
161    name: &'static str,
162    finished: AtomicBool,
163}
164
165impl TaskIdentity {
166    fn new(name: &'static str) -> Self {
167        Self {
168            id: TaskId::next(),
169            name,
170            finished: AtomicBool::new(false),
171        }
172    }
173
174    fn finish(&self) {
175        self.finished.store(true, Ordering::Release);
176    }
177
178    fn failure(&self, failure: &str) -> String {
179        format!("task '{}' ({}) {failure}", self.name, self.id)
180    }
181}
182
183/// Owns a related group of cancellation-aware live tasks, one generation at a time.
184///
185/// Call [`Self::finish_shutdown`] to observe task failures. Dropping the group requests forced
186/// cancellation but cannot asynchronously prove termination.
187#[derive(Debug)]
188pub struct TaskGroup {
189    inner: Arc<TaskGroupInner>,
190}
191
192impl Default for TaskGroup {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198impl TaskGroup {
199    /// Creates an open initial task generation.
200    #[must_use]
201    pub fn new() -> Self {
202        Self {
203            inner: Arc::new(TaskGroupInner {
204                generation: ArcSwap::from_pointee(TaskGeneration::new()),
205                generation_lock: parking_lot::Mutex::new(()),
206                drain_lock: tokio::sync::Mutex::new(()),
207            }),
208        }
209    }
210
211    /// Returns a capability bound to the current open generation.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error after shutdown begins.
216    pub fn spawner(&self) -> Result<TaskSpawner, TaskSpawnError> {
217        let generation = self.inner.current();
218        if !generation.is_open() {
219            return Err(TaskSpawnError::CLOSED);
220        }
221
222        Ok(TaskSpawner { generation })
223    }
224
225    /// Returns a non-authoritative cancellation signal for the current generation.
226    #[must_use]
227    pub fn cancellation_token(&self) -> CancellationToken {
228        self.inner.current().cancellation.child_token()
229    }
230
231    /// Registers `future` before allowing it to poll.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error after shutdown begins.
236    pub fn spawn<F>(&self, future: F) -> Result<(), TaskSpawnError>
237    where
238        F: Future<Output = ()> + Send + 'static,
239    {
240        self.inner.current().spawn(future)
241    }
242
243    // panics-doc-ok (transitive via task ID allocation)
244    /// Registers a named `future` before allowing it to poll.
245    ///
246    /// The returned reference observes identity and terminal state without owning cancellation or
247    /// joining. The task may reach a terminal state before this method returns.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error after shutdown begins.
252    ///
253    /// # Panics
254    ///
255    /// Panics if the process exhausts the `u64` task identifier space.
256    pub fn spawn_named<F>(&self, name: &'static str, future: F) -> Result<TaskRef, TaskSpawnError>
257    where
258        F: Future<Output = ()> + Send + 'static,
259    {
260        self.inner.current().spawn_named(name, future)
261    }
262
263    /// Closes admission and cancels the current generation.
264    pub fn begin_shutdown(&self) {
265        self.inner.begin_shutdown();
266    }
267
268    /// Closes admission and requests immediate forced cancellation.
269    pub fn abort(&self) {
270        self.inner.begin_shutdown().force.cancel();
271    }
272
273    /// Drains the closed generation within graceful and forced bounds.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if admission is open, a task fails unexpectedly, or forced completion
278    /// reaches its deadline. Timed-out tasks remain tracked and reopening stays disabled.
279    pub async fn finish_shutdown(
280        &self,
281        graceful_timeout: Duration,
282        abort_timeout: Duration,
283    ) -> Result<(), TaskShutdownError> {
284        let generation = self.inner.current();
285        if generation.phase() == TaskGroupPhase::Open {
286            return Err(TaskShutdownError::StillOpen);
287        }
288
289        let started = time::Instant::now();
290
291        let Some(graceful_deadline) = started.checked_add(graceful_timeout) else {
292            return Err(generation.timeout_error());
293        };
294
295        let Some(abort_deadline) = graceful_deadline.checked_add(abort_timeout) else {
296            return Err(generation.timeout_error());
297        };
298
299        let lock_timeout = abort_deadline.saturating_duration_since(time::Instant::now());
300
301        let Ok(_drain_lock) = time::timeout(lock_timeout, self.inner.drain_lock.lock()).await
302        else {
303            return Err(generation.timeout_error());
304        };
305
306        match generation.phase() {
307            TaskGroupPhase::Open => return Err(TaskShutdownError::StillOpen),
308            TaskGroupPhase::Drained => return generation.complete_shutdown(),
309            TaskGroupPhase::Closing => {}
310        }
311
312        generation.tasks.close();
313        generation.cancellation.cancel();
314        let graceful_remaining = graceful_deadline.saturating_duration_since(time::Instant::now());
315        if time::timeout(graceful_remaining, generation.tasks.wait())
316            .await
317            .is_ok()
318        {
319            return generation.complete_shutdown();
320        }
321
322        generation.force.cancel();
323        let abort_remaining = abort_deadline.saturating_duration_since(time::Instant::now());
324        if time::timeout(abort_remaining, generation.tasks.wait())
325            .await
326            .is_err()
327        {
328            let incomplete = generation.tasks.len();
329            if incomplete == 0 {
330                return generation.complete_shutdown();
331            }
332
333            return Err(TaskShutdownError::Timeout {
334                failures: generation.take_failures(),
335                incomplete,
336            });
337        }
338
339        generation.complete_shutdown()
340    }
341
342    /// Opens a fresh generation after the prior generation fully drains.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error while the current generation remains open or owns tasks.
347    pub fn start_generation(&self) -> Result<(), TaskGenerationError> {
348        self.inner.start_generation()
349    }
350
351    /// Returns whether no tasks remain owned.
352    #[must_use]
353    pub fn is_empty(&self) -> bool {
354        self.inner.current().tasks.is_empty()
355    }
356
357    /// Returns whether every tracked task has finished.
358    #[must_use]
359    pub fn all_finished(&self) -> bool {
360        self.is_empty()
361    }
362
363    /// Returns whether the current generation accepts tasks.
364    #[must_use]
365    pub fn is_open(&self) -> bool {
366        self.inner.current().is_open()
367    }
368
369    /// Returns the number of tasks currently owned.
370    #[must_use]
371    pub fn len(&self) -> usize {
372        self.inner.current().tasks.len()
373    }
374}
375
376impl Drop for TaskGroup {
377    fn drop(&mut self) {
378        self.abort();
379    }
380}
381
382/// A generation-bound capability for cancellation-aware child tasks.
383#[derive(Clone, Debug)]
384pub struct TaskSpawner {
385    generation: Arc<TaskGeneration>,
386}
387
388impl TaskSpawner {
389    /// Returns a non-authoritative cancellation signal for this generation.
390    #[must_use]
391    pub fn cancellation_token(&self) -> CancellationToken {
392        self.generation.cancellation.child_token()
393    }
394
395    /// Registers `future` before allowing it to poll.
396    ///
397    /// # Errors
398    ///
399    /// Returns an error when this spawner no longer belongs to the open generation.
400    pub fn spawn<F>(&self, future: F) -> Result<(), TaskSpawnError>
401    where
402        F: Future<Output = ()> + Send + 'static,
403    {
404        self.generation.spawn(future)
405    }
406
407    // panics-doc-ok (transitive via task ID allocation)
408    /// Registers a named `future` before allowing it to poll.
409    ///
410    /// The returned reference observes identity and terminal state without owning cancellation or
411    /// joining. The task may reach a terminal state before this method returns.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error when this spawner no longer belongs to the open generation.
416    ///
417    /// # Panics
418    ///
419    /// Panics if the process exhausts the `u64` task identifier space.
420    pub fn spawn_named<F>(&self, name: &'static str, future: F) -> Result<TaskRef, TaskSpawnError>
421    where
422        F: Future<Output = ()> + Send + 'static,
423    {
424        self.generation.spawn_named(name, future)
425    }
426}
427
428/// Closes task groups and runs synchronous rollback when dropped while armed.
429pub struct TaskGroupGuard<F: FnOnce()> {
430    groups: Vec<Arc<TaskGroupInner>>,
431    rollback: Option<F>,
432}
433
434impl<F: FnOnce()> std::fmt::Debug for TaskGroupGuard<F> {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        f.debug_struct(stringify!(TaskGroupGuard))
437            .field("groups", &self.groups.len())
438            .field("armed", &self.rollback.is_some())
439            .finish_non_exhaustive()
440    }
441}
442
443impl<F: FnOnce()> TaskGroupGuard<F> {
444    /// Arms rollback for `groups`.
445    #[must_use]
446    pub fn new(groups: &[&TaskGroup], rollback: F) -> Self {
447        Self {
448            groups: groups
449                .iter()
450                .map(|group| Arc::clone(&group.inner))
451                .collect(),
452            rollback: Some(rollback),
453        }
454    }
455
456    /// Disarms the guard without running rollback.
457    pub fn disarm(mut self) {
458        self.rollback.take();
459    }
460}
461
462impl<F: FnOnce()> Drop for TaskGroupGuard<F> {
463    fn drop(&mut self) {
464        if let Some(rollback) = self.rollback.take() {
465            for group in &self.groups {
466                group.begin_shutdown();
467            }
468
469            rollback();
470        }
471    }
472}
473
474/// A task admission failure.
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
476pub struct TaskSpawnError(&'static str);
477
478impl TaskSpawnError {
479    const CLOSED: Self = Self("task group admission is closed");
480    const START_FAILED: Self = Self("task stopped before its start gate opened");
481}
482
483impl Display for TaskSpawnError {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        f.write_str(self.0)
486    }
487}
488
489impl Error for TaskSpawnError {}
490
491/// Indicates that the prior task group generation has not fully drained.
492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub struct TaskGenerationError(&'static str);
494
495impl TaskGenerationError {
496    const NOT_DRAINED: Self = Self("prior task group generation has not fully drained");
497}
498
499impl Display for TaskGenerationError {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        f.write_str(self.0)
502    }
503}
504
505impl Error for TaskGenerationError {}
506
507/// A bounded task shutdown failure.
508#[derive(Clone, Debug, PartialEq, Eq)]
509pub enum TaskShutdownError {
510    /// Shutdown was requested before admission closed.
511    StillOpen,
512    /// All handles drained, but at least one join failed unexpectedly.
513    Join(Vec<String>),
514    /// Forced completion reached its deadline with tasks still owned.
515    Timeout {
516        /// Join failures observed before the deadline.
517        failures: Vec<String>,
518        /// Tasks still owned after the deadline.
519        incomplete: usize,
520    },
521}
522
523impl Display for TaskShutdownError {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        match self {
526            Self::StillOpen => f.write_str("task group admission is still open"),
527            Self::Join(failures) => write!(
528                f,
529                "task shutdown observed join failures: {}",
530                failures.join("; ")
531            ),
532            Self::Timeout {
533                failures,
534                incomplete,
535            } => {
536                write!(
537                    f,
538                    "task shutdown timed out with {incomplete} task(s) still owned"
539                )?;
540
541                if !failures.is_empty() {
542                    write!(f, ": join failures: {}", failures.join("; "))?;
543                }
544
545                Ok(())
546            }
547        }
548    }
549}
550
551impl Error for TaskShutdownError {}
552
553/// The observed result of bounded shutdown for one explicitly singular task.
554#[derive(Debug)]
555#[must_use]
556pub enum TaskJoinOutcome<T> {
557    /// The task returned before either shutdown deadline.
558    Completed(T),
559    /// The task was canceled by the forced-abort phase.
560    Aborted,
561    /// The task failed before or after forced abort.
562    Failed(JoinError),
563    /// The task did not finish within the supplied bounds and remains owned.
564    Incomplete,
565}
566
567/// Owns one typed task and its forced-abort state across bounded drain attempts.
568///
569/// Call [`finish_task`] to observe the join outcome. Dropping the slot requests abort but cannot
570/// asynchronously prove termination.
571#[derive(Debug)]
572pub struct TaskSlot<T> {
573    handle: Option<JoinHandle<T>>,
574    abort_requested: bool,
575}
576
577impl<T> Default for TaskSlot<T> {
578    fn default() -> Self {
579        Self::new()
580    }
581}
582
583impl<T> TaskSlot<T> {
584    /// Creates an empty task slot.
585    #[must_use]
586    pub const fn new() -> Self {
587        Self {
588            handle: None,
589            abort_requested: false,
590        }
591    }
592
593    /// Creates a task slot owning `handle`.
594    #[must_use]
595    pub const fn from_handle(handle: JoinHandle<T>) -> Self {
596        Self {
597            handle: Some(handle),
598            abort_requested: false,
599        }
600    }
601
602    /// Spawns and stores a task before its future can be polled.
603    ///
604    /// # Errors
605    ///
606    /// Returns an error if the task stops before its start gate opens. The terminal handle remains
607    /// owned so its join outcome stays observable.
608    ///
609    /// # Panics
610    ///
611    /// Panics if the slot already owns a task.
612    #[expect(
613        clippy::panic_in_result_fn,
614        reason = "an occupied slot is a caller invariant violation"
615    )]
616    pub fn spawn<F>(&mut self, future: F) -> Result<(), TaskSpawnError>
617    where
618        T: Send + 'static,
619        F: Future<Output = T> + Send + 'static,
620    {
621        assert!(self.handle.is_none(), "task slot is already occupied");
622        let (handle, start) = spawn_gated(future);
623        self.handle = Some(handle);
624        self.abort_requested = false;
625        start.send(()).map_err(|()| TaskSpawnError::START_FAILED)
626    }
627
628    /// Returns whether the slot owns a task.
629    #[must_use]
630    pub const fn is_some(&self) -> bool {
631        self.handle.is_some()
632    }
633
634    /// Returns whether the slot is empty.
635    #[must_use]
636    pub const fn is_none(&self) -> bool {
637        self.handle.is_none()
638    }
639
640    /// Returns the owned task handle, when present.
641    #[must_use]
642    pub const fn as_ref(&self) -> Option<&JoinHandle<T>> {
643        self.handle.as_ref()
644    }
645
646    /// Stores a task in an empty slot.
647    ///
648    /// # Panics
649    ///
650    /// Aborts `handle` and panics if the slot already owns a task.
651    pub fn insert(&mut self, handle: JoinHandle<T>) {
652        if self.handle.is_some() {
653            handle.abort();
654            panic!("task slot is already occupied");
655        }
656
657        self.handle = Some(handle);
658        self.abort_requested = false;
659    }
660
661    /// Requests task cancellation and records it as owner-initiated.
662    pub fn abort(&mut self) {
663        if let Some(handle) = self.handle.as_ref() {
664            handle.abort();
665            self.abort_requested = true;
666        }
667    }
668
669    fn complete(&mut self, result: Result<T, JoinError>) -> TaskJoinOutcome<T> {
670        let outcome = match result {
671            Ok(output) => TaskJoinOutcome::Completed(output),
672            Err(e) if e.is_cancelled() && self.abort_requested => TaskJoinOutcome::Aborted,
673            Err(e) => TaskJoinOutcome::Failed(e),
674        };
675
676        self.handle.take();
677        self.abort_requested = false;
678        outcome
679    }
680}
681
682impl<T> Drop for TaskSlot<T> {
683    fn drop(&mut self) {
684        if let Some(handle) = self.handle.as_ref() {
685            handle.abort();
686        }
687    }
688}
689
690/// Owns one typed task shared by cloned clients.
691///
692/// Concurrent finish calls are serialized within the caller's graceful and abort durations.
693/// Canceling a finish or reaching the forced-completion deadline retains the task for another drain
694/// attempt. Dropping the owner requests abort but cannot asynchronously prove termination.
695#[derive(Debug)]
696pub struct SharedTaskSlot<T> {
697    state: Mutex<SharedTaskState<T>>,
698    drain_lock: tokio::sync::Mutex<()>,
699    owned: AtomicBool,
700}
701
702#[derive(Debug)]
703struct SharedTaskState<T> {
704    slot: TaskSlot<T>,
705    abort: CancellationToken,
706    abort_requested: bool,
707    draining: bool,
708}
709
710impl<T> SharedTaskState<T> {
711    fn try_reserve_drain(&mut self) -> Option<(TaskSlot<T>, CancellationToken, bool)> {
712        if self.draining {
713            return None;
714        }
715
716        self.draining = true;
717        Some((
718            std::mem::take(&mut self.slot),
719            self.abort.clone(),
720            self.abort_requested,
721        ))
722    }
723}
724
725impl<T> Default for SharedTaskSlot<T> {
726    fn default() -> Self {
727        Self::new()
728    }
729}
730
731impl<T> SharedTaskSlot<T> {
732    /// Creates an empty shared task slot.
733    #[must_use]
734    pub fn new() -> Self {
735        Self {
736            state: Mutex::new(SharedTaskState {
737                slot: TaskSlot::new(),
738                abort: CancellationToken::new(),
739                abort_requested: false,
740                draining: false,
741            }),
742            drain_lock: tokio::sync::Mutex::const_new(()),
743            owned: AtomicBool::new(false),
744        }
745    }
746
747    /// Returns whether the slot owns no task, including while a drain is pending.
748    #[must_use]
749    pub fn is_empty(&self) -> bool {
750        !self.owned.load(Ordering::Acquire)
751    }
752
753    /// Returns whether the owned task has finished without being joined.
754    ///
755    /// Returns `false` while a drain owns the handle and its state is unavailable.
756    #[must_use]
757    pub fn is_finished(&self) -> bool {
758        if self.is_empty() {
759            return false;
760        }
761
762        let state = self.state.lock();
763        !state.draining && state.slot.as_ref().is_some_and(JoinHandle::is_finished)
764    }
765
766    /// Stores a task in an empty slot.
767    ///
768    /// # Panics
769    ///
770    /// Aborts `handle` and panics if the slot already owns a task or is draining one.
771    pub fn insert(&self, handle: JoinHandle<T>) {
772        self.insert_slot(TaskSlot::from_handle(handle));
773    }
774
775    // panics-doc-ok (transitive via insert on an occupied or draining slot)
776    /// Spawns and stores a task before its future can be polled.
777    ///
778    /// # Errors
779    ///
780    /// Returns an error if the task stops before its start gate opens. The terminal handle remains
781    /// owned so its join outcome stays observable.
782    ///
783    /// # Panics
784    ///
785    /// Panics if the slot already owns a task or is draining one.
786    pub fn spawn<F>(&self, future: F) -> Result<(), TaskSpawnError>
787    where
788        T: Send + 'static,
789        F: Future<Output = T> + Send + 'static,
790    {
791        let (handle, start) = spawn_gated(future);
792        self.insert(handle);
793        start.send(()).map_err(|()| TaskSpawnError::START_FAILED)
794    }
795
796    fn insert_slot(&self, slot: TaskSlot<T>) {
797        assert!(
798            self.try_insert_slot(slot).is_ok(),
799            "shared task slot is already occupied"
800        );
801    }
802
803    /// Transfers a task slot into this owner if it is empty.
804    ///
805    /// An empty input slot is always accepted. Returns a nonempty input unchanged when this owner
806    /// already has a task or is draining one.
807    ///
808    /// # Errors
809    ///
810    /// Returns the input slot when this owner already has a task or is draining one.
811    pub fn try_insert_slot(&self, slot: TaskSlot<T>) -> Result<(), TaskSlot<T>> {
812        if slot.is_none() {
813            return Ok(());
814        }
815
816        let mut state = self.state.lock();
817        if self.owned.load(Ordering::Acquire) || state.slot.is_some() || state.draining {
818            return Err(slot);
819        }
820
821        self.owned.store(true, Ordering::Release);
822        state.slot = slot;
823        state.abort = CancellationToken::new();
824        state.abort_requested = false;
825        Ok(())
826    }
827
828    /// Requests task cancellation and records it as owner-initiated.
829    ///
830    /// # Panics
831    ///
832    /// Panics if the shared task slot changes while cancellation temporarily drains it.
833    pub fn abort(&self) {
834        let (mut slot, abort, moved_slot) = {
835            let mut state = self.state.lock();
836            state.abort_requested = true;
837            let moved_slot = !state.draining && state.slot.is_some();
838            if moved_slot {
839                state.draining = true;
840            }
841
842            let slot = if moved_slot {
843                std::mem::take(&mut state.slot)
844            } else {
845                TaskSlot::new()
846            };
847
848            (slot, state.abort.clone(), moved_slot)
849        };
850
851        slot.abort();
852        abort.cancel();
853
854        if moved_slot {
855            let mut state = self.state.lock();
856            assert!(
857                state.slot.is_none(),
858                "shared task slot changed while aborting"
859            );
860            state.slot = slot;
861            state.draining = false;
862        }
863    }
864
865    /// Gracefully joins the task, then aborts and joins it within a second bound.
866    ///
867    /// If another caller holds the drain lock through both bounds, returns
868    /// [`TaskJoinOutcome::Incomplete`] when a task remains owned.
869    pub async fn finish(
870        &self,
871        graceful_timeout: Duration,
872        abort_timeout: Duration,
873    ) -> Option<TaskJoinOutcome<T>> {
874        let started = time::Instant::now();
875
876        let Some(graceful_deadline) = started.checked_add(graceful_timeout) else {
877            return self.incomplete_outcome();
878        };
879
880        let Some(abort_deadline) = graceful_deadline.checked_add(abort_timeout) else {
881            return self.incomplete_outcome();
882        };
883
884        let lock_timeout = abort_deadline.saturating_duration_since(time::Instant::now());
885
886        let Ok(_drain_lock) = time::timeout(lock_timeout, self.drain_lock.lock()).await else {
887            return self.incomplete_outcome();
888        };
889
890        let reserve_timeout = abort_deadline.saturating_duration_since(time::Instant::now());
891
892        let Ok((slot, abort, abort_requested)) = time::timeout(reserve_timeout, async {
893            loop {
894                if let Some(reservation) = self.state.lock().try_reserve_drain() {
895                    break reservation;
896                }
897
898                nautilus_common::live::dst::task::yield_now().await;
899            }
900        })
901        .await
902        else {
903            return self.incomplete_outcome();
904        };
905
906        if slot.is_none() {
907            let mut state = self.state.lock();
908            self.owned.store(false, Ordering::Release);
909            state.draining = false;
910            return None;
911        }
912
913        let mut draining = SharedTaskDrain { owner: self, slot };
914        let aborting = abort_requested || abort.is_cancelled();
915        if aborting {
916            draining.slot.abort();
917        }
918
919        let graceful_remaining = graceful_deadline.saturating_duration_since(time::Instant::now());
920
921        let outcome = if aborting {
922            let abort_remaining = abort_deadline.saturating_duration_since(time::Instant::now());
923            finish_task(&mut draining.slot, Duration::ZERO, abort_remaining).await
924        } else {
925            let graceful = {
926                let abort_remaining = abort_deadline
927                    .saturating_duration_since(graceful_deadline.max(time::Instant::now()));
928                let finish = finish_task(&mut draining.slot, graceful_remaining, abort_remaining);
929                tokio::pin!(finish);
930                tokio::select! {
931                    biased;
932                    outcome = &mut finish => Some(outcome),
933                    () = abort.cancelled() => None,
934                }
935            };
936
937            if let Some(outcome) = graceful {
938                outcome
939            } else {
940                draining.slot.abort();
941                let abort_remaining =
942                    abort_deadline.saturating_duration_since(time::Instant::now());
943                finish_task(&mut draining.slot, Duration::ZERO, abort_remaining).await
944            }
945        };
946
947        drop(draining);
948        outcome
949    }
950
951    fn incomplete_outcome(&self) -> Option<TaskJoinOutcome<T>> {
952        (!self.is_empty()).then_some(TaskJoinOutcome::Incomplete)
953    }
954}
955
956impl<T> Drop for SharedTaskSlot<T> {
957    fn drop(&mut self) {
958        self.state.get_mut().slot.abort();
959    }
960}
961
962struct SharedTaskDrain<'a, T> {
963    owner: &'a SharedTaskSlot<T>,
964    slot: TaskSlot<T>,
965}
966
967impl<T> Drop for SharedTaskDrain<'_, T> {
968    fn drop(&mut self) {
969        let owns_task = self.slot.is_some();
970        let mut abort_applied = false;
971
972        loop {
973            let mut state = self.owner.state.lock();
974            if owns_task && !abort_applied && (state.abort_requested || state.abort.is_cancelled())
975            {
976                drop(state);
977                self.slot.abort();
978                abort_applied = true;
979                continue;
980            }
981
982            assert!(
983                state.slot.is_none(),
984                "shared task slot changed while draining"
985            );
986            state.slot = std::mem::take(&mut self.slot);
987            state.draining = false;
988            self.owner.owned.store(owns_task, Ordering::Release);
989            break;
990        }
991    }
992}
993
994/// Gracefully joins one task, then aborts and joins it within a second bound.
995///
996/// This preserves typed task results. The handle and forced-abort state remain in `slot` while the
997/// function is pending, so canceling the finish future cannot detach the task or lose its expected
998/// cancellation provenance. A terminal join clears the slot, while a second timeout leaves the
999/// incomplete task in place for another drain attempt.
1000pub async fn finish_task<T>(
1001    slot: &mut TaskSlot<T>,
1002    graceful_timeout: Duration,
1003    abort_timeout: Duration,
1004) -> Option<TaskJoinOutcome<T>> {
1005    let graceful_result = {
1006        let handle = slot.handle.as_mut()?;
1007        time::timeout(graceful_timeout, handle).await
1008    };
1009
1010    match graceful_result {
1011        Ok(result) => Some(slot.complete(result)),
1012        Err(_) => {
1013            slot.abort();
1014
1015            let abort_result = {
1016                let handle = slot.handle.as_mut()?;
1017                time::timeout(abort_timeout, handle).await
1018            };
1019
1020            match abort_result {
1021                Ok(result) => Some(slot.complete(result)),
1022                Err(_) => Some(TaskJoinOutcome::Incomplete),
1023            }
1024        }
1025    }
1026}
1027
1028#[repr(u8)]
1029#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1030enum TaskGroupPhase {
1031    Open,
1032    Closing,
1033    Drained,
1034}
1035
1036impl TaskGroupPhase {
1037    fn load(value: &TaskState) -> Self {
1038        match value.load(Ordering::Acquire) {
1039            0 => Self::Open,
1040            1 => Self::Closing,
1041            2 => Self::Drained,
1042            value => unreachable!("invalid task group phase {value}"),
1043        }
1044    }
1045}
1046
1047#[derive(Debug)]
1048struct TaskGeneration {
1049    phase: TaskState,
1050    admission_lock: parking_lot::Mutex<()>,
1051    cancellation: CancellationToken,
1052    force: CancellationToken,
1053    tasks: TaskTracker,
1054    failures: Mutex<Vec<String>>,
1055}
1056
1057impl TaskGeneration {
1058    fn new() -> Self {
1059        Self {
1060            phase: TaskState::new(TaskGroupPhase::Open as u8),
1061            admission_lock: parking_lot::Mutex::new(()),
1062            cancellation: CancellationToken::new(),
1063            force: CancellationToken::new(),
1064            tasks: TaskTracker::new(),
1065            failures: Mutex::new(Vec::new()),
1066        }
1067    }
1068
1069    fn phase(&self) -> TaskGroupPhase {
1070        TaskGroupPhase::load(&self.phase)
1071    }
1072
1073    fn is_open(&self) -> bool {
1074        self.phase() == TaskGroupPhase::Open
1075    }
1076
1077    fn close_admission(&self) {
1078        let _guard = self.admission_lock.lock();
1079
1080        let _ = self
1081            .phase
1082            .try_update(Ordering::AcqRel, Ordering::Acquire, |phase| match phase {
1083                value
1084                    if value == TaskGroupPhase::Open as u8
1085                        || value == TaskGroupPhase::Drained as u8 =>
1086                {
1087                    Some(TaskGroupPhase::Closing as u8)
1088                }
1089                value if value == TaskGroupPhase::Closing as u8 => None,
1090                value => unreachable!("invalid task group phase {value}"),
1091            });
1092    }
1093
1094    fn cancel(&self) {
1095        self.tasks.close();
1096        self.cancellation.cancel();
1097    }
1098
1099    fn spawn<F>(self: &Arc<Self>, future: F) -> Result<(), TaskSpawnError>
1100    where
1101        F: Future<Output = ()> + Send + 'static,
1102    {
1103        let registration = match self.register_task(None) {
1104            Ok(registration) => registration,
1105            Err(e) => {
1106                drop(future);
1107                return Err(e);
1108            }
1109        };
1110
1111        self.spawn_registered(registration, future);
1112
1113        Ok(())
1114    }
1115
1116    fn spawn_named<F>(
1117        self: &Arc<Self>,
1118        name: &'static str,
1119        future: F,
1120    ) -> Result<TaskRef, TaskSpawnError>
1121    where
1122        F: Future<Output = ()> + Send + 'static,
1123    {
1124        let identity = Arc::new(TaskIdentity::new(name));
1125
1126        let task = TaskRef {
1127            identity: Arc::clone(&identity),
1128        };
1129
1130        let registration = match self.register_task(Some(identity)) {
1131            Ok(registration) => registration,
1132            Err(e) => {
1133                drop(future);
1134                return Err(e);
1135            }
1136        };
1137
1138        self.spawn_registered(registration, future);
1139
1140        Ok(task)
1141    }
1142
1143    fn spawn_registered<F>(&self, registration: TaskRegistration, future: F)
1144    where
1145        F: Future<Output = ()> + Send + 'static,
1146    {
1147        let force = self.force.clone();
1148
1149        spawn(async move {
1150            let result = AssertUnwindSafe(async move {
1151                tokio::select! {
1152                    biased;
1153                    () = force.cancelled() => {}
1154                    () = future => {}
1155                }
1156            })
1157            .catch_unwind()
1158            .await;
1159
1160            registration.complete(result.err());
1161        });
1162    }
1163
1164    fn register_task(
1165        self: &Arc<Self>,
1166        identity: Option<Arc<TaskIdentity>>,
1167    ) -> Result<TaskRegistration, TaskSpawnError> {
1168        let _guard = self.admission_lock.lock();
1169
1170        if !self.is_open() {
1171            return Err(TaskSpawnError::CLOSED);
1172        }
1173
1174        let token = self.tasks.token();
1175        Ok(TaskRegistration::new(Arc::clone(self), token, identity))
1176    }
1177
1178    fn record_failure(&self, failure: String) {
1179        self.lock_failures().push(failure);
1180    }
1181
1182    fn take_failures(&self) -> Vec<String> {
1183        std::mem::take(&mut *self.lock_failures())
1184    }
1185
1186    fn timeout_error(&self) -> TaskShutdownError {
1187        TaskShutdownError::Timeout {
1188            failures: self.lock_failures().clone(),
1189            incomplete: self.tasks.len(),
1190        }
1191    }
1192
1193    fn complete_shutdown(&self) -> Result<(), TaskShutdownError> {
1194        self.phase
1195            .store(TaskGroupPhase::Drained as u8, Ordering::Release);
1196        let failures = self.take_failures();
1197        if failures.is_empty() {
1198            Ok(())
1199        } else {
1200            Err(TaskShutdownError::Join(failures))
1201        }
1202    }
1203
1204    fn lock_failures(&self) -> MutexGuard<'_, Vec<String>> {
1205        self.failures.lock()
1206    }
1207}
1208
1209struct TaskRegistration {
1210    generation: Arc<TaskGeneration>,
1211    _token: TaskTrackerToken,
1212    identity: Option<Arc<TaskIdentity>>,
1213    terminal: bool,
1214}
1215
1216impl TaskRegistration {
1217    fn new(
1218        generation: Arc<TaskGeneration>,
1219        token: TaskTrackerToken,
1220        identity: Option<Arc<TaskIdentity>>,
1221    ) -> Self {
1222        Self {
1223            generation,
1224            _token: token,
1225            identity,
1226            terminal: false,
1227        }
1228    }
1229
1230    fn complete(mut self, panic: Option<Box<dyn Any + Send>>) {
1231        self.terminal = true;
1232        self.finish();
1233
1234        if let Some(panic) = panic {
1235            let failure = format!("panicked: {}", panic_message(panic.as_ref()));
1236            self.record_failure(&failure);
1237        }
1238    }
1239
1240    fn finish(&self) {
1241        if let Some(identity) = &self.identity {
1242            identity.finish();
1243        }
1244    }
1245
1246    fn record_failure(&self, failure: &str) {
1247        let failure = self.identity.as_ref().map_or_else(
1248            || format!("task {failure}"),
1249            |identity| identity.failure(failure),
1250        );
1251        self.generation.record_failure(failure);
1252    }
1253}
1254
1255impl Drop for TaskRegistration {
1256    fn drop(&mut self) {
1257        self.finish();
1258
1259        if !self.terminal && !self.generation.force.is_cancelled() {
1260            self.record_failure("was canceled unexpectedly");
1261        }
1262    }
1263}
1264
1265fn panic_message(payload: &(dyn Any + Send)) -> &str {
1266    if let Some(message) = payload.downcast_ref::<&'static str>() {
1267        message
1268    } else if let Some(message) = payload.downcast_ref::<String>() {
1269        message.as_str()
1270    } else {
1271        "non-string panic payload"
1272    }
1273}
1274
1275#[derive(Debug)]
1276struct TaskGroupInner {
1277    generation: ArcSwap<TaskGeneration>,
1278    generation_lock: parking_lot::Mutex<()>,
1279    drain_lock: tokio::sync::Mutex<()>,
1280}
1281
1282impl TaskGroupInner {
1283    fn current(&self) -> Arc<TaskGeneration> {
1284        self.generation.load_full()
1285    }
1286
1287    fn begin_shutdown(&self) -> Arc<TaskGeneration> {
1288        let generation = {
1289            let _guard = self.generation_lock.lock();
1290            let generation = self.current();
1291            generation.close_admission();
1292            generation
1293        };
1294
1295        generation.cancel();
1296        generation
1297    }
1298
1299    fn start_generation(&self) -> Result<(), TaskGenerationError> {
1300        let _guard = self.generation_lock.lock();
1301        let current = self.current();
1302        if current.phase() != TaskGroupPhase::Drained || !current.tasks.is_empty() {
1303            return Err(TaskGenerationError::NOT_DRAINED);
1304        }
1305
1306        self.generation.store(Arc::new(TaskGeneration::new()));
1307        Ok(())
1308    }
1309}
1310
1311fn spawn_gated<F>(future: F) -> (JoinHandle<F::Output>, tokio::sync::oneshot::Sender<()>)
1312where
1313    F: Future + Send + 'static,
1314    F::Output: Send + 'static,
1315{
1316    let (start, wait) = tokio::sync::oneshot::channel();
1317
1318    let handle = spawn(async move {
1319        wait.await.expect("task start gate sender dropped");
1320        future.await
1321    });
1322
1323    (handle, start)
1324}
1325
1326#[cfg(all(feature = "simulation", madsim))]
1327fn spawn<F>(future: F) -> JoinHandle<F::Output>
1328where
1329    F: Future + Send + 'static,
1330    F::Output: Send + 'static,
1331{
1332    nautilus_common::live::dst::task::spawn(future)
1333}
1334
1335#[cfg(not(all(feature = "simulation", madsim)))]
1336fn spawn<F>(future: F) -> JoinHandle<F::Output>
1337where
1338    F: Future + Send + 'static,
1339    F::Output: Send + 'static,
1340{
1341    nautilus_common::live::get_runtime().spawn(future)
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346    use std::{
1347        pin::Pin,
1348        sync::{
1349            Arc,
1350            atomic::{AtomicBool, AtomicUsize, Ordering},
1351        },
1352        task::{Context, Poll, Wake, Waker},
1353    };
1354
1355    use nautilus_common::live::dst::task;
1356    use rstest::rstest;
1357
1358    use super::*;
1359
1360    const TEST_TIMEOUT: Duration = Duration::from_secs(1);
1361
1362    /// Interval a test waits on while letting an in-flight `finish_*` future make progress.
1363    ///
1364    /// Sleeping rather than yielding is deliberate. A `yield_now` arm leaves the polling task
1365    /// immediately runnable again, so the loop can re-poll without the runtime servicing its
1366    /// timer driver, and a zero-duration `time::timeout` inside the raced future may then not
1367    /// fire promptly. Sleeping yields to the timer driver before the next poll.
1368    #[cfg(not(all(feature = "simulation", madsim)))]
1369    const POLL_INTERVAL: Duration = Duration::from_millis(10);
1370
1371    #[rstest]
1372    fn task_group_guard_closes_all_groups_and_runs_rollback_until_disarmed() {
1373        let first = Arc::new(TaskGroup::new());
1374        let second = Arc::new(TaskGroup::new());
1375        let rolled_back = Arc::new(AtomicBool::new(false));
1376        let first_on_drop = Arc::clone(&first);
1377        let second_on_drop = Arc::clone(&second);
1378        let rolled_back_on_drop = Arc::clone(&rolled_back);
1379
1380        drop(TaskGroupGuard::new(&[&first, &second], move || {
1381            assert!(!first_on_drop.is_open());
1382            assert!(!second_on_drop.is_open());
1383            rolled_back_on_drop.store(true, Ordering::Release);
1384        }));
1385
1386        assert!(!first.is_open());
1387        assert!(!second.is_open());
1388        assert!(rolled_back.load(Ordering::Acquire));
1389
1390        let group = Arc::new(TaskGroup::new());
1391        let rolled_back = Arc::new(AtomicBool::new(false));
1392        let rolled_back_on_drop = Arc::clone(&rolled_back);
1393        TaskGroupGuard::new(&[&group], move || {
1394            rolled_back_on_drop.store(true, Ordering::Release);
1395        })
1396        .disarm();
1397
1398        assert!(group.is_open());
1399        assert!(!rolled_back.load(Ordering::Acquire));
1400    }
1401
1402    #[rstest]
1403    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1404    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1405    async fn task_is_registered_before_first_poll() {
1406        let group = TaskGroup::new();
1407        let observed = Arc::new(AtomicUsize::new(0));
1408        let observed_task = Arc::clone(&observed);
1409        let generation = group.inner.current();
1410
1411        group
1412            .spawn(async move {
1413                observed_task.store(generation.tasks.len(), Ordering::Release);
1414            })
1415            .expect("spawn");
1416
1417        time::timeout(TEST_TIMEOUT, async {
1418            while observed.load(Ordering::Acquire) == 0 {
1419                task::yield_now().await;
1420            }
1421        })
1422        .await
1423        .expect("task should poll");
1424
1425        group.begin_shutdown();
1426        group
1427            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1428            .await
1429            .expect("shutdown");
1430        assert_eq!(observed.load(Ordering::Acquire), 1);
1431    }
1432
1433    #[rstest]
1434    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1435    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1436    async fn named_tasks_expose_distinct_identity_without_transferring_ownership() {
1437        let group = TaskGroup::new();
1438        let first = group
1439            .spawn_named("dispatch", std::future::pending())
1440            .expect("spawn first task");
1441        let second = group
1442            .spawner()
1443            .expect("task spawner")
1444            .spawn_named("dispatch", std::future::pending())
1445            .expect("spawn second task");
1446        let first_observer = first.clone();
1447        drop(first);
1448
1449        assert_eq!(first_observer.name(), "dispatch");
1450        assert_eq!(second.name(), "dispatch");
1451        assert_ne!(first_observer.id(), second.id());
1452        assert!(first_observer.is_active());
1453        assert!(second.is_active());
1454        assert!(!first_observer.is_finished());
1455        assert!(!second.is_finished());
1456        assert_eq!(group.len(), 2);
1457
1458        group.abort();
1459        group
1460            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1461            .await
1462            .expect("shutdown");
1463
1464        assert!(!first_observer.is_active());
1465        assert!(!second.is_active());
1466        assert!(first_observer.is_finished());
1467        assert!(second.is_finished());
1468        assert!(group.is_empty());
1469    }
1470
1471    #[rstest]
1472    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1473    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1474    async fn named_task_ids_are_unique_across_groups_and_generations() {
1475        let first_group = TaskGroup::new();
1476        let first = first_group
1477            .spawn_named("dispatch", std::future::pending())
1478            .expect("spawn first task");
1479        first_group.abort();
1480        first_group
1481            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1482            .await
1483            .expect("first generation shutdown");
1484        first_group
1485            .start_generation()
1486            .expect("replacement generation");
1487
1488        let second = first_group
1489            .spawn_named("dispatch", std::future::pending())
1490            .expect("spawn replacement task");
1491        let second_group = TaskGroup::new();
1492        let third = second_group
1493            .spawn_named("dispatch", std::future::pending())
1494            .expect("spawn other group task");
1495
1496        first_group.abort();
1497        first_group
1498            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1499            .await
1500            .expect("replacement generation shutdown");
1501        second_group.abort();
1502        second_group
1503            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1504            .await
1505            .expect("other group shutdown");
1506
1507        assert_ne!(first.id(), second.id());
1508        assert_ne!(first.id(), third.id());
1509        assert_ne!(second.id(), third.id());
1510    }
1511
1512    #[rstest]
1513    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1514    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1515    async fn dropping_named_task_ref_preserves_group_ownership() {
1516        let group = TaskGroup::new();
1517        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1518        let (request_tx, request_rx) = tokio::sync::oneshot::channel();
1519        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
1520
1521        let task = group
1522            .spawn_named("dispatch", async move {
1523                let _ = started_tx.send(());
1524                let _ = request_rx.await;
1525                let _ = response_tx.send(());
1526                std::future::pending::<()>().await;
1527            })
1528            .expect("spawn task");
1529
1530        started_rx.await.expect("task should start");
1531
1532        drop(task);
1533        request_tx.send(()).expect("task should remain owned");
1534        time::timeout(TEST_TIMEOUT, response_rx)
1535            .await
1536            .expect("task should respond")
1537            .expect("task should remain active");
1538
1539        assert_eq!(group.len(), 1);
1540
1541        group.abort();
1542        group
1543            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1544            .await
1545            .expect("shutdown");
1546
1547        assert!(group.is_empty());
1548    }
1549
1550    #[rstest]
1551    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1552    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1553    async fn named_task_observes_normal_completion() {
1554        let group = TaskGroup::new();
1555        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
1556
1557        let task = group
1558            .spawn_named("finite", async move {
1559                let _ = release_rx.await;
1560            })
1561            .expect("spawn task");
1562
1563        release_tx.send(()).expect("task should be waiting");
1564        time::timeout(TEST_TIMEOUT, async {
1565            while !task.is_finished() {
1566                task::yield_now().await;
1567            }
1568        })
1569        .await
1570        .expect("task should finish");
1571
1572        group.begin_shutdown();
1573        group
1574            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1575            .await
1576            .expect("shutdown");
1577
1578        assert!(!task.is_active());
1579        assert!(task.is_finished());
1580        assert!(group.is_empty());
1581    }
1582
1583    #[rstest]
1584    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1585    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1586    async fn named_task_panic_includes_identity() {
1587        let group = TaskGroup::new();
1588
1589        let task = group
1590            .spawn_named("panicking", async {
1591                panic!("task panic");
1592            })
1593            .expect("spawn task");
1594
1595        group.begin_shutdown();
1596        let error = group
1597            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1598            .await
1599            .expect_err("panic should be reported");
1600
1601        let TaskShutdownError::Join(failures) = error else {
1602            panic!("expected join failure");
1603        };
1604
1605        assert_eq!(
1606            failures,
1607            [format!(
1608                "task 'panicking' ({}) panicked: task panic",
1609                task.id()
1610            )]
1611        );
1612        assert!(task.is_finished());
1613        assert!(group.is_empty());
1614    }
1615
1616    #[rstest]
1617    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1618    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1619    async fn named_task_forced_before_first_poll_becomes_finished() {
1620        let group = TaskGroup::new();
1621        let generation = group.inner.current();
1622        let identity = Arc::new(TaskIdentity::new("not-polled"));
1623
1624        let task = TaskRef {
1625            identity: Arc::clone(&identity),
1626        };
1627
1628        let registration = generation
1629            .register_task(Some(identity))
1630            .expect("registration");
1631        let polled = Arc::new(AtomicBool::new(false));
1632        let polled_task = Arc::clone(&polled);
1633
1634        assert!(task.is_active());
1635        assert!(!task.is_finished());
1636
1637        group.abort();
1638        generation.spawn_registered(registration, async move {
1639            polled_task.store(true, Ordering::Release);
1640        });
1641
1642        group
1643            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1644            .await
1645            .expect("shutdown");
1646
1647        assert!(!polled.load(Ordering::Acquire));
1648        assert!(!task.is_active());
1649        assert!(task.is_finished());
1650        assert!(group.is_empty());
1651    }
1652
1653    #[rstest]
1654    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1655    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1656    async fn named_registration_drop_after_forced_cancellation_is_not_reported() {
1657        let group = TaskGroup::new();
1658        let generation = group.inner.current();
1659        let identity = Arc::new(TaskIdentity::new("canceled"));
1660
1661        let task = TaskRef {
1662            identity: Arc::clone(&identity),
1663        };
1664
1665        let registration = generation
1666            .register_task(Some(identity))
1667            .expect("registration");
1668
1669        group.abort();
1670        drop(registration);
1671        group
1672            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1673            .await
1674            .expect("forced cancellation should not be reported");
1675
1676        assert!(!task.is_active());
1677        assert!(task.is_finished());
1678        assert!(group.is_empty());
1679    }
1680
1681    #[rstest]
1682    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1683    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1684    async fn rejected_named_task_drops_future_without_polling() {
1685        let group = Arc::new(TaskGroup::new());
1686        let dropped = Arc::new(AtomicBool::new(false));
1687        let polled = Arc::new(AtomicBool::new(false));
1688        group.begin_shutdown();
1689
1690        let result = group.spawn_named(
1691            "rejected",
1692            ReentrantDropFuture {
1693                group: Arc::clone(&group),
1694                dropped: Arc::clone(&dropped),
1695                polled: Arc::clone(&polled),
1696            },
1697        );
1698
1699        assert!(matches!(result, Err(TaskSpawnError::CLOSED)));
1700        assert!(dropped.load(Ordering::Acquire));
1701        assert!(!polled.load(Ordering::Acquire));
1702        group
1703            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1704            .await
1705            .expect("shutdown");
1706    }
1707
1708    #[rstest]
1709    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1710    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1711    async fn shutdown_closes_admission_before_future_poll() {
1712        let group = TaskGroup::new();
1713        let polled = Arc::new(AtomicBool::new(false));
1714        let polled_task = Arc::clone(&polled);
1715
1716        group.begin_shutdown();
1717
1718        let result = group.spawn(async move {
1719            polled_task.store(true, Ordering::Release);
1720        });
1721
1722        assert!(matches!(group.spawner(), Err(TaskSpawnError::CLOSED)));
1723        assert_eq!(result, Err(TaskSpawnError::CLOSED));
1724        assert!(!polled.load(Ordering::Acquire));
1725        group
1726            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1727            .await
1728            .expect("shutdown");
1729    }
1730
1731    #[rstest]
1732    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1733    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1734    async fn rejected_future_can_reenter_group_on_drop() {
1735        let group = Arc::new(TaskGroup::new());
1736        let dropped = Arc::new(AtomicBool::new(false));
1737        let polled = Arc::new(AtomicBool::new(false));
1738        group.begin_shutdown();
1739
1740        let result = group.spawn(ReentrantDropFuture {
1741            group: Arc::clone(&group),
1742            dropped: Arc::clone(&dropped),
1743            polled: Arc::clone(&polled),
1744        });
1745
1746        assert_eq!(result, Err(TaskSpawnError::CLOSED));
1747        assert!(dropped.load(Ordering::Acquire));
1748        assert!(!polled.load(Ordering::Acquire));
1749        group
1750            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1751            .await
1752            .expect("shutdown");
1753    }
1754
1755    #[rstest]
1756    fn shutdown_cancellation_wake_can_reenter_group() {
1757        let group = Arc::new(TaskGroup::new());
1758        let cancellation = group.cancellation_token();
1759        let woke = Arc::new(AtomicBool::new(false));
1760
1761        let waker = Waker::from(Arc::new(ReentrantWake {
1762            group: Arc::clone(&group),
1763            woke: Arc::clone(&woke),
1764        }));
1765
1766        let mut context = Context::from_waker(&waker);
1767        let mut cancelled = Box::pin(cancellation.cancelled());
1768
1769        assert!(matches!(
1770            Pin::as_mut(&mut cancelled).poll(&mut context),
1771            Poll::Pending
1772        ));
1773        group.begin_shutdown();
1774
1775        assert!(woke.load(Ordering::Acquire));
1776    }
1777
1778    #[rstest]
1779    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1780    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1781    async fn stale_spawner_cannot_spawn_into_new_generation() {
1782        let group = TaskGroup::new();
1783        let old = group.spawner().expect("old spawner");
1784        let old_generation = Arc::clone(&old.generation);
1785        group.begin_shutdown();
1786        group
1787            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1788            .await
1789            .expect("shutdown");
1790
1791        assert_eq!(old.spawn(async {}), Err(TaskSpawnError::CLOSED));
1792        assert!(matches!(
1793            old.spawn_named("stale", async {}),
1794            Err(TaskSpawnError::CLOSED)
1795        ));
1796        assert!(old_generation.tasks.is_empty());
1797
1798        group.start_generation().expect("new generation");
1799        let current = group.spawner().expect("current spawner");
1800
1801        current
1802            .spawn_named("current", async {})
1803            .expect("current spawn");
1804
1805        group.begin_shutdown();
1806        group
1807            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1808            .await
1809            .expect("shutdown");
1810    }
1811
1812    #[rstest]
1813    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1814    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1815    async fn shutdown_allows_graceful_cancellation_cleanup() {
1816        let group = TaskGroup::new();
1817        let cancellation = group
1818            .spawner()
1819            .expect("task group spawner")
1820            .cancellation_token();
1821        let cleaned = Arc::new(AtomicBool::new(false));
1822        let cleaned_task = Arc::clone(&cleaned);
1823        group
1824            .spawn(async move {
1825                cancellation.cancelled().await;
1826                cleaned_task.store(true, Ordering::Release);
1827            })
1828            .expect("spawn");
1829
1830        group.begin_shutdown();
1831        group
1832            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1833            .await
1834            .expect("shutdown");
1835
1836        assert!(cleaned.load(Ordering::Acquire));
1837        assert!(group.is_empty());
1838    }
1839
1840    #[rstest]
1841    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1842    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1843    async fn shutdown_forces_abort_and_observes_canceled_join() {
1844        let group = TaskGroup::new();
1845        let (future, started_rx, dropped) = pending_with_drop_signal();
1846
1847        group.spawn(future).expect("spawn");
1848        started_rx.await.expect("task should start");
1849
1850        group.begin_shutdown();
1851        group
1852            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
1853            .await
1854            .expect("shutdown");
1855
1856        assert!(dropped.load(Ordering::Acquire));
1857        assert!(group.is_empty());
1858    }
1859
1860    #[rstest]
1861    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1862    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1863    async fn abort_closes_admission_and_cancels_tasks() {
1864        let group = TaskGroup::new();
1865        let dropped = Arc::new(AtomicBool::new(false));
1866        let drop_signal = DropSignal(Arc::clone(&dropped));
1867        group
1868            .spawn(async move {
1869                let _drop_signal = drop_signal;
1870                std::future::pending::<()>().await;
1871            })
1872            .expect("spawn");
1873
1874        group.abort();
1875        group
1876            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1877            .await
1878            .expect("shutdown");
1879
1880        assert!(!group.is_open());
1881        assert!(group.is_empty());
1882        assert!(dropped.load(Ordering::Acquire));
1883    }
1884
1885    #[rstest]
1886    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1887    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1888    async fn dropping_task_group_aborts_owned_tasks() {
1889        let group = TaskGroup::new();
1890        let (future, started_rx, dropped) = pending_with_drop_signal();
1891
1892        group.spawn(future).expect("spawn");
1893        started_rx.await.expect("task should start");
1894
1895        drop(group);
1896        wait_for_drop(&dropped).await;
1897    }
1898
1899    #[rstest]
1900    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1901    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1902    async fn unexpected_task_cancellation_is_reported() {
1903        let group = TaskGroup::new();
1904        group.spawn(std::future::pending()).expect("spawn");
1905        let generation = group.inner.current();
1906        let registration =
1907            TaskRegistration::new(Arc::clone(&generation), generation.tasks.token(), None);
1908        drop(registration);
1909        group.begin_shutdown();
1910
1911        let error = group
1912            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
1913            .await
1914            .expect_err("unexpected cancellation should be reported");
1915
1916        let TaskShutdownError::Join(failures) = error else {
1917            panic!("expected join failure");
1918        };
1919
1920        assert_eq!(failures, ["task was canceled unexpectedly"]);
1921        assert!(group.is_empty());
1922    }
1923
1924    #[rstest]
1925    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1926    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1927    async fn named_unexpected_cancellation_includes_identity() {
1928        let group = TaskGroup::new();
1929        group.spawn(std::future::pending()).expect("spawn");
1930        let generation = group.inner.current();
1931        let identity = Arc::new(TaskIdentity::new("canceled"));
1932
1933        let task = TaskRef {
1934            identity: Arc::clone(&identity),
1935        };
1936
1937        let registration = generation
1938            .register_task(Some(identity))
1939            .expect("registration");
1940        drop(registration);
1941        group.begin_shutdown();
1942
1943        let error = group
1944            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
1945            .await
1946            .expect_err("unexpected cancellation should be reported");
1947
1948        let TaskShutdownError::Join(failures) = error else {
1949            panic!("expected join failure");
1950        };
1951
1952        assert_eq!(
1953            failures,
1954            [format!(
1955                "task 'canceled' ({}) was canceled unexpectedly",
1956                task.id()
1957            )]
1958        );
1959        assert!(task.is_finished());
1960        assert!(group.is_empty());
1961    }
1962
1963    #[rstest]
1964    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1965    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1966    async fn late_child_is_rejected_after_parent_observes_shutdown() {
1967        let group = TaskGroup::new();
1968        let spawner = group.spawner().expect("spawner");
1969        let cancellation = spawner.cancellation_token();
1970        let (result_tx, result_rx) = tokio::sync::oneshot::channel();
1971        group
1972            .spawn(async move {
1973                cancellation.cancelled().await;
1974
1975                let _ = result_tx.send(spawner.spawn(async {}));
1976            })
1977            .expect("spawn parent");
1978
1979        group.begin_shutdown();
1980        group
1981            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1982            .await
1983            .expect("shutdown");
1984
1985        assert_eq!(
1986            result_rx.await.expect("late spawn result"),
1987            Err(TaskSpawnError::CLOSED),
1988        );
1989    }
1990
1991    #[rstest]
1992    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1993    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1994    async fn shutdown_drains_accepted_registration_and_rejects_late_registration() {
1995        let group = TaskGroup::new();
1996        let generation = group.inner.current();
1997        let registration = generation.register_task(None).expect("registration");
1998
1999        group.begin_shutdown();
2000        assert!(matches!(
2001            generation.register_task(None),
2002            Err(TaskSpawnError::CLOSED)
2003        ));
2004        assert_eq!(group.len(), 1);
2005
2006        let error = group
2007            .finish_shutdown(Duration::ZERO, Duration::ZERO)
2008            .await
2009            .expect_err("accepted registration should prevent completed shutdown");
2010        assert!(matches!(
2011            error,
2012            TaskShutdownError::Timeout { incomplete: 1, .. }
2013        ));
2014        assert_eq!(
2015            group.start_generation(),
2016            Err(TaskGenerationError::NOT_DRAINED)
2017        );
2018
2019        registration.complete(None);
2020        group
2021            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2022            .await
2023            .expect("retry shutdown");
2024        group.start_generation().expect("new generation");
2025    }
2026
2027    #[rstest]
2028    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2029    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2030    async fn generation_cannot_reopen_before_finish_shutdown() {
2031        let group = TaskGroup::new();
2032        group.begin_shutdown();
2033
2034        assert!(matches!(
2035            group.start_generation(),
2036            Err(TaskGenerationError::NOT_DRAINED),
2037        ));
2038
2039        group
2040            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2041            .await
2042            .expect("shutdown");
2043        group.start_generation().expect("new generation");
2044    }
2045
2046    #[rstest]
2047    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2048    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2049    async fn shutdown_after_drain_requires_another_finish_before_reopening() {
2050        let group = TaskGroup::new();
2051        group.begin_shutdown();
2052        group
2053            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2054            .await
2055            .expect("initial shutdown");
2056
2057        group.begin_shutdown();
2058
2059        assert_eq!(
2060            group.start_generation(),
2061            Err(TaskGenerationError::NOT_DRAINED),
2062        );
2063
2064        group
2065            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2066            .await
2067            .expect("repeated shutdown");
2068        group.start_generation().expect("new generation");
2069    }
2070
2071    #[rstest]
2072    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2073    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2074    async fn canceled_finish_preserves_tracked_task() {
2075        let group = Arc::new(TaskGroup::new());
2076        group.spawn(std::future::pending()).expect("spawn");
2077        group.begin_shutdown();
2078        let finishing_group = Arc::clone(&group);
2079
2080        let finish = task::spawn(async move {
2081            finishing_group
2082                .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2083                .await
2084        });
2085
2086        time::timeout(TEST_TIMEOUT, async {
2087            while group.inner.drain_lock.try_lock().is_ok() {
2088                task::yield_now().await;
2089            }
2090        })
2091        .await
2092        .expect("finisher should begin draining");
2093
2094        finish.abort();
2095        let _ = finish.await;
2096
2097        assert_eq!(group.len(), 1);
2098        assert!(!group.is_empty());
2099
2100        group
2101            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2102            .await
2103            .expect("retry shutdown");
2104    }
2105
2106    #[cfg(not(all(feature = "simulation", madsim)))]
2107    #[rstest]
2108    #[tokio::test]
2109    async fn canceled_finish_preserves_observed_join_failures() {
2110        let group = TaskGroup::new();
2111        let (panicking_tx, panicking_rx) = tokio::sync::oneshot::channel();
2112        group
2113            .spawn(async move {
2114                let _ = panicking_tx.send(());
2115                panic!("task panic");
2116            })
2117            .expect("spawn panicking task");
2118
2119        panicking_rx.await.expect("panicking task should start");
2120        group
2121            .spawn(std::future::pending())
2122            .expect("spawn pending task");
2123        group.begin_shutdown();
2124
2125        loop {
2126            let finish = group.finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT);
2127            tokio::pin!(finish);
2128            tokio::select! {
2129                biased;
2130                outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
2131                () = task::yield_now() => {}
2132            }
2133
2134            if group.inner.current().lock_failures().len() == 1 {
2135                break;
2136            }
2137        }
2138
2139        let error = group
2140            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2141            .await
2142            .expect_err("panic should remain observable");
2143
2144        let TaskShutdownError::Join(failures) = error else {
2145            panic!("expected join failure");
2146        };
2147
2148        assert_eq!(failures.len(), 1);
2149        assert!(group.is_empty());
2150    }
2151
2152    #[cfg(not(all(feature = "simulation", madsim)))]
2153    #[rstest]
2154    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2155    async fn canceled_finish_preserves_forced_abort_classification() {
2156        let group = TaskGroup::new();
2157        let generation = group.inner.current();
2158        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2159        let (release_tx, release_rx) = std::sync::mpsc::channel();
2160        group
2161            .spawn(async move {
2162                let _ = started_tx.send(());
2163                let _ = release_rx.recv();
2164                task::yield_now().await;
2165            })
2166            .expect("spawn blocking task");
2167
2168        started_rx.await.expect("blocking task should start");
2169        group.begin_shutdown();
2170
2171        loop {
2172            {
2173                let finish = group.finish_shutdown(Duration::ZERO, TEST_TIMEOUT);
2174                tokio::pin!(finish);
2175                tokio::select! {
2176                    biased;
2177                    outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
2178                    () = time::sleep(POLL_INTERVAL) => {}
2179                }
2180            }
2181
2182            if generation.force.is_cancelled() {
2183                break;
2184            }
2185        }
2186
2187        assert!(generation.force.is_cancelled());
2188        release_tx
2189            .send(())
2190            .expect("blocking task should be waiting");
2191        group
2192            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2193            .await
2194            .expect("retry shutdown");
2195        assert!(group.is_empty());
2196    }
2197
2198    #[cfg(not(all(feature = "simulation", madsim)))]
2199    #[rstest]
2200    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2201    async fn concurrent_finish_respects_its_own_shutdown_bound() {
2202        let group = Arc::new(TaskGroup::new());
2203        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2204        let (release_tx, release_rx) = std::sync::mpsc::channel();
2205        group
2206            .spawn(async move {
2207                let _ = started_tx.send(());
2208                let _ = release_rx.recv();
2209            })
2210            .expect("spawn blocking task");
2211
2212        started_rx.await.expect("blocking task should start");
2213        group.begin_shutdown();
2214        let finishing_group = Arc::clone(&group);
2215
2216        let finish = task::spawn(async move {
2217            finishing_group
2218                .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2219                .await
2220        });
2221
2222        time::timeout(TEST_TIMEOUT, async {
2223            while group.inner.drain_lock.try_lock().is_ok() {
2224                task::yield_now().await;
2225            }
2226        })
2227        .await
2228        .expect("first finisher should hold the drain lock");
2229
2230        let error = group
2231            .finish_shutdown(Duration::ZERO, Duration::ZERO)
2232            .await
2233            .expect_err("second finisher should exhaust its own bound");
2234
2235        let TaskShutdownError::Timeout { incomplete, .. } = error else {
2236            panic!("expected shutdown timeout");
2237        };
2238
2239        assert_eq!(incomplete, 1);
2240
2241        release_tx
2242            .send(())
2243            .expect("blocking task should be waiting");
2244        finish
2245            .await
2246            .expect("first finisher should join")
2247            .expect("first shutdown should complete");
2248    }
2249
2250    #[rstest]
2251    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2252    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2253    async fn queued_finish_remains_bound_to_original_generation() {
2254        let group = TaskGroup::new();
2255        group.begin_shutdown();
2256        let generation = group.inner.current();
2257        let drain_lock = group.inner.drain_lock.lock().await;
2258        let mut finish = Box::pin(group.finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT));
2259
2260        assert!(matches!(
2261            futures_util::poll!(finish.as_mut()),
2262            Poll::Pending,
2263        ));
2264
2265        generation
2266            .complete_shutdown()
2267            .expect("original generation should drain");
2268        group.start_generation().expect("replacement generation");
2269        drop(drain_lock);
2270
2271        finish
2272            .await
2273            .expect("queued finish should observe the original generation");
2274        assert!(group.is_open());
2275    }
2276
2277    #[cfg(not(all(feature = "simulation", madsim)))]
2278    #[rstest]
2279    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2280    async fn forced_timeout_retains_task_and_in_flight_registration() {
2281        let group = Arc::new(TaskGroup::new());
2282        let generation = group.inner.current();
2283        let registration =
2284            TaskRegistration::new(Arc::clone(&generation), generation.tasks.token(), None);
2285        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2286        let (release_tx, release_rx) = std::sync::mpsc::channel();
2287        group
2288            .spawn(async move {
2289                let _ = started_tx.send(());
2290                let _ = release_rx.recv();
2291            })
2292            .expect("spawn blocking task");
2293
2294        started_rx.await.expect("blocking task should start");
2295        group.begin_shutdown();
2296
2297        let finishing_group = Arc::clone(&group);
2298
2299        let finish = task::spawn(async move {
2300            finishing_group
2301                .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2302                .await
2303        });
2304
2305        time::timeout(TEST_TIMEOUT, async {
2306            while !generation.force.is_cancelled() {
2307                task::yield_now().await;
2308            }
2309        })
2310        .await
2311        .expect("finisher should request forced cancellation");
2312
2313        let error = finish
2314            .await
2315            .expect("finisher should join")
2316            .expect_err("blocking task should exceed abort deadline");
2317
2318        let TaskShutdownError::Timeout { incomplete, .. } = error else {
2319            panic!("expected shutdown timeout");
2320        };
2321
2322        assert_eq!(incomplete, 2);
2323        assert_eq!(group.len(), 2);
2324
2325        release_tx.send(()).expect("release blocking task");
2326        registration.complete(None);
2327        group
2328            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2329            .await
2330            .expect("retry shutdown");
2331        assert!(group.is_empty());
2332    }
2333
2334    #[cfg(not(all(feature = "simulation", madsim)))]
2335    #[rstest]
2336    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2337    async fn stuck_head_does_not_hide_later_join_failure() {
2338        let group = TaskGroup::new();
2339        let generation = group.inner.current();
2340        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2341        let (release_tx, release_rx) = std::sync::mpsc::channel();
2342        group
2343            .spawn(async move {
2344                let _ = started_tx.send(());
2345                let _ = release_rx.recv();
2346            })
2347            .expect("spawn blocking task");
2348
2349        started_rx.await.expect("blocking task should start");
2350        group
2351            .spawn(async {
2352                panic!("later task panic");
2353            })
2354            .expect("spawn panicking task");
2355
2356        time::timeout(TEST_TIMEOUT, async {
2357            while generation.lock_failures().is_empty() {
2358                task::yield_now().await;
2359            }
2360        })
2361        .await
2362        .expect("panicking task should finish");
2363
2364        group.begin_shutdown();
2365
2366        let error = group
2367            .finish_shutdown(Duration::ZERO, Duration::from_millis(10))
2368            .await
2369            .expect_err("blocking task should exceed abort deadline");
2370
2371        let TaskShutdownError::Timeout {
2372            failures,
2373            incomplete,
2374        } = error
2375        else {
2376            panic!("expected shutdown timeout");
2377        };
2378
2379        assert_eq!(failures.len(), 1);
2380        assert!(failures[0].contains("later task panic"));
2381        assert_eq!(incomplete, 1);
2382        assert_eq!(group.len(), 1);
2383
2384        release_tx.send(()).expect("release blocking task");
2385        group
2386            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2387            .await
2388            .expect("retry shutdown");
2389        assert!(group.is_empty());
2390    }
2391
2392    #[rstest]
2393    #[case(Duration::MAX, Duration::ZERO)]
2394    #[case(Duration::ZERO, Duration::MAX)]
2395    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2396    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2397    async fn unrepresentable_deadline_retains_owned_task(
2398        #[case] graceful_timeout: Duration,
2399        #[case] abort_timeout: Duration,
2400    ) {
2401        let group = TaskGroup::new();
2402        group.spawn(std::future::pending()).expect("spawn");
2403        group.begin_shutdown();
2404
2405        let error = group
2406            .finish_shutdown(graceful_timeout, abort_timeout)
2407            .await
2408            .expect_err("deadline should be rejected");
2409
2410        assert!(matches!(
2411            error,
2412            TaskShutdownError::Timeout { incomplete: 1, .. }
2413        ));
2414        assert_eq!(group.len(), 1);
2415    }
2416
2417    #[rstest]
2418    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2419    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2420    async fn unrepresentable_deadline_reports_open_group() {
2421        let group = TaskGroup::new();
2422
2423        let error = group
2424            .finish_shutdown(Duration::MAX, Duration::ZERO)
2425            .await
2426            .expect_err("open group should reject shutdown completion");
2427
2428        assert!(matches!(error, TaskShutdownError::StillOpen));
2429    }
2430
2431    #[cfg(not(all(feature = "simulation", madsim)))]
2432    #[rstest]
2433    #[tokio::test]
2434    async fn panic_join_is_reported_after_group_drains() {
2435        let group = TaskGroup::new();
2436        group
2437            .spawn(async {
2438                panic!("task panic");
2439            })
2440            .expect("spawn");
2441
2442        group.begin_shutdown();
2443
2444        let error = group
2445            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2446            .await
2447            .expect_err("panic should be reported");
2448
2449        let TaskShutdownError::Join(failures) = error else {
2450            panic!("expected join failure");
2451        };
2452
2453        assert_eq!(failures, ["task panicked: task panic"]);
2454        assert!(group.is_empty());
2455        group.start_generation().expect("group should reopen");
2456    }
2457
2458    #[rstest]
2459    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2460    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2461    async fn shared_task_spawn_reports_finished_and_preserves_typed_result() {
2462        let slot = SharedTaskSlot::new();
2463        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2464        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
2465
2466        slot.spawn(async move {
2467            let _ = started_tx.send(());
2468            let _ = release_rx.await;
2469            42
2470        })
2471        .expect("spawn");
2472
2473        started_rx.await.expect("task should start");
2474
2475        assert!(!slot.is_finished());
2476        release_tx.send(()).expect("task should be waiting");
2477
2478        time::timeout(TEST_TIMEOUT, async {
2479            while !slot.is_finished() {
2480                task::yield_now().await;
2481            }
2482        })
2483        .await
2484        .expect("task should finish");
2485
2486        assert!(!slot.is_empty());
2487        assert!(slot.is_finished());
2488        let outcome = slot
2489            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2490            .await
2491            .expect("task should be present");
2492
2493        let TaskJoinOutcome::Completed(value) = outcome else {
2494            panic!("expected completed task");
2495        };
2496
2497        assert_eq!(value, 42);
2498        assert!(slot.is_empty());
2499    }
2500
2501    #[rstest]
2502    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2503    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2504    async fn shared_task_abort_interrupts_active_finish() {
2505        let slot = Arc::new(SharedTaskSlot::new());
2506        slot.insert(task::spawn(std::future::pending::<u32>()));
2507        let finishing_slot = Arc::clone(&slot);
2508        let finish =
2509            task::spawn(async move { finishing_slot.finish(TEST_TIMEOUT, TEST_TIMEOUT).await });
2510
2511        while !slot.state.lock().draining {
2512            task::yield_now().await;
2513        }
2514
2515        slot.abort();
2516
2517        let outcome = time::timeout(TEST_TIMEOUT, finish)
2518            .await
2519            .expect("abort should wake the active finish")
2520            .expect("finisher should join")
2521            .expect("task should be present");
2522        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2523        assert!(slot.is_empty());
2524    }
2525
2526    #[rstest]
2527    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2528    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2529    async fn concurrent_shared_finish_respects_its_own_bound() {
2530        let slot = Arc::new(SharedTaskSlot::new());
2531        slot.insert(task::spawn(std::future::pending::<u32>()));
2532        let finishing_slot = Arc::clone(&slot);
2533
2534        let finish = task::spawn(async move {
2535            finishing_slot
2536                .finish(Duration::from_secs(10), TEST_TIMEOUT)
2537                .await
2538        });
2539
2540        time::timeout(TEST_TIMEOUT, async {
2541            while slot.drain_lock.try_lock().is_ok() {
2542                task::yield_now().await;
2543            }
2544        })
2545        .await
2546        .expect("first finisher should hold the drain lock");
2547
2548        let second = time::timeout(TEST_TIMEOUT, slot.finish(Duration::ZERO, Duration::ZERO)).await;
2549        slot.abort();
2550        let first = time::timeout(TEST_TIMEOUT, finish)
2551            .await
2552            .expect("abort should wake the first finisher")
2553            .expect("first finisher should join")
2554            .expect("task should be present");
2555        let second = second
2556            .expect("second finisher should respect its own bound")
2557            .expect("task should remain owned");
2558
2559        assert!(matches!(first, TaskJoinOutcome::Aborted));
2560        assert!(matches!(second, TaskJoinOutcome::Incomplete));
2561        assert!(slot.is_empty());
2562    }
2563
2564    #[rstest]
2565    #[case(Duration::MAX, Duration::ZERO)]
2566    #[case(Duration::ZERO, Duration::MAX)]
2567    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2568    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2569    async fn unrepresentable_shared_deadline_retains_owned_task(
2570        #[case] graceful_timeout: Duration,
2571        #[case] abort_timeout: Duration,
2572    ) {
2573        let slot = SharedTaskSlot::new();
2574        slot.insert(task::spawn(std::future::pending::<()>()));
2575
2576        let outcome = slot
2577            .finish(graceful_timeout, abort_timeout)
2578            .await
2579            .expect("task should remain present");
2580
2581        assert!(matches!(outcome, TaskJoinOutcome::Incomplete));
2582        assert!(!slot.is_empty());
2583
2584        slot.abort();
2585        let outcome = slot
2586            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2587            .await
2588            .expect("task should remain present");
2589        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2590        assert!(slot.is_empty());
2591    }
2592
2593    #[rstest]
2594    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2595    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2596    async fn shared_task_abort_does_not_leak_into_next_generation() {
2597        let slot = SharedTaskSlot::new();
2598        slot.insert(task::spawn(std::future::pending::<u32>()));
2599        slot.abort();
2600        let first = slot
2601            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2602            .await
2603            .expect("first task");
2604        assert!(matches!(first, TaskJoinOutcome::Aborted));
2605
2606        slot.insert(task::spawn(async { 42 }));
2607        let second = slot
2608            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2609            .await
2610            .expect("second task");
2611
2612        let TaskJoinOutcome::Completed(value) = second else {
2613            panic!("expected completed second-generation task");
2614        };
2615
2616        assert_eq!(value, 42);
2617        assert!(slot.is_empty());
2618    }
2619
2620    #[rstest]
2621    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2622    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2623    async fn canceled_shared_finish_restores_owned_task() {
2624        let slot = Arc::new(SharedTaskSlot::new());
2625        slot.insert(task::spawn(std::future::pending::<()>()));
2626        let finishing_slot = Arc::clone(&slot);
2627        let finish =
2628            task::spawn(async move { finishing_slot.finish(TEST_TIMEOUT, TEST_TIMEOUT).await });
2629
2630        while !slot.state.lock().draining {
2631            task::yield_now().await;
2632        }
2633
2634        finish.abort();
2635        let _ = finish.await;
2636
2637        assert!(!slot.is_empty());
2638        let outcome = slot
2639            .finish(Duration::ZERO, TEST_TIMEOUT)
2640            .await
2641            .expect("restored task");
2642        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2643        assert!(slot.is_empty());
2644    }
2645
2646    #[cfg(not(all(feature = "simulation", madsim)))]
2647    #[rstest]
2648    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2649    async fn shared_task_timeout_preserves_owned_typed_result() {
2650        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2651        let (release_tx, release_rx) = std::sync::mpsc::channel();
2652        let slot = SharedTaskSlot::new();
2653        slot.insert(tokio::task::spawn_blocking(move || {
2654            let _ = started_tx.send(());
2655            let _ = release_rx.recv();
2656            42
2657        }));
2658
2659        started_rx.await.expect("blocking task should start");
2660
2661        let outcome = slot
2662            .finish(Duration::ZERO, Duration::ZERO)
2663            .await
2664            .expect("task should be present");
2665
2666        if !matches!(outcome, TaskJoinOutcome::Incomplete) {
2667            let _ = release_tx.send(());
2668            panic!("expected incomplete task, was {outcome:?}");
2669        }
2670
2671        assert!(!slot.is_empty());
2672
2673        release_tx
2674            .send(())
2675            .expect("blocking task should be waiting");
2676        let outcome = slot
2677            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2678            .await
2679            .expect("task should be present");
2680
2681        let TaskJoinOutcome::Completed(value) = outcome else {
2682            panic!("expected completed task after retry, was {outcome:?}");
2683        };
2684
2685        assert_eq!(value, 42);
2686        assert!(slot.is_empty());
2687    }
2688
2689    #[rstest]
2690    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2691    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2692    async fn shared_task_rejects_insertion_during_empty_drain_reservation() {
2693        let slot = SharedTaskSlot::new();
2694        let (reserved, _, _) = slot
2695            .state
2696            .lock()
2697            .try_reserve_drain()
2698            .expect("empty slot should reserve");
2699
2700        let candidate = TaskSlot::from_handle(task::spawn(async {}));
2701
2702        let rejected = slot
2703            .try_insert_slot(candidate)
2704            .expect_err("draining slot should reject insertion");
2705
2706        assert!(rejected.is_some());
2707        assert!(slot.is_empty());
2708        let mut state = slot.state.lock();
2709        state.slot = reserved;
2710        state.draining = false;
2711    }
2712
2713    #[rstest]
2714    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2715    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2716    async fn shared_task_try_insert_returns_task_when_occupied() {
2717        let slot = SharedTaskSlot::new();
2718        slot.insert(task::spawn(std::future::pending::<()>()));
2719        let candidate = TaskSlot::from_handle(task::spawn(async {}));
2720
2721        let mut rejected = slot
2722            .try_insert_slot(candidate)
2723            .expect_err("occupied slot should reject insertion");
2724        let outcome = finish_task(&mut rejected, TEST_TIMEOUT, TEST_TIMEOUT)
2725            .await
2726            .expect("rejected task should remain present");
2727
2728        assert!(matches!(outcome, TaskJoinOutcome::Completed(())));
2729        assert!(rejected.is_none());
2730        assert!(!slot.is_empty());
2731
2732        slot.abort();
2733        let outcome = slot
2734            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2735            .await
2736            .expect("original task should remain present");
2737        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2738        assert!(slot.is_empty());
2739    }
2740
2741    #[rstest]
2742    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2743    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2744    async fn shared_task_insert_aborts_rejected_task_before_panicking() {
2745        let slot = SharedTaskSlot::new();
2746        slot.insert(task::spawn(std::future::pending::<()>()));
2747        let (future, started_rx, dropped) = pending_with_drop_signal();
2748
2749        let candidate = task::spawn(future);
2750        started_rx.await.expect("candidate task should start");
2751
2752        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| slot.insert(candidate)))
2753            .expect_err("occupied slot should panic");
2754        wait_for_drop(&dropped).await;
2755
2756        assert_eq!(
2757            panic_message(panic.as_ref()),
2758            "shared task slot is already occupied",
2759        );
2760        assert!(!slot.is_empty());
2761        slot.abort();
2762        let outcome = slot
2763            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2764            .await
2765            .expect("original task should be present");
2766        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2767        assert!(slot.is_empty());
2768    }
2769
2770    #[rstest]
2771    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2772    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2773    async fn dropping_shared_task_slot_aborts_owned_task() {
2774        let slot = SharedTaskSlot::new();
2775        let (future, started_rx, dropped) = pending_with_drop_signal();
2776
2777        slot.insert(task::spawn(future));
2778        started_rx.await.expect("task should start");
2779
2780        drop(slot);
2781        wait_for_drop(&dropped).await;
2782    }
2783
2784    #[rstest]
2785    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2786    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2787    async fn singular_task_preserves_typed_result() {
2788        let mut slot = TaskSlot::new();
2789        slot.spawn(async { 42 }).expect("spawn");
2790        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2791            .await
2792            .expect("task present");
2793
2794        let TaskJoinOutcome::Completed(value) = outcome else {
2795            panic!("expected completed task");
2796        };
2797
2798        assert_eq!(value, 42);
2799        assert!(slot.is_none());
2800    }
2801
2802    #[rstest]
2803    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2804    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2805    async fn task_slot_insert_aborts_rejected_task_before_panicking() {
2806        let mut slot = TaskSlot::from_handle(task::spawn(std::future::pending::<()>()));
2807        let (future, started_rx, dropped) = pending_with_drop_signal();
2808
2809        let candidate = task::spawn(future);
2810        started_rx.await.expect("candidate task should start");
2811
2812        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| slot.insert(candidate)))
2813            .expect_err("occupied slot should panic");
2814        wait_for_drop(&dropped).await;
2815
2816        assert_eq!(
2817            panic_message(panic.as_ref()),
2818            "task slot is already occupied"
2819        );
2820        assert!(slot.is_some());
2821        slot.abort();
2822        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2823            .await
2824            .expect("original task should be present");
2825        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2826        assert!(slot.is_none());
2827    }
2828
2829    #[rstest]
2830    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2831    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2832    async fn task_slot_spawn_rejects_occupied_slot_without_detaching_original() {
2833        let dropped = Arc::new(AtomicBool::new(false));
2834        let dropped_task = Arc::clone(&dropped);
2835        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2836        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
2837
2838        let mut slot = TaskSlot::from_handle(task::spawn(async move {
2839            let _drop = DropSignal(dropped_task);
2840            let _ = started_tx.send(());
2841            let _ = release_rx.await;
2842            42
2843        }));
2844
2845        started_rx.await.expect("original task should start");
2846
2847        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| {
2848            slot.spawn(std::future::pending::<u32>())
2849        }));
2850
2851        let _ = release_tx.send(());
2852        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2853            .await
2854            .expect("task should remain present");
2855        wait_for_drop(&dropped).await;
2856        let panic = panic.expect_err("occupied slot should panic");
2857
2858        let TaskJoinOutcome::Completed(value) = outcome else {
2859            panic!("expected original task to complete, was {outcome:?}");
2860        };
2861
2862        assert_eq!(
2863            panic_message(panic.as_ref()),
2864            "task slot is already occupied"
2865        );
2866        assert_eq!(value, 42);
2867        assert!(dropped.load(Ordering::Acquire));
2868        assert!(slot.is_none());
2869    }
2870
2871    #[rstest]
2872    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2873    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2874    async fn dropping_task_slot_aborts_owned_task() {
2875        let (future, started_rx, dropped) = pending_with_drop_signal();
2876
2877        let slot = TaskSlot::from_handle(task::spawn(future));
2878        started_rx.await.expect("task should start");
2879
2880        drop(slot);
2881        wait_for_drop(&dropped).await;
2882    }
2883
2884    #[rstest]
2885    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2886    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2887    async fn singular_task_is_joined_after_forced_abort() {
2888        let dropped = Arc::new(AtomicBool::new(false));
2889        let signal = DropSignal(Arc::clone(&dropped));
2890
2891        let mut slot = TaskSlot::from_handle(task::spawn(async move {
2892            let _signal = signal;
2893            std::future::pending::<()>().await;
2894        }));
2895
2896        let outcome = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT)
2897            .await
2898            .expect("task present");
2899
2900        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2901        assert!(dropped.load(Ordering::Acquire));
2902        assert!(slot.is_none());
2903    }
2904
2905    #[cfg(not(all(feature = "simulation", madsim)))]
2906    #[rstest]
2907    #[tokio::test(start_paused = true)]
2908    async fn singular_task_joins_after_abort_with_paused_time() {
2909        let mut slot = TaskSlot::from_handle(task::spawn(std::future::pending::<()>()));
2910
2911        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2912            .await
2913            .expect("task present");
2914
2915        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2916        assert!(slot.is_none());
2917    }
2918
2919    #[cfg(not(all(feature = "simulation", madsim)))]
2920    #[rstest]
2921    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2922    async fn singular_task_timeout_preserves_owned_typed_result() {
2923        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2924        let (release_tx, release_rx) = std::sync::mpsc::channel();
2925
2926        let mut slot = TaskSlot::from_handle(tokio::task::spawn_blocking(move || {
2927            let _ = started_tx.send(());
2928            let _ = release_rx.recv();
2929            42
2930        }));
2931
2932        started_rx.await.expect("blocking task should start");
2933
2934        let outcome = finish_task(&mut slot, Duration::ZERO, Duration::ZERO)
2935            .await
2936            .expect("task present");
2937
2938        if !matches!(outcome, TaskJoinOutcome::Incomplete) {
2939            let _ = release_tx.send(());
2940            panic!("expected incomplete task, was {outcome:?}");
2941        }
2942
2943        assert!(slot.is_some());
2944
2945        release_tx
2946            .send(())
2947            .expect("blocking task should be waiting");
2948        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2949            .await
2950            .expect("task present");
2951
2952        let value = match outcome {
2953            TaskJoinOutcome::Completed(value) => value,
2954            other => panic!("expected completed task after retry, was {other:?}"),
2955        };
2956
2957        assert_eq!(value, 42);
2958        assert!(slot.is_none());
2959    }
2960
2961    #[cfg(not(all(feature = "simulation", madsim)))]
2962    #[rstest]
2963    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2964    async fn singular_task_retry_preserves_forced_abort_classification() {
2965        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2966        let (release_tx, release_rx) = std::sync::mpsc::channel();
2967
2968        let mut slot = TaskSlot::from_handle(task::spawn(async move {
2969            let _ = started_tx.send(());
2970            let _ = release_rx.recv();
2971            task::yield_now().await;
2972        }));
2973
2974        started_rx.await.expect("blocking task should start");
2975
2976        let outcome = finish_task(&mut slot, Duration::ZERO, Duration::ZERO)
2977            .await
2978            .expect("task present");
2979        assert!(matches!(outcome, TaskJoinOutcome::Incomplete));
2980        assert!(slot.is_some());
2981
2982        release_tx
2983            .send(())
2984            .expect("blocking task should be waiting");
2985        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2986            .await
2987            .expect("task present");
2988
2989        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2990        assert!(slot.is_none());
2991    }
2992
2993    #[rstest]
2994    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2995    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2996    async fn singular_task_reports_unexpected_cancellation() {
2997        let handle = task::spawn(std::future::pending::<()>());
2998        handle.abort();
2999        let mut slot = TaskSlot::from_handle(handle);
3000
3001        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3002            .await
3003            .expect("task present");
3004
3005        let TaskJoinOutcome::Failed(error) = outcome else {
3006            panic!("expected failed task");
3007        };
3008
3009        assert!(error.is_cancelled());
3010        assert!(slot.is_none());
3011    }
3012
3013    #[cfg(not(all(feature = "simulation", madsim)))]
3014    #[rstest]
3015    #[tokio::test]
3016    async fn singular_task_reports_panicked_join() {
3017        let mut slot = TaskSlot::from_handle(task::spawn(async {
3018            panic!("task panic");
3019        }));
3020
3021        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3022            .await
3023            .expect("task should be present");
3024
3025        let TaskJoinOutcome::Failed(error) = outcome else {
3026            panic!("expected failed task");
3027        };
3028
3029        assert!(error.is_panic());
3030        assert!(slot.is_none());
3031    }
3032
3033    #[rstest]
3034    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
3035    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
3036    async fn canceled_singular_finish_preserves_owner_slot() {
3037        let mut slot = TaskSlot::from_handle(task::spawn(std::future::pending::<()>()));
3038
3039        {
3040            let finish = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT);
3041            tokio::pin!(finish);
3042            tokio::select! {
3043                outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
3044                () = task::yield_now() => {}
3045            }
3046        }
3047
3048        assert!(slot.is_some());
3049        let outcome = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT)
3050            .await
3051            .expect("task present");
3052        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
3053        assert!(slot.is_none());
3054    }
3055
3056    #[cfg(not(all(feature = "simulation", madsim)))]
3057    #[rstest]
3058    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3059    async fn canceled_singular_abort_wait_preserves_owner_slot() {
3060        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3061        let (release_tx, release_rx) = std::sync::mpsc::channel();
3062
3063        let mut slot = TaskSlot::from_handle(tokio::task::spawn_blocking(move || {
3064            let _ = started_tx.send(());
3065            let _ = release_rx.recv();
3066            42
3067        }));
3068
3069        started_rx.await.expect("blocking task should start");
3070
3071        {
3072            let finish = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT);
3073            tokio::pin!(finish);
3074            tokio::select! {
3075                outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
3076                () = task::yield_now() => {}
3077            }
3078        }
3079
3080        assert!(slot.is_some());
3081        release_tx
3082            .send(())
3083            .expect("blocking task should be waiting");
3084        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3085            .await
3086            .expect("task present");
3087
3088        let TaskJoinOutcome::Completed(value) = outcome else {
3089            panic!("expected completed task after canceled abort wait, was {outcome:?}");
3090        };
3091
3092        assert_eq!(value, 42);
3093        assert!(slot.is_none());
3094    }
3095
3096    #[cfg(not(all(feature = "simulation", madsim)))]
3097    #[rstest]
3098    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3099    async fn canceled_singular_abort_wait_preserves_abort_classification() {
3100        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3101        let (release_tx, release_rx) = std::sync::mpsc::channel();
3102
3103        let mut slot = TaskSlot::from_handle(task::spawn(async move {
3104            let _ = started_tx.send(());
3105            let _ = release_rx.recv();
3106            task::yield_now().await;
3107        }));
3108
3109        started_rx.await.expect("blocking task should start");
3110
3111        while !slot.abort_requested {
3112            {
3113                let finish = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT);
3114                tokio::pin!(finish);
3115                tokio::select! {
3116                    biased;
3117                    outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
3118                    () = time::sleep(POLL_INTERVAL) => {}
3119                }
3120            }
3121        }
3122
3123        assert!(slot.is_some());
3124        release_tx
3125            .send(())
3126            .expect("blocking task should be waiting");
3127        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3128            .await
3129            .expect("task present");
3130
3131        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
3132        assert!(slot.is_none());
3133    }
3134
3135    fn pending_with_drop_signal() -> (
3136        impl Future<Output = ()>,
3137        tokio::sync::oneshot::Receiver<()>,
3138        Arc<AtomicBool>,
3139    ) {
3140        let dropped = Arc::new(AtomicBool::new(false));
3141        let dropped_task = Arc::clone(&dropped);
3142        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3143
3144        let future = async move {
3145            let _drop = DropSignal(dropped_task);
3146            let _ = started_tx.send(());
3147            std::future::pending::<()>().await;
3148        };
3149
3150        (future, started_rx, dropped)
3151    }
3152
3153    async fn wait_for_drop(dropped: &AtomicBool) {
3154        time::timeout(TEST_TIMEOUT, async {
3155            while !dropped.load(Ordering::Acquire) {
3156                task::yield_now().await;
3157            }
3158        })
3159        .await
3160        .expect("task should be dropped");
3161    }
3162
3163    struct DropSignal(Arc<AtomicBool>);
3164
3165    impl Drop for DropSignal {
3166        fn drop(&mut self) {
3167            self.0.store(true, Ordering::Release);
3168        }
3169    }
3170
3171    struct ReentrantDropFuture {
3172        group: Arc<TaskGroup>,
3173        dropped: Arc<AtomicBool>,
3174        polled: Arc<AtomicBool>,
3175    }
3176
3177    struct ReentrantWake {
3178        group: Arc<TaskGroup>,
3179        woke: Arc<AtomicBool>,
3180    }
3181
3182    impl Wake for ReentrantWake {
3183        fn wake(self: Arc<Self>) {
3184            let _ = self.group.is_open();
3185            self.woke.store(true, Ordering::Release);
3186        }
3187    }
3188
3189    impl Future for ReentrantDropFuture {
3190        type Output = ();
3191
3192        fn poll(
3193            self: std::pin::Pin<&mut Self>,
3194            _cx: &mut std::task::Context<'_>,
3195        ) -> std::task::Poll<Self::Output> {
3196            self.polled.store(true, Ordering::Release);
3197            std::task::Poll::Pending
3198        }
3199    }
3200
3201    impl Drop for ReentrantDropFuture {
3202        fn drop(&mut self) {
3203            let _ = self.group.is_open();
3204            self.dropped.store(true, Ordering::Release);
3205        }
3206    }
3207}