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 if shutdown has begun.
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 if shutdown has begun.
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 if shutdown has begun.
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 if 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 if 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 if 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    /// Panics if the slot already owns a task. Aborts `handle` before panicking.
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    /// Polls the owned task to termination without borrowing its owner across an await.
670    ///
671    /// A pending or canceled poll retains ownership. A ready poll consumes the terminal handle.
672    pub fn poll_join(
673        &mut self,
674        context: &mut std::task::Context<'_>,
675    ) -> std::task::Poll<Option<TaskJoinOutcome<T>>> {
676        let Some(handle) = self.handle.as_mut() else {
677            return std::task::Poll::Ready(None);
678        };
679
680        match std::pin::Pin::new(handle).poll(context) {
681            std::task::Poll::Pending => std::task::Poll::Pending,
682            std::task::Poll::Ready(result) => std::task::Poll::Ready(Some(self.complete(result))),
683        }
684    }
685
686    fn complete(&mut self, result: Result<T, JoinError>) -> TaskJoinOutcome<T> {
687        let outcome = match result {
688            Ok(output) => TaskJoinOutcome::Completed(output),
689            Err(e) if e.is_cancelled() && self.abort_requested => TaskJoinOutcome::Aborted,
690            Err(e) => TaskJoinOutcome::Failed(e),
691        };
692
693        self.handle.take();
694        self.abort_requested = false;
695        outcome
696    }
697}
698
699impl<T> Drop for TaskSlot<T> {
700    fn drop(&mut self) {
701        if let Some(handle) = self.handle.as_ref() {
702            handle.abort();
703        }
704    }
705}
706
707/// Owns one typed task shared by cloned clients.
708///
709/// Concurrent finish calls are serialized within the caller's graceful and abort durations.
710/// Canceling a finish or reaching the forced-completion deadline retains the task for another drain
711/// attempt. Dropping the owner requests abort but cannot asynchronously prove termination.
712#[derive(Debug)]
713pub struct SharedTaskSlot<T> {
714    state: Mutex<SharedTaskState<T>>,
715    drain_lock: tokio::sync::Mutex<()>,
716    owned: AtomicBool,
717}
718
719#[derive(Debug)]
720struct SharedTaskState<T> {
721    slot: TaskSlot<T>,
722    abort: CancellationToken,
723    abort_requested: bool,
724    draining: bool,
725}
726
727impl<T> SharedTaskState<T> {
728    fn try_reserve_drain(&mut self) -> Option<(TaskSlot<T>, CancellationToken, bool)> {
729        if self.draining {
730            return None;
731        }
732
733        self.draining = true;
734        Some((
735            std::mem::take(&mut self.slot),
736            self.abort.clone(),
737            self.abort_requested,
738        ))
739    }
740}
741
742impl<T> Default for SharedTaskSlot<T> {
743    fn default() -> Self {
744        Self::new()
745    }
746}
747
748impl<T> SharedTaskSlot<T> {
749    /// Creates an empty shared task slot.
750    #[must_use]
751    pub fn new() -> Self {
752        Self {
753            state: Mutex::new(SharedTaskState {
754                slot: TaskSlot::new(),
755                abort: CancellationToken::new(),
756                abort_requested: false,
757                draining: false,
758            }),
759            drain_lock: tokio::sync::Mutex::const_new(()),
760            owned: AtomicBool::new(false),
761        }
762    }
763
764    /// Returns whether the slot owns no task, including while a drain is pending.
765    #[must_use]
766    pub fn is_empty(&self) -> bool {
767        !self.owned.load(Ordering::Acquire)
768    }
769
770    /// Returns whether the owned task has finished without being joined.
771    ///
772    /// Returns `false` while a drain owns the handle and its state is unavailable.
773    #[must_use]
774    pub fn is_finished(&self) -> bool {
775        if self.is_empty() {
776            return false;
777        }
778
779        let state = self.state.lock();
780        !state.draining && state.slot.as_ref().is_some_and(JoinHandle::is_finished)
781    }
782
783    /// Stores a task in an empty slot.
784    ///
785    /// # Panics
786    ///
787    /// Panics if the slot already owns a task or is draining one. Aborts `handle` before panicking.
788    pub fn insert(&self, handle: JoinHandle<T>) {
789        self.insert_slot(TaskSlot::from_handle(handle));
790    }
791
792    // panics-doc-ok (transitive via insert on an occupied or draining slot)
793    /// Spawns and stores a task before its future can be polled.
794    ///
795    /// # Errors
796    ///
797    /// Returns an error if the task stops before its start gate opens. The terminal handle remains
798    /// owned so its join outcome stays observable.
799    ///
800    /// # Panics
801    ///
802    /// Panics if the slot already owns a task or is draining one.
803    pub fn spawn<F>(&self, future: F) -> Result<(), TaskSpawnError>
804    where
805        T: Send + 'static,
806        F: Future<Output = T> + Send + 'static,
807    {
808        let (handle, start) = spawn_gated(future);
809        self.insert(handle);
810        start.send(()).map_err(|()| TaskSpawnError::START_FAILED)
811    }
812
813    fn insert_slot(&self, slot: TaskSlot<T>) {
814        assert!(
815            self.try_insert_slot(slot).is_ok(),
816            "shared task slot is already occupied"
817        );
818    }
819
820    /// Transfers a task slot into this owner if it is empty.
821    ///
822    /// An empty input slot is always accepted. Returns a nonempty input unchanged when this owner
823    /// already has a task or is draining one.
824    ///
825    /// # Errors
826    ///
827    /// Returns an error if this owner already has a task or is draining one. The error contains the
828    /// input slot.
829    pub fn try_insert_slot(&self, slot: TaskSlot<T>) -> Result<(), TaskSlot<T>> {
830        if slot.is_none() {
831            return Ok(());
832        }
833
834        let mut state = self.state.lock();
835        if self.owned.load(Ordering::Acquire) || state.slot.is_some() || state.draining {
836            return Err(slot);
837        }
838
839        self.owned.store(true, Ordering::Release);
840        state.slot = slot;
841        state.abort = CancellationToken::new();
842        state.abort_requested = false;
843        Ok(())
844    }
845
846    /// Requests task cancellation and records it as owner-initiated.
847    ///
848    /// # Panics
849    ///
850    /// Panics if the shared task slot changes while cancellation temporarily drains it.
851    pub fn abort(&self) {
852        let (mut slot, abort, moved_slot) = {
853            let mut state = self.state.lock();
854            state.abort_requested = true;
855            let moved_slot = !state.draining && state.slot.is_some();
856            if moved_slot {
857                state.draining = true;
858            }
859
860            let slot = if moved_slot {
861                std::mem::take(&mut state.slot)
862            } else {
863                TaskSlot::new()
864            };
865
866            (slot, state.abort.clone(), moved_slot)
867        };
868
869        slot.abort();
870        abort.cancel();
871
872        if moved_slot {
873            let mut state = self.state.lock();
874            assert!(
875                state.slot.is_none(),
876                "shared task slot changed while aborting"
877            );
878            state.slot = slot;
879            state.draining = false;
880        }
881    }
882
883    /// Gracefully joins the task, then aborts and joins it within a second bound.
884    ///
885    /// If another caller holds the drain lock through both bounds, returns
886    /// [`TaskJoinOutcome::Incomplete`] when a task remains owned.
887    pub async fn finish(
888        &self,
889        graceful_timeout: Duration,
890        abort_timeout: Duration,
891    ) -> Option<TaskJoinOutcome<T>> {
892        let started = time::Instant::now();
893
894        let Some(graceful_deadline) = started.checked_add(graceful_timeout) else {
895            return self.incomplete_outcome();
896        };
897
898        let Some(abort_deadline) = graceful_deadline.checked_add(abort_timeout) else {
899            return self.incomplete_outcome();
900        };
901
902        let lock_timeout = abort_deadline.saturating_duration_since(time::Instant::now());
903
904        let Ok(_drain_lock) = time::timeout(lock_timeout, self.drain_lock.lock()).await else {
905            return self.incomplete_outcome();
906        };
907
908        let reserve_timeout = abort_deadline.saturating_duration_since(time::Instant::now());
909
910        let Ok((slot, abort, abort_requested)) = time::timeout(reserve_timeout, async {
911            loop {
912                if let Some(reservation) = self.state.lock().try_reserve_drain() {
913                    break reservation;
914                }
915
916                nautilus_common::live::dst::task::yield_now().await;
917            }
918        })
919        .await
920        else {
921            return self.incomplete_outcome();
922        };
923
924        if slot.is_none() {
925            let mut state = self.state.lock();
926            self.owned.store(false, Ordering::Release);
927            state.draining = false;
928            return None;
929        }
930
931        let mut draining = SharedTaskDrain { owner: self, slot };
932        let aborting = abort_requested || abort.is_cancelled();
933        if aborting {
934            draining.slot.abort();
935        }
936
937        let graceful_remaining = graceful_deadline.saturating_duration_since(time::Instant::now());
938
939        let outcome = if aborting {
940            let abort_remaining = abort_deadline.saturating_duration_since(time::Instant::now());
941            finish_task(&mut draining.slot, Duration::ZERO, abort_remaining).await
942        } else {
943            let graceful = {
944                let abort_remaining = abort_deadline
945                    .saturating_duration_since(graceful_deadline.max(time::Instant::now()));
946                let finish = finish_task(&mut draining.slot, graceful_remaining, abort_remaining);
947                tokio::pin!(finish);
948                tokio::select! {
949                    biased;
950                    outcome = &mut finish => Some(outcome),
951                    () = abort.cancelled() => None,
952                }
953            };
954
955            if let Some(outcome) = graceful {
956                outcome
957            } else {
958                draining.slot.abort();
959                let abort_remaining =
960                    abort_deadline.saturating_duration_since(time::Instant::now());
961                finish_task(&mut draining.slot, Duration::ZERO, abort_remaining).await
962            }
963        };
964
965        drop(draining);
966        outcome
967    }
968
969    fn incomplete_outcome(&self) -> Option<TaskJoinOutcome<T>> {
970        (!self.is_empty()).then_some(TaskJoinOutcome::Incomplete)
971    }
972}
973
974impl<T> Drop for SharedTaskSlot<T> {
975    fn drop(&mut self) {
976        self.state.get_mut().slot.abort();
977    }
978}
979
980struct SharedTaskDrain<'a, T> {
981    owner: &'a SharedTaskSlot<T>,
982    slot: TaskSlot<T>,
983}
984
985impl<T> Drop for SharedTaskDrain<'_, T> {
986    fn drop(&mut self) {
987        let owns_task = self.slot.is_some();
988        let mut abort_applied = false;
989
990        loop {
991            let mut state = self.owner.state.lock();
992            if owns_task && !abort_applied && (state.abort_requested || state.abort.is_cancelled())
993            {
994                drop(state);
995                self.slot.abort();
996                abort_applied = true;
997                continue;
998            }
999
1000            assert!(
1001                state.slot.is_none(),
1002                "shared task slot changed while draining"
1003            );
1004            state.slot = std::mem::take(&mut self.slot);
1005            state.draining = false;
1006            self.owner.owned.store(owns_task, Ordering::Release);
1007            break;
1008        }
1009    }
1010}
1011
1012/// Gracefully joins one task, then aborts and joins it within a second bound.
1013///
1014/// This preserves typed task results. The handle and forced-abort state remain in `slot` while the
1015/// function is pending, so canceling the finish future cannot detach the task or lose its expected
1016/// cancellation provenance. A terminal join clears the slot, while a second timeout leaves the
1017/// incomplete task in place for another drain attempt.
1018pub async fn finish_task<T>(
1019    slot: &mut TaskSlot<T>,
1020    graceful_timeout: Duration,
1021    abort_timeout: Duration,
1022) -> Option<TaskJoinOutcome<T>> {
1023    let graceful_result = {
1024        let handle = slot.handle.as_mut()?;
1025        time::timeout(graceful_timeout, handle).await
1026    };
1027
1028    match graceful_result {
1029        Ok(result) => Some(slot.complete(result)),
1030        Err(_) => {
1031            slot.abort();
1032
1033            let abort_result = {
1034                let handle = slot.handle.as_mut()?;
1035                time::timeout(abort_timeout, handle).await
1036            };
1037
1038            match abort_result {
1039                Ok(result) => Some(slot.complete(result)),
1040                Err(_) => Some(TaskJoinOutcome::Incomplete),
1041            }
1042        }
1043    }
1044}
1045
1046#[repr(u8)]
1047#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1048enum TaskGroupPhase {
1049    Open,
1050    Closing,
1051    Drained,
1052}
1053
1054impl TaskGroupPhase {
1055    fn load(value: &TaskState) -> Self {
1056        match value.load(Ordering::Acquire) {
1057            0 => Self::Open,
1058            1 => Self::Closing,
1059            2 => Self::Drained,
1060            value => unreachable!("invalid task group phase {value}"),
1061        }
1062    }
1063}
1064
1065#[derive(Debug)]
1066struct TaskGeneration {
1067    phase: TaskState,
1068    admission_lock: parking_lot::Mutex<()>,
1069    cancellation: CancellationToken,
1070    force: CancellationToken,
1071    tasks: TaskTracker,
1072    failures: Mutex<Vec<String>>,
1073}
1074
1075impl TaskGeneration {
1076    fn new() -> Self {
1077        Self {
1078            phase: TaskState::new(TaskGroupPhase::Open as u8),
1079            admission_lock: parking_lot::Mutex::new(()),
1080            cancellation: CancellationToken::new(),
1081            force: CancellationToken::new(),
1082            tasks: TaskTracker::new(),
1083            failures: Mutex::new(Vec::new()),
1084        }
1085    }
1086
1087    fn phase(&self) -> TaskGroupPhase {
1088        TaskGroupPhase::load(&self.phase)
1089    }
1090
1091    fn is_open(&self) -> bool {
1092        self.phase() == TaskGroupPhase::Open
1093    }
1094
1095    fn close_admission(&self) {
1096        let _guard = self.admission_lock.lock();
1097
1098        let _ = self
1099            .phase
1100            .try_update(Ordering::AcqRel, Ordering::Acquire, |phase| match phase {
1101                value
1102                    if value == TaskGroupPhase::Open as u8
1103                        || value == TaskGroupPhase::Drained as u8 =>
1104                {
1105                    Some(TaskGroupPhase::Closing as u8)
1106                }
1107                value if value == TaskGroupPhase::Closing as u8 => None,
1108                value => unreachable!("invalid task group phase {value}"),
1109            });
1110    }
1111
1112    fn cancel(&self) {
1113        self.tasks.close();
1114        self.cancellation.cancel();
1115    }
1116
1117    fn spawn<F>(self: &Arc<Self>, future: F) -> Result<(), TaskSpawnError>
1118    where
1119        F: Future<Output = ()> + Send + 'static,
1120    {
1121        let registration = match self.register_task(None) {
1122            Ok(registration) => registration,
1123            Err(e) => {
1124                drop(future);
1125                return Err(e);
1126            }
1127        };
1128
1129        self.spawn_registered(registration, future);
1130
1131        Ok(())
1132    }
1133
1134    fn spawn_named<F>(
1135        self: &Arc<Self>,
1136        name: &'static str,
1137        future: F,
1138    ) -> Result<TaskRef, TaskSpawnError>
1139    where
1140        F: Future<Output = ()> + Send + 'static,
1141    {
1142        let identity = Arc::new(TaskIdentity::new(name));
1143
1144        let task = TaskRef {
1145            identity: Arc::clone(&identity),
1146        };
1147
1148        let registration = match self.register_task(Some(identity)) {
1149            Ok(registration) => registration,
1150            Err(e) => {
1151                drop(future);
1152                return Err(e);
1153            }
1154        };
1155
1156        self.spawn_registered(registration, future);
1157
1158        Ok(task)
1159    }
1160
1161    fn spawn_registered<F>(&self, registration: TaskRegistration, future: F)
1162    where
1163        F: Future<Output = ()> + Send + 'static,
1164    {
1165        let force = self.force.clone();
1166
1167        spawn(async move {
1168            let result = AssertUnwindSafe(async move {
1169                tokio::select! {
1170                    biased;
1171                    () = force.cancelled() => {}
1172                    () = future => {}
1173                }
1174            })
1175            .catch_unwind()
1176            .await;
1177
1178            registration.complete(result.err());
1179        });
1180    }
1181
1182    fn register_task(
1183        self: &Arc<Self>,
1184        identity: Option<Arc<TaskIdentity>>,
1185    ) -> Result<TaskRegistration, TaskSpawnError> {
1186        let _guard = self.admission_lock.lock();
1187
1188        if !self.is_open() {
1189            return Err(TaskSpawnError::CLOSED);
1190        }
1191
1192        let token = self.tasks.token();
1193        Ok(TaskRegistration::new(Arc::clone(self), token, identity))
1194    }
1195
1196    fn record_failure(&self, failure: String) {
1197        self.lock_failures().push(failure);
1198    }
1199
1200    fn take_failures(&self) -> Vec<String> {
1201        std::mem::take(&mut *self.lock_failures())
1202    }
1203
1204    fn timeout_error(&self) -> TaskShutdownError {
1205        TaskShutdownError::Timeout {
1206            failures: self.lock_failures().clone(),
1207            incomplete: self.tasks.len(),
1208        }
1209    }
1210
1211    fn complete_shutdown(&self) -> Result<(), TaskShutdownError> {
1212        self.phase
1213            .store(TaskGroupPhase::Drained as u8, Ordering::Release);
1214        let failures = self.take_failures();
1215        if failures.is_empty() {
1216            Ok(())
1217        } else {
1218            Err(TaskShutdownError::Join(failures))
1219        }
1220    }
1221
1222    fn lock_failures(&self) -> MutexGuard<'_, Vec<String>> {
1223        self.failures.lock()
1224    }
1225}
1226
1227struct TaskRegistration {
1228    generation: Arc<TaskGeneration>,
1229    _token: TaskTrackerToken,
1230    identity: Option<Arc<TaskIdentity>>,
1231    terminal: bool,
1232}
1233
1234impl TaskRegistration {
1235    fn new(
1236        generation: Arc<TaskGeneration>,
1237        token: TaskTrackerToken,
1238        identity: Option<Arc<TaskIdentity>>,
1239    ) -> Self {
1240        Self {
1241            generation,
1242            _token: token,
1243            identity,
1244            terminal: false,
1245        }
1246    }
1247
1248    fn complete(mut self, panic: Option<Box<dyn Any + Send>>) {
1249        self.terminal = true;
1250        self.finish();
1251
1252        if let Some(panic) = panic {
1253            let failure = format!("panicked: {}", panic_message(panic.as_ref()));
1254            self.record_failure(&failure);
1255        }
1256    }
1257
1258    fn finish(&self) {
1259        if let Some(identity) = &self.identity {
1260            identity.finish();
1261        }
1262    }
1263
1264    fn record_failure(&self, failure: &str) {
1265        let failure = self.identity.as_ref().map_or_else(
1266            || format!("task {failure}"),
1267            |identity| identity.failure(failure),
1268        );
1269        self.generation.record_failure(failure);
1270    }
1271}
1272
1273impl Drop for TaskRegistration {
1274    fn drop(&mut self) {
1275        self.finish();
1276
1277        if !self.terminal && !self.generation.force.is_cancelled() {
1278            self.record_failure("was canceled unexpectedly");
1279        }
1280    }
1281}
1282
1283fn panic_message(payload: &(dyn Any + Send)) -> &str {
1284    if let Some(message) = payload.downcast_ref::<&'static str>() {
1285        message
1286    } else if let Some(message) = payload.downcast_ref::<String>() {
1287        message.as_str()
1288    } else {
1289        "non-string panic payload"
1290    }
1291}
1292
1293#[derive(Debug)]
1294struct TaskGroupInner {
1295    generation: ArcSwap<TaskGeneration>,
1296    generation_lock: parking_lot::Mutex<()>,
1297    drain_lock: tokio::sync::Mutex<()>,
1298}
1299
1300impl TaskGroupInner {
1301    fn current(&self) -> Arc<TaskGeneration> {
1302        self.generation.load_full()
1303    }
1304
1305    fn begin_shutdown(&self) -> Arc<TaskGeneration> {
1306        let generation = {
1307            let _guard = self.generation_lock.lock();
1308            let generation = self.current();
1309            generation.close_admission();
1310            generation
1311        };
1312
1313        generation.cancel();
1314        generation
1315    }
1316
1317    fn start_generation(&self) -> Result<(), TaskGenerationError> {
1318        let _guard = self.generation_lock.lock();
1319        let current = self.current();
1320        if current.phase() != TaskGroupPhase::Drained || !current.tasks.is_empty() {
1321            return Err(TaskGenerationError::NOT_DRAINED);
1322        }
1323
1324        self.generation.store(Arc::new(TaskGeneration::new()));
1325        Ok(())
1326    }
1327}
1328
1329fn spawn_gated<F>(future: F) -> (JoinHandle<F::Output>, tokio::sync::oneshot::Sender<()>)
1330where
1331    F: Future + Send + 'static,
1332    F::Output: Send + 'static,
1333{
1334    let (start, wait) = tokio::sync::oneshot::channel();
1335
1336    let handle = spawn(async move {
1337        wait.await.expect("task start gate sender dropped");
1338        future.await
1339    });
1340
1341    (handle, start)
1342}
1343
1344#[cfg(all(feature = "simulation", madsim))]
1345fn spawn<F>(future: F) -> JoinHandle<F::Output>
1346where
1347    F: Future + Send + 'static,
1348    F::Output: Send + 'static,
1349{
1350    nautilus_common::live::dst::task::spawn(future)
1351}
1352
1353#[cfg(not(all(feature = "simulation", madsim)))]
1354fn spawn<F>(future: F) -> JoinHandle<F::Output>
1355where
1356    F: Future + Send + 'static,
1357    F::Output: Send + 'static,
1358{
1359    nautilus_common::live::get_runtime().spawn(future)
1360}
1361
1362#[cfg(test)]
1363mod tests {
1364    use std::{
1365        pin::Pin,
1366        sync::{
1367            Arc,
1368            atomic::{AtomicBool, AtomicUsize, Ordering},
1369        },
1370        task::{Context, Poll, Wake, Waker},
1371    };
1372
1373    use nautilus_common::live::dst::task;
1374    use rstest::rstest;
1375
1376    use super::*;
1377
1378    const TEST_TIMEOUT: Duration = Duration::from_secs(1);
1379
1380    /// Interval a test waits on while letting an in-flight `finish_*` future make progress.
1381    ///
1382    /// Sleeping rather than yielding is deliberate. A `yield_now` arm leaves the polling task
1383    /// immediately runnable again, so the loop can re-poll without the runtime servicing its
1384    /// timer driver, and a zero-duration `time::timeout` inside the raced future may then not
1385    /// fire promptly. Sleeping yields to the timer driver before the next poll.
1386    #[cfg(not(all(feature = "simulation", madsim)))]
1387    const POLL_INTERVAL: Duration = Duration::from_millis(10);
1388
1389    #[rstest]
1390    fn task_group_guard_closes_all_groups_and_runs_rollback_until_disarmed() {
1391        let first = Arc::new(TaskGroup::new());
1392        let second = Arc::new(TaskGroup::new());
1393        let rolled_back = Arc::new(AtomicBool::new(false));
1394        let first_on_drop = Arc::clone(&first);
1395        let second_on_drop = Arc::clone(&second);
1396        let rolled_back_on_drop = Arc::clone(&rolled_back);
1397
1398        drop(TaskGroupGuard::new(&[&first, &second], move || {
1399            assert!(!first_on_drop.is_open());
1400            assert!(!second_on_drop.is_open());
1401            rolled_back_on_drop.store(true, Ordering::Release);
1402        }));
1403
1404        assert!(!first.is_open());
1405        assert!(!second.is_open());
1406        assert!(rolled_back.load(Ordering::Acquire));
1407
1408        let group = Arc::new(TaskGroup::new());
1409        let rolled_back = Arc::new(AtomicBool::new(false));
1410        let rolled_back_on_drop = Arc::clone(&rolled_back);
1411        TaskGroupGuard::new(&[&group], move || {
1412            rolled_back_on_drop.store(true, Ordering::Release);
1413        })
1414        .disarm();
1415
1416        assert!(group.is_open());
1417        assert!(!rolled_back.load(Ordering::Acquire));
1418    }
1419
1420    #[rstest]
1421    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1422    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1423    async fn task_is_registered_before_first_poll() {
1424        let group = TaskGroup::new();
1425        let observed = Arc::new(AtomicUsize::new(0));
1426        let observed_task = Arc::clone(&observed);
1427        let generation = group.inner.current();
1428
1429        group
1430            .spawn(async move {
1431                observed_task.store(generation.tasks.len(), Ordering::Release);
1432            })
1433            .expect("spawn");
1434
1435        time::timeout(TEST_TIMEOUT, async {
1436            while observed.load(Ordering::Acquire) == 0 {
1437                task::yield_now().await;
1438            }
1439        })
1440        .await
1441        .expect("task should poll");
1442
1443        group.begin_shutdown();
1444        group
1445            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1446            .await
1447            .expect("shutdown");
1448        assert_eq!(observed.load(Ordering::Acquire), 1);
1449    }
1450
1451    #[rstest]
1452    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1453    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1454    async fn named_tasks_expose_distinct_identity_without_transferring_ownership() {
1455        let group = TaskGroup::new();
1456        let first = group
1457            .spawn_named("dispatch", std::future::pending())
1458            .expect("spawn first task");
1459        let second = group
1460            .spawner()
1461            .expect("task spawner")
1462            .spawn_named("dispatch", std::future::pending())
1463            .expect("spawn second task");
1464        let first_observer = first.clone();
1465        drop(first);
1466
1467        assert_eq!(first_observer.name(), "dispatch");
1468        assert_eq!(second.name(), "dispatch");
1469        assert_ne!(first_observer.id(), second.id());
1470        assert!(first_observer.is_active());
1471        assert!(second.is_active());
1472        assert!(!first_observer.is_finished());
1473        assert!(!second.is_finished());
1474        assert_eq!(group.len(), 2);
1475
1476        group.abort();
1477        group
1478            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1479            .await
1480            .expect("shutdown");
1481
1482        assert!(!first_observer.is_active());
1483        assert!(!second.is_active());
1484        assert!(first_observer.is_finished());
1485        assert!(second.is_finished());
1486        assert!(group.is_empty());
1487    }
1488
1489    #[rstest]
1490    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1491    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1492    async fn named_task_ids_are_unique_across_groups_and_generations() {
1493        let first_group = TaskGroup::new();
1494        let first = first_group
1495            .spawn_named("dispatch", std::future::pending())
1496            .expect("spawn first task");
1497        first_group.abort();
1498        first_group
1499            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1500            .await
1501            .expect("first generation shutdown");
1502        first_group
1503            .start_generation()
1504            .expect("replacement generation");
1505
1506        let second = first_group
1507            .spawn_named("dispatch", std::future::pending())
1508            .expect("spawn replacement task");
1509        let second_group = TaskGroup::new();
1510        let third = second_group
1511            .spawn_named("dispatch", std::future::pending())
1512            .expect("spawn other group task");
1513
1514        first_group.abort();
1515        first_group
1516            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1517            .await
1518            .expect("replacement generation shutdown");
1519        second_group.abort();
1520        second_group
1521            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1522            .await
1523            .expect("other group shutdown");
1524
1525        assert_ne!(first.id(), second.id());
1526        assert_ne!(first.id(), third.id());
1527        assert_ne!(second.id(), third.id());
1528    }
1529
1530    #[rstest]
1531    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1532    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1533    async fn dropping_named_task_ref_preserves_group_ownership() {
1534        let group = TaskGroup::new();
1535        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1536        let (request_tx, request_rx) = tokio::sync::oneshot::channel();
1537        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
1538
1539        let task = group
1540            .spawn_named("dispatch", async move {
1541                let _ = started_tx.send(());
1542                let _ = request_rx.await;
1543                let _ = response_tx.send(());
1544                std::future::pending::<()>().await;
1545            })
1546            .expect("spawn task");
1547
1548        started_rx.await.expect("task should start");
1549
1550        drop(task);
1551        request_tx.send(()).expect("task should remain owned");
1552        time::timeout(TEST_TIMEOUT, response_rx)
1553            .await
1554            .expect("task should respond")
1555            .expect("task should remain active");
1556
1557        assert_eq!(group.len(), 1);
1558
1559        group.abort();
1560        group
1561            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1562            .await
1563            .expect("shutdown");
1564
1565        assert!(group.is_empty());
1566    }
1567
1568    #[rstest]
1569    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1570    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1571    async fn named_task_observes_normal_completion() {
1572        let group = TaskGroup::new();
1573        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
1574
1575        let task = group
1576            .spawn_named("finite", async move {
1577                let _ = release_rx.await;
1578            })
1579            .expect("spawn task");
1580
1581        release_tx.send(()).expect("task should be waiting");
1582        time::timeout(TEST_TIMEOUT, async {
1583            while !task.is_finished() {
1584                task::yield_now().await;
1585            }
1586        })
1587        .await
1588        .expect("task should finish");
1589
1590        group.begin_shutdown();
1591        group
1592            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1593            .await
1594            .expect("shutdown");
1595
1596        assert!(!task.is_active());
1597        assert!(task.is_finished());
1598        assert!(group.is_empty());
1599    }
1600
1601    #[rstest]
1602    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1603    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1604    async fn named_task_panic_includes_identity() {
1605        let group = TaskGroup::new();
1606
1607        let task = group
1608            .spawn_named("panicking", async {
1609                panic!("task panic");
1610            })
1611            .expect("spawn task");
1612
1613        group.begin_shutdown();
1614        let error = group
1615            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1616            .await
1617            .expect_err("panic should be reported");
1618
1619        let TaskShutdownError::Join(failures) = error else {
1620            panic!("expected join failure");
1621        };
1622
1623        assert_eq!(
1624            failures,
1625            [format!(
1626                "task 'panicking' ({}) panicked: task panic",
1627                task.id()
1628            )]
1629        );
1630        assert!(task.is_finished());
1631        assert!(group.is_empty());
1632    }
1633
1634    #[rstest]
1635    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1636    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1637    async fn named_task_forced_before_first_poll_becomes_finished() {
1638        let group = TaskGroup::new();
1639        let generation = group.inner.current();
1640        let identity = Arc::new(TaskIdentity::new("not-polled"));
1641
1642        let task = TaskRef {
1643            identity: Arc::clone(&identity),
1644        };
1645
1646        let registration = generation
1647            .register_task(Some(identity))
1648            .expect("registration");
1649        let polled = Arc::new(AtomicBool::new(false));
1650        let polled_task = Arc::clone(&polled);
1651
1652        assert!(task.is_active());
1653        assert!(!task.is_finished());
1654
1655        group.abort();
1656        generation.spawn_registered(registration, async move {
1657            polled_task.store(true, Ordering::Release);
1658        });
1659
1660        group
1661            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1662            .await
1663            .expect("shutdown");
1664
1665        assert!(!polled.load(Ordering::Acquire));
1666        assert!(!task.is_active());
1667        assert!(task.is_finished());
1668        assert!(group.is_empty());
1669    }
1670
1671    #[rstest]
1672    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1673    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1674    async fn named_registration_drop_after_forced_cancellation_is_not_reported() {
1675        let group = TaskGroup::new();
1676        let generation = group.inner.current();
1677        let identity = Arc::new(TaskIdentity::new("canceled"));
1678
1679        let task = TaskRef {
1680            identity: Arc::clone(&identity),
1681        };
1682
1683        let registration = generation
1684            .register_task(Some(identity))
1685            .expect("registration");
1686
1687        group.abort();
1688        drop(registration);
1689        group
1690            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1691            .await
1692            .expect("forced cancellation should not be reported");
1693
1694        assert!(!task.is_active());
1695        assert!(task.is_finished());
1696        assert!(group.is_empty());
1697    }
1698
1699    #[rstest]
1700    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1701    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1702    async fn rejected_named_task_drops_future_without_polling() {
1703        let group = Arc::new(TaskGroup::new());
1704        let dropped = Arc::new(AtomicBool::new(false));
1705        let polled = Arc::new(AtomicBool::new(false));
1706        group.begin_shutdown();
1707
1708        let result = group.spawn_named(
1709            "rejected",
1710            ReentrantDropFuture {
1711                group: Arc::clone(&group),
1712                dropped: Arc::clone(&dropped),
1713                polled: Arc::clone(&polled),
1714            },
1715        );
1716
1717        assert!(matches!(result, Err(TaskSpawnError::CLOSED)));
1718        assert!(dropped.load(Ordering::Acquire));
1719        assert!(!polled.load(Ordering::Acquire));
1720        group
1721            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1722            .await
1723            .expect("shutdown");
1724    }
1725
1726    #[rstest]
1727    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1728    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1729    async fn shutdown_closes_admission_before_future_poll() {
1730        let group = TaskGroup::new();
1731        let polled = Arc::new(AtomicBool::new(false));
1732        let polled_task = Arc::clone(&polled);
1733
1734        group.begin_shutdown();
1735
1736        let result = group.spawn(async move {
1737            polled_task.store(true, Ordering::Release);
1738        });
1739
1740        assert!(matches!(group.spawner(), Err(TaskSpawnError::CLOSED)));
1741        assert_eq!(result, Err(TaskSpawnError::CLOSED));
1742        assert!(!polled.load(Ordering::Acquire));
1743        group
1744            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1745            .await
1746            .expect("shutdown");
1747    }
1748
1749    #[rstest]
1750    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1751    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1752    async fn rejected_future_can_reenter_group_on_drop() {
1753        let group = Arc::new(TaskGroup::new());
1754        let dropped = Arc::new(AtomicBool::new(false));
1755        let polled = Arc::new(AtomicBool::new(false));
1756        group.begin_shutdown();
1757
1758        let result = group.spawn(ReentrantDropFuture {
1759            group: Arc::clone(&group),
1760            dropped: Arc::clone(&dropped),
1761            polled: Arc::clone(&polled),
1762        });
1763
1764        assert_eq!(result, Err(TaskSpawnError::CLOSED));
1765        assert!(dropped.load(Ordering::Acquire));
1766        assert!(!polled.load(Ordering::Acquire));
1767        group
1768            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1769            .await
1770            .expect("shutdown");
1771    }
1772
1773    #[rstest]
1774    fn shutdown_cancellation_wake_can_reenter_group() {
1775        let group = Arc::new(TaskGroup::new());
1776        let cancellation = group.cancellation_token();
1777        let woke = Arc::new(AtomicBool::new(false));
1778
1779        let waker = Waker::from(Arc::new(ReentrantWake {
1780            group: Arc::clone(&group),
1781            woke: Arc::clone(&woke),
1782        }));
1783
1784        let mut context = Context::from_waker(&waker);
1785        let mut cancelled = Box::pin(cancellation.cancelled());
1786
1787        assert!(matches!(
1788            Pin::as_mut(&mut cancelled).poll(&mut context),
1789            Poll::Pending
1790        ));
1791        group.begin_shutdown();
1792
1793        assert!(woke.load(Ordering::Acquire));
1794    }
1795
1796    #[rstest]
1797    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1798    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1799    async fn stale_spawner_cannot_spawn_into_new_generation() {
1800        let group = TaskGroup::new();
1801        let old = group.spawner().expect("old spawner");
1802        let old_generation = Arc::clone(&old.generation);
1803        group.begin_shutdown();
1804        group
1805            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1806            .await
1807            .expect("shutdown");
1808
1809        assert_eq!(old.spawn(async {}), Err(TaskSpawnError::CLOSED));
1810        assert!(matches!(
1811            old.spawn_named("stale", async {}),
1812            Err(TaskSpawnError::CLOSED)
1813        ));
1814        assert!(old_generation.tasks.is_empty());
1815
1816        group.start_generation().expect("new generation");
1817        let current = group.spawner().expect("current spawner");
1818
1819        current
1820            .spawn_named("current", async {})
1821            .expect("current spawn");
1822
1823        group.begin_shutdown();
1824        group
1825            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1826            .await
1827            .expect("shutdown");
1828    }
1829
1830    #[rstest]
1831    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1832    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1833    async fn shutdown_allows_graceful_cancellation_cleanup() {
1834        let group = TaskGroup::new();
1835        let cancellation = group
1836            .spawner()
1837            .expect("task group spawner")
1838            .cancellation_token();
1839        let cleaned = Arc::new(AtomicBool::new(false));
1840        let cleaned_task = Arc::clone(&cleaned);
1841        group
1842            .spawn(async move {
1843                cancellation.cancelled().await;
1844                cleaned_task.store(true, Ordering::Release);
1845            })
1846            .expect("spawn");
1847
1848        group.begin_shutdown();
1849        group
1850            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1851            .await
1852            .expect("shutdown");
1853
1854        assert!(cleaned.load(Ordering::Acquire));
1855        assert!(group.is_empty());
1856    }
1857
1858    #[rstest]
1859    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1860    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1861    async fn shutdown_forces_abort_and_observes_canceled_join() {
1862        let group = TaskGroup::new();
1863        let (future, started_rx, dropped) = pending_with_drop_signal();
1864
1865        group.spawn(future).expect("spawn");
1866        started_rx.await.expect("task should start");
1867
1868        group.begin_shutdown();
1869        group
1870            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
1871            .await
1872            .expect("shutdown");
1873
1874        assert!(dropped.load(Ordering::Acquire));
1875        assert!(group.is_empty());
1876    }
1877
1878    #[rstest]
1879    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1880    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1881    async fn abort_closes_admission_and_cancels_tasks() {
1882        let group = TaskGroup::new();
1883        let dropped = Arc::new(AtomicBool::new(false));
1884        let drop_signal = DropSignal(Arc::clone(&dropped));
1885        group
1886            .spawn(async move {
1887                let _drop_signal = drop_signal;
1888                std::future::pending::<()>().await;
1889            })
1890            .expect("spawn");
1891
1892        group.abort();
1893        group
1894            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
1895            .await
1896            .expect("shutdown");
1897
1898        assert!(!group.is_open());
1899        assert!(group.is_empty());
1900        assert!(dropped.load(Ordering::Acquire));
1901    }
1902
1903    #[rstest]
1904    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1905    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1906    async fn dropping_task_group_aborts_owned_tasks() {
1907        let group = TaskGroup::new();
1908        let (future, started_rx, dropped) = pending_with_drop_signal();
1909
1910        group.spawn(future).expect("spawn");
1911        started_rx.await.expect("task should start");
1912
1913        drop(group);
1914        wait_for_drop(&dropped).await;
1915    }
1916
1917    #[rstest]
1918    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1919    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1920    async fn unexpected_task_cancellation_is_reported() {
1921        let group = TaskGroup::new();
1922        group.spawn(std::future::pending()).expect("spawn");
1923        let generation = group.inner.current();
1924        let registration =
1925            TaskRegistration::new(Arc::clone(&generation), generation.tasks.token(), None);
1926        drop(registration);
1927        group.begin_shutdown();
1928
1929        let error = group
1930            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
1931            .await
1932            .expect_err("unexpected cancellation should be reported");
1933
1934        let TaskShutdownError::Join(failures) = error else {
1935            panic!("expected join failure");
1936        };
1937
1938        assert_eq!(failures, ["task was canceled unexpectedly"]);
1939        assert!(group.is_empty());
1940    }
1941
1942    #[rstest]
1943    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1944    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1945    async fn named_unexpected_cancellation_includes_identity() {
1946        let group = TaskGroup::new();
1947        group.spawn(std::future::pending()).expect("spawn");
1948        let generation = group.inner.current();
1949        let identity = Arc::new(TaskIdentity::new("canceled"));
1950
1951        let task = TaskRef {
1952            identity: Arc::clone(&identity),
1953        };
1954
1955        let registration = generation
1956            .register_task(Some(identity))
1957            .expect("registration");
1958        drop(registration);
1959        group.begin_shutdown();
1960
1961        let error = group
1962            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
1963            .await
1964            .expect_err("unexpected cancellation should be reported");
1965
1966        let TaskShutdownError::Join(failures) = error else {
1967            panic!("expected join failure");
1968        };
1969
1970        assert_eq!(
1971            failures,
1972            [format!(
1973                "task 'canceled' ({}) was canceled unexpectedly",
1974                task.id()
1975            )]
1976        );
1977        assert!(task.is_finished());
1978        assert!(group.is_empty());
1979    }
1980
1981    #[rstest]
1982    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
1983    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
1984    async fn late_child_is_rejected_after_parent_observes_shutdown() {
1985        let group = TaskGroup::new();
1986        let spawner = group.spawner().expect("spawner");
1987        let cancellation = spawner.cancellation_token();
1988        let (result_tx, result_rx) = tokio::sync::oneshot::channel();
1989        group
1990            .spawn(async move {
1991                cancellation.cancelled().await;
1992
1993                let _ = result_tx.send(spawner.spawn(async {}));
1994            })
1995            .expect("spawn parent");
1996
1997        group.begin_shutdown();
1998        group
1999            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2000            .await
2001            .expect("shutdown");
2002
2003        assert_eq!(
2004            result_rx.await.expect("late spawn result"),
2005            Err(TaskSpawnError::CLOSED),
2006        );
2007    }
2008
2009    #[rstest]
2010    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2011    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2012    async fn shutdown_drains_accepted_registration_and_rejects_late_registration() {
2013        let group = TaskGroup::new();
2014        let generation = group.inner.current();
2015        let registration = generation.register_task(None).expect("registration");
2016
2017        group.begin_shutdown();
2018        assert!(matches!(
2019            generation.register_task(None),
2020            Err(TaskSpawnError::CLOSED)
2021        ));
2022        assert_eq!(group.len(), 1);
2023
2024        let error = group
2025            .finish_shutdown(Duration::ZERO, Duration::ZERO)
2026            .await
2027            .expect_err("accepted registration should prevent completed shutdown");
2028        assert!(matches!(
2029            error,
2030            TaskShutdownError::Timeout { incomplete: 1, .. }
2031        ));
2032        assert_eq!(
2033            group.start_generation(),
2034            Err(TaskGenerationError::NOT_DRAINED)
2035        );
2036
2037        registration.complete(None);
2038        group
2039            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2040            .await
2041            .expect("retry shutdown");
2042        group.start_generation().expect("new generation");
2043    }
2044
2045    #[rstest]
2046    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2047    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2048    async fn generation_cannot_reopen_before_finish_shutdown() {
2049        let group = TaskGroup::new();
2050        group.begin_shutdown();
2051
2052        assert!(matches!(
2053            group.start_generation(),
2054            Err(TaskGenerationError::NOT_DRAINED),
2055        ));
2056
2057        group
2058            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2059            .await
2060            .expect("shutdown");
2061        group.start_generation().expect("new generation");
2062    }
2063
2064    #[rstest]
2065    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2066    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2067    async fn shutdown_after_drain_requires_another_finish_before_reopening() {
2068        let group = TaskGroup::new();
2069        group.begin_shutdown();
2070        group
2071            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2072            .await
2073            .expect("initial shutdown");
2074
2075        group.begin_shutdown();
2076
2077        assert_eq!(
2078            group.start_generation(),
2079            Err(TaskGenerationError::NOT_DRAINED),
2080        );
2081
2082        group
2083            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2084            .await
2085            .expect("repeated shutdown");
2086        group.start_generation().expect("new generation");
2087    }
2088
2089    #[rstest]
2090    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2091    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2092    async fn canceled_finish_preserves_tracked_task() {
2093        let group = Arc::new(TaskGroup::new());
2094        group.spawn(std::future::pending()).expect("spawn");
2095        group.begin_shutdown();
2096        let finishing_group = Arc::clone(&group);
2097
2098        let finish = task::spawn(async move {
2099            finishing_group
2100                .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2101                .await
2102        });
2103
2104        time::timeout(TEST_TIMEOUT, async {
2105            while group.inner.drain_lock.try_lock().is_ok() {
2106                task::yield_now().await;
2107            }
2108        })
2109        .await
2110        .expect("finisher should begin draining");
2111
2112        finish.abort();
2113        let _ = finish.await;
2114
2115        assert_eq!(group.len(), 1);
2116        assert!(!group.is_empty());
2117
2118        group
2119            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2120            .await
2121            .expect("retry shutdown");
2122    }
2123
2124    #[cfg(not(all(feature = "simulation", madsim)))]
2125    #[rstest]
2126    #[tokio::test]
2127    async fn canceled_finish_preserves_observed_join_failures() {
2128        let group = TaskGroup::new();
2129        let (panicking_tx, panicking_rx) = tokio::sync::oneshot::channel();
2130        group
2131            .spawn(async move {
2132                let _ = panicking_tx.send(());
2133                panic!("task panic");
2134            })
2135            .expect("spawn panicking task");
2136
2137        panicking_rx.await.expect("panicking task should start");
2138        group
2139            .spawn(std::future::pending())
2140            .expect("spawn pending task");
2141        group.begin_shutdown();
2142
2143        loop {
2144            let finish = group.finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT);
2145            tokio::pin!(finish);
2146            tokio::select! {
2147                biased;
2148                outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
2149                () = task::yield_now() => {}
2150            }
2151
2152            if group.inner.current().lock_failures().len() == 1 {
2153                break;
2154            }
2155        }
2156
2157        let error = group
2158            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2159            .await
2160            .expect_err("panic should remain observable");
2161
2162        let TaskShutdownError::Join(failures) = error else {
2163            panic!("expected join failure");
2164        };
2165
2166        assert_eq!(failures.len(), 1);
2167        assert!(group.is_empty());
2168    }
2169
2170    #[cfg(not(all(feature = "simulation", madsim)))]
2171    #[rstest]
2172    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2173    async fn canceled_finish_preserves_forced_abort_classification() {
2174        let group = TaskGroup::new();
2175        let generation = group.inner.current();
2176        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2177        let (release_tx, release_rx) = std::sync::mpsc::channel();
2178        group
2179            .spawn(async move {
2180                let _ = started_tx.send(());
2181                let _ = release_rx.recv();
2182                task::yield_now().await;
2183            })
2184            .expect("spawn blocking task");
2185
2186        started_rx.await.expect("blocking task should start");
2187        group.begin_shutdown();
2188
2189        loop {
2190            {
2191                let finish = group.finish_shutdown(Duration::ZERO, TEST_TIMEOUT);
2192                tokio::pin!(finish);
2193                tokio::select! {
2194                    biased;
2195                    outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
2196                    () = time::sleep(POLL_INTERVAL) => {}
2197                }
2198            }
2199
2200            if generation.force.is_cancelled() {
2201                break;
2202            }
2203        }
2204
2205        assert!(generation.force.is_cancelled());
2206        release_tx
2207            .send(())
2208            .expect("blocking task should be waiting");
2209        group
2210            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2211            .await
2212            .expect("retry shutdown");
2213        assert!(group.is_empty());
2214    }
2215
2216    #[cfg(not(all(feature = "simulation", madsim)))]
2217    #[rstest]
2218    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2219    async fn concurrent_finish_respects_its_own_shutdown_bound() {
2220        let group = Arc::new(TaskGroup::new());
2221        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2222        let (release_tx, release_rx) = std::sync::mpsc::channel();
2223        group
2224            .spawn(async move {
2225                let _ = started_tx.send(());
2226                let _ = release_rx.recv();
2227            })
2228            .expect("spawn blocking task");
2229
2230        started_rx.await.expect("blocking task should start");
2231        group.begin_shutdown();
2232        let finishing_group = Arc::clone(&group);
2233
2234        let finish = task::spawn(async move {
2235            finishing_group
2236                .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2237                .await
2238        });
2239
2240        time::timeout(TEST_TIMEOUT, async {
2241            while group.inner.drain_lock.try_lock().is_ok() {
2242                task::yield_now().await;
2243            }
2244        })
2245        .await
2246        .expect("first finisher should hold the drain lock");
2247
2248        let error = group
2249            .finish_shutdown(Duration::ZERO, Duration::ZERO)
2250            .await
2251            .expect_err("second finisher should exhaust its own bound");
2252
2253        let TaskShutdownError::Timeout { incomplete, .. } = error else {
2254            panic!("expected shutdown timeout");
2255        };
2256
2257        assert_eq!(incomplete, 1);
2258
2259        release_tx
2260            .send(())
2261            .expect("blocking task should be waiting");
2262        finish
2263            .await
2264            .expect("first finisher should join")
2265            .expect("first shutdown should complete");
2266    }
2267
2268    #[rstest]
2269    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2270    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2271    async fn queued_finish_remains_bound_to_original_generation() {
2272        let group = TaskGroup::new();
2273        group.begin_shutdown();
2274        let generation = group.inner.current();
2275        let drain_lock = group.inner.drain_lock.lock().await;
2276        let mut finish = Box::pin(group.finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT));
2277
2278        assert!(matches!(
2279            futures_util::poll!(finish.as_mut()),
2280            Poll::Pending,
2281        ));
2282
2283        generation
2284            .complete_shutdown()
2285            .expect("original generation should drain");
2286        group.start_generation().expect("replacement generation");
2287        drop(drain_lock);
2288
2289        finish
2290            .await
2291            .expect("queued finish should observe the original generation");
2292        assert!(group.is_open());
2293    }
2294
2295    #[cfg(not(all(feature = "simulation", madsim)))]
2296    #[rstest]
2297    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2298    async fn forced_timeout_retains_task_and_in_flight_registration() {
2299        let group = Arc::new(TaskGroup::new());
2300        let generation = group.inner.current();
2301        let registration =
2302            TaskRegistration::new(Arc::clone(&generation), generation.tasks.token(), None);
2303        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2304        let (release_tx, release_rx) = std::sync::mpsc::channel();
2305        group
2306            .spawn(async move {
2307                let _ = started_tx.send(());
2308                let _ = release_rx.recv();
2309            })
2310            .expect("spawn blocking task");
2311
2312        started_rx.await.expect("blocking task should start");
2313        group.begin_shutdown();
2314
2315        let finishing_group = Arc::clone(&group);
2316
2317        let finish = task::spawn(async move {
2318            finishing_group
2319                .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2320                .await
2321        });
2322
2323        time::timeout(TEST_TIMEOUT, async {
2324            while !generation.force.is_cancelled() {
2325                task::yield_now().await;
2326            }
2327        })
2328        .await
2329        .expect("finisher should request forced cancellation");
2330
2331        let error = finish
2332            .await
2333            .expect("finisher should join")
2334            .expect_err("blocking task should exceed abort deadline");
2335
2336        let TaskShutdownError::Timeout { incomplete, .. } = error else {
2337            panic!("expected shutdown timeout");
2338        };
2339
2340        assert_eq!(incomplete, 2);
2341        assert_eq!(group.len(), 2);
2342
2343        release_tx.send(()).expect("release blocking task");
2344        registration.complete(None);
2345        group
2346            .finish_shutdown(Duration::ZERO, TEST_TIMEOUT)
2347            .await
2348            .expect("retry shutdown");
2349        assert!(group.is_empty());
2350    }
2351
2352    #[cfg(not(all(feature = "simulation", madsim)))]
2353    #[rstest]
2354    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2355    async fn stuck_head_does_not_hide_later_join_failure() {
2356        let group = TaskGroup::new();
2357        let generation = group.inner.current();
2358        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2359        let (release_tx, release_rx) = std::sync::mpsc::channel();
2360        group
2361            .spawn(async move {
2362                let _ = started_tx.send(());
2363                let _ = release_rx.recv();
2364            })
2365            .expect("spawn blocking task");
2366
2367        started_rx.await.expect("blocking task should start");
2368        group
2369            .spawn(async {
2370                panic!("later task panic");
2371            })
2372            .expect("spawn panicking task");
2373
2374        time::timeout(TEST_TIMEOUT, async {
2375            while generation.lock_failures().is_empty() {
2376                task::yield_now().await;
2377            }
2378        })
2379        .await
2380        .expect("panicking task should finish");
2381
2382        group.begin_shutdown();
2383
2384        let error = group
2385            .finish_shutdown(Duration::ZERO, Duration::from_millis(10))
2386            .await
2387            .expect_err("blocking task should exceed abort deadline");
2388
2389        let TaskShutdownError::Timeout {
2390            failures,
2391            incomplete,
2392        } = error
2393        else {
2394            panic!("expected shutdown timeout");
2395        };
2396
2397        assert_eq!(failures.len(), 1);
2398        assert!(failures[0].contains("later task panic"));
2399        assert_eq!(incomplete, 1);
2400        assert_eq!(group.len(), 1);
2401
2402        release_tx.send(()).expect("release blocking task");
2403        group
2404            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2405            .await
2406            .expect("retry shutdown");
2407        assert!(group.is_empty());
2408    }
2409
2410    #[rstest]
2411    #[case(Duration::MAX, Duration::ZERO)]
2412    #[case(Duration::ZERO, Duration::MAX)]
2413    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2414    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2415    async fn unrepresentable_deadline_retains_owned_task(
2416        #[case] graceful_timeout: Duration,
2417        #[case] abort_timeout: Duration,
2418    ) {
2419        let group = TaskGroup::new();
2420        group.spawn(std::future::pending()).expect("spawn");
2421        group.begin_shutdown();
2422
2423        let error = group
2424            .finish_shutdown(graceful_timeout, abort_timeout)
2425            .await
2426            .expect_err("deadline should be rejected");
2427
2428        assert!(matches!(
2429            error,
2430            TaskShutdownError::Timeout { incomplete: 1, .. }
2431        ));
2432        assert_eq!(group.len(), 1);
2433    }
2434
2435    #[rstest]
2436    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2437    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2438    async fn unrepresentable_deadline_reports_open_group() {
2439        let group = TaskGroup::new();
2440
2441        let error = group
2442            .finish_shutdown(Duration::MAX, Duration::ZERO)
2443            .await
2444            .expect_err("open group should reject shutdown completion");
2445
2446        assert!(matches!(error, TaskShutdownError::StillOpen));
2447    }
2448
2449    #[cfg(not(all(feature = "simulation", madsim)))]
2450    #[rstest]
2451    #[tokio::test]
2452    async fn panic_join_is_reported_after_group_drains() {
2453        let group = TaskGroup::new();
2454        group
2455            .spawn(async {
2456                panic!("task panic");
2457            })
2458            .expect("spawn");
2459
2460        group.begin_shutdown();
2461
2462        let error = group
2463            .finish_shutdown(TEST_TIMEOUT, TEST_TIMEOUT)
2464            .await
2465            .expect_err("panic should be reported");
2466
2467        let TaskShutdownError::Join(failures) = error else {
2468            panic!("expected join failure");
2469        };
2470
2471        assert_eq!(failures, ["task panicked: task panic"]);
2472        assert!(group.is_empty());
2473        group.start_generation().expect("group should reopen");
2474    }
2475
2476    #[rstest]
2477    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2478    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2479    async fn shared_task_spawn_reports_finished_and_preserves_typed_result() {
2480        let slot = SharedTaskSlot::new();
2481        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2482        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
2483
2484        slot.spawn(async move {
2485            let _ = started_tx.send(());
2486            let _ = release_rx.await;
2487            42
2488        })
2489        .expect("spawn");
2490
2491        started_rx.await.expect("task should start");
2492
2493        assert!(!slot.is_finished());
2494        release_tx.send(()).expect("task should be waiting");
2495
2496        time::timeout(TEST_TIMEOUT, async {
2497            while !slot.is_finished() {
2498                task::yield_now().await;
2499            }
2500        })
2501        .await
2502        .expect("task should finish");
2503
2504        assert!(!slot.is_empty());
2505        assert!(slot.is_finished());
2506        let outcome = slot
2507            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2508            .await
2509            .expect("task should be present");
2510
2511        let TaskJoinOutcome::Completed(value) = outcome else {
2512            panic!("expected completed task");
2513        };
2514
2515        assert_eq!(value, 42);
2516        assert!(slot.is_empty());
2517    }
2518
2519    #[rstest]
2520    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2521    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2522    async fn shared_task_abort_interrupts_active_finish() {
2523        let slot = Arc::new(SharedTaskSlot::new());
2524        slot.insert(task::spawn(std::future::pending::<u32>()));
2525        let finishing_slot = Arc::clone(&slot);
2526
2527        let finish =
2528            task::spawn(async move { finishing_slot.finish(TEST_TIMEOUT, TEST_TIMEOUT).await });
2529
2530        while !slot.state.lock().draining {
2531            task::yield_now().await;
2532        }
2533
2534        slot.abort();
2535
2536        let outcome = time::timeout(TEST_TIMEOUT, finish)
2537            .await
2538            .expect("abort should wake the active finish")
2539            .expect("finisher should join")
2540            .expect("task should be present");
2541        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2542        assert!(slot.is_empty());
2543    }
2544
2545    #[rstest]
2546    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2547    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2548    async fn concurrent_shared_finish_respects_its_own_bound() {
2549        let slot = Arc::new(SharedTaskSlot::new());
2550        slot.insert(task::spawn(std::future::pending::<u32>()));
2551        let finishing_slot = Arc::clone(&slot);
2552
2553        let finish = task::spawn(async move {
2554            finishing_slot
2555                .finish(Duration::from_secs(10), TEST_TIMEOUT)
2556                .await
2557        });
2558
2559        time::timeout(TEST_TIMEOUT, async {
2560            while slot.drain_lock.try_lock().is_ok() {
2561                task::yield_now().await;
2562            }
2563        })
2564        .await
2565        .expect("first finisher should hold the drain lock");
2566
2567        let second = time::timeout(TEST_TIMEOUT, slot.finish(Duration::ZERO, Duration::ZERO)).await;
2568        slot.abort();
2569        let first = time::timeout(TEST_TIMEOUT, finish)
2570            .await
2571            .expect("abort should wake the first finisher")
2572            .expect("first finisher should join")
2573            .expect("task should be present");
2574        let second = second
2575            .expect("second finisher should respect its own bound")
2576            .expect("task should remain owned");
2577
2578        assert!(matches!(first, TaskJoinOutcome::Aborted));
2579        assert!(matches!(second, TaskJoinOutcome::Incomplete));
2580        assert!(slot.is_empty());
2581    }
2582
2583    #[rstest]
2584    #[case(Duration::MAX, Duration::ZERO)]
2585    #[case(Duration::ZERO, Duration::MAX)]
2586    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2587    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2588    async fn unrepresentable_shared_deadline_retains_owned_task(
2589        #[case] graceful_timeout: Duration,
2590        #[case] abort_timeout: Duration,
2591    ) {
2592        let slot = SharedTaskSlot::new();
2593        slot.insert(task::spawn(std::future::pending::<()>()));
2594
2595        let outcome = slot
2596            .finish(graceful_timeout, abort_timeout)
2597            .await
2598            .expect("task should remain present");
2599
2600        assert!(matches!(outcome, TaskJoinOutcome::Incomplete));
2601        assert!(!slot.is_empty());
2602
2603        slot.abort();
2604        let outcome = slot
2605            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2606            .await
2607            .expect("task should remain present");
2608        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2609        assert!(slot.is_empty());
2610    }
2611
2612    #[rstest]
2613    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2614    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2615    async fn shared_task_abort_does_not_leak_into_next_generation() {
2616        let slot = SharedTaskSlot::new();
2617        slot.insert(task::spawn(std::future::pending::<u32>()));
2618        slot.abort();
2619        let first = slot
2620            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2621            .await
2622            .expect("first task");
2623        assert!(matches!(first, TaskJoinOutcome::Aborted));
2624
2625        slot.insert(task::spawn(async { 42 }));
2626        let second = slot
2627            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2628            .await
2629            .expect("second task");
2630
2631        let TaskJoinOutcome::Completed(value) = second else {
2632            panic!("expected completed second-generation task");
2633        };
2634
2635        assert_eq!(value, 42);
2636        assert!(slot.is_empty());
2637    }
2638
2639    #[rstest]
2640    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2641    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2642    async fn canceled_shared_finish_restores_owned_task() {
2643        let slot = Arc::new(SharedTaskSlot::new());
2644        slot.insert(task::spawn(std::future::pending::<()>()));
2645        let finishing_slot = Arc::clone(&slot);
2646
2647        let finish =
2648            task::spawn(async move { finishing_slot.finish(TEST_TIMEOUT, TEST_TIMEOUT).await });
2649
2650        while !slot.state.lock().draining {
2651            task::yield_now().await;
2652        }
2653
2654        finish.abort();
2655        let _ = finish.await;
2656
2657        assert!(!slot.is_empty());
2658        let outcome = slot
2659            .finish(Duration::ZERO, TEST_TIMEOUT)
2660            .await
2661            .expect("restored task");
2662        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2663        assert!(slot.is_empty());
2664    }
2665
2666    #[cfg(not(all(feature = "simulation", madsim)))]
2667    #[rstest]
2668    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2669    async fn shared_task_timeout_preserves_owned_typed_result() {
2670        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2671        let (release_tx, release_rx) = std::sync::mpsc::channel();
2672        let slot = SharedTaskSlot::new();
2673        slot.insert(tokio::task::spawn_blocking(move || {
2674            let _ = started_tx.send(());
2675            let _ = release_rx.recv();
2676            42
2677        }));
2678
2679        started_rx.await.expect("blocking task should start");
2680
2681        let outcome = slot
2682            .finish(Duration::ZERO, Duration::ZERO)
2683            .await
2684            .expect("task should be present");
2685
2686        if !matches!(outcome, TaskJoinOutcome::Incomplete) {
2687            let _ = release_tx.send(());
2688            panic!("expected incomplete task, was {outcome:?}");
2689        }
2690
2691        assert!(!slot.is_empty());
2692
2693        release_tx
2694            .send(())
2695            .expect("blocking task should be waiting");
2696        let outcome = slot
2697            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2698            .await
2699            .expect("task should be present");
2700
2701        let TaskJoinOutcome::Completed(value) = outcome else {
2702            panic!("expected completed task after retry, was {outcome:?}");
2703        };
2704
2705        assert_eq!(value, 42);
2706        assert!(slot.is_empty());
2707    }
2708
2709    #[rstest]
2710    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2711    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2712    async fn shared_task_rejects_insertion_during_empty_drain_reservation() {
2713        let slot = SharedTaskSlot::new();
2714        let (reserved, _, _) = slot
2715            .state
2716            .lock()
2717            .try_reserve_drain()
2718            .expect("empty slot should reserve");
2719
2720        let candidate = TaskSlot::from_handle(task::spawn(async {}));
2721
2722        let rejected = slot
2723            .try_insert_slot(candidate)
2724            .expect_err("draining slot should reject insertion");
2725
2726        assert!(rejected.is_some());
2727        assert!(slot.is_empty());
2728        let mut state = slot.state.lock();
2729        state.slot = reserved;
2730        state.draining = false;
2731    }
2732
2733    #[rstest]
2734    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2735    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2736    async fn shared_task_try_insert_returns_task_when_occupied() {
2737        let slot = SharedTaskSlot::new();
2738        slot.insert(task::spawn(std::future::pending::<()>()));
2739        let candidate = TaskSlot::from_handle(task::spawn(async {}));
2740
2741        let mut rejected = slot
2742            .try_insert_slot(candidate)
2743            .expect_err("occupied slot should reject insertion");
2744        let outcome = finish_task(&mut rejected, TEST_TIMEOUT, TEST_TIMEOUT)
2745            .await
2746            .expect("rejected task should remain present");
2747
2748        assert!(matches!(outcome, TaskJoinOutcome::Completed(())));
2749        assert!(rejected.is_none());
2750        assert!(!slot.is_empty());
2751
2752        slot.abort();
2753        let outcome = slot
2754            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2755            .await
2756            .expect("original task should remain present");
2757        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2758        assert!(slot.is_empty());
2759    }
2760
2761    #[rstest]
2762    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2763    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2764    async fn shared_task_insert_aborts_rejected_task_before_panicking() {
2765        let slot = SharedTaskSlot::new();
2766        slot.insert(task::spawn(std::future::pending::<()>()));
2767        let (future, started_rx, dropped) = pending_with_drop_signal();
2768
2769        let candidate = task::spawn(future);
2770        started_rx.await.expect("candidate task should start");
2771
2772        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| slot.insert(candidate)))
2773            .expect_err("occupied slot should panic");
2774        wait_for_drop(&dropped).await;
2775
2776        assert_eq!(
2777            panic_message(panic.as_ref()),
2778            "shared task slot is already occupied",
2779        );
2780        assert!(!slot.is_empty());
2781        slot.abort();
2782        let outcome = slot
2783            .finish(TEST_TIMEOUT, TEST_TIMEOUT)
2784            .await
2785            .expect("original task should be present");
2786        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2787        assert!(slot.is_empty());
2788    }
2789
2790    #[rstest]
2791    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2792    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2793    async fn dropping_shared_task_slot_aborts_owned_task() {
2794        let slot = SharedTaskSlot::new();
2795        let (future, started_rx, dropped) = pending_with_drop_signal();
2796
2797        slot.insert(task::spawn(future));
2798        started_rx.await.expect("task should start");
2799
2800        drop(slot);
2801        wait_for_drop(&dropped).await;
2802    }
2803
2804    #[rstest]
2805    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2806    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2807    async fn singular_task_preserves_typed_result() {
2808        let mut slot = TaskSlot::new();
2809        slot.spawn(async { 42 }).expect("spawn");
2810        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2811            .await
2812            .expect("task present");
2813
2814        let TaskJoinOutcome::Completed(value) = outcome else {
2815            panic!("expected completed task");
2816        };
2817
2818        assert_eq!(value, 42);
2819        assert!(slot.is_none());
2820    }
2821
2822    #[rstest]
2823    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2824    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2825    async fn task_slot_insert_aborts_rejected_task_before_panicking() {
2826        let mut slot = TaskSlot::from_handle(task::spawn(std::future::pending::<()>()));
2827        let (future, started_rx, dropped) = pending_with_drop_signal();
2828
2829        let candidate = task::spawn(future);
2830        started_rx.await.expect("candidate task should start");
2831
2832        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| slot.insert(candidate)))
2833            .expect_err("occupied slot should panic");
2834        wait_for_drop(&dropped).await;
2835
2836        assert_eq!(
2837            panic_message(panic.as_ref()),
2838            "task slot is already occupied"
2839        );
2840        assert!(slot.is_some());
2841        slot.abort();
2842        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2843            .await
2844            .expect("original task should be present");
2845        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2846        assert!(slot.is_none());
2847    }
2848
2849    #[rstest]
2850    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2851    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2852    async fn task_slot_spawn_rejects_occupied_slot_without_detaching_original() {
2853        let dropped = Arc::new(AtomicBool::new(false));
2854        let dropped_task = Arc::clone(&dropped);
2855        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2856        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
2857
2858        let mut slot = TaskSlot::from_handle(task::spawn(async move {
2859            let _drop = DropSignal(dropped_task);
2860            let _ = started_tx.send(());
2861            let _ = release_rx.await;
2862            42
2863        }));
2864
2865        started_rx.await.expect("original task should start");
2866
2867        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| {
2868            slot.spawn(std::future::pending::<u32>())
2869        }));
2870
2871        let _ = release_tx.send(());
2872        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2873            .await
2874            .expect("task should remain present");
2875        wait_for_drop(&dropped).await;
2876        let panic = panic.expect_err("occupied slot should panic");
2877
2878        let TaskJoinOutcome::Completed(value) = outcome else {
2879            panic!("expected original task to complete, was {outcome:?}");
2880        };
2881
2882        assert_eq!(
2883            panic_message(panic.as_ref()),
2884            "task slot is already occupied"
2885        );
2886        assert_eq!(value, 42);
2887        assert!(dropped.load(Ordering::Acquire));
2888        assert!(slot.is_none());
2889    }
2890
2891    #[rstest]
2892    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2893    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2894    async fn dropping_task_slot_aborts_owned_task() {
2895        let (future, started_rx, dropped) = pending_with_drop_signal();
2896
2897        let slot = TaskSlot::from_handle(task::spawn(future));
2898        started_rx.await.expect("task should start");
2899
2900        drop(slot);
2901        wait_for_drop(&dropped).await;
2902    }
2903
2904    #[rstest]
2905    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
2906    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
2907    async fn singular_task_is_joined_after_forced_abort() {
2908        let dropped = Arc::new(AtomicBool::new(false));
2909        let signal = DropSignal(Arc::clone(&dropped));
2910
2911        let mut slot = TaskSlot::from_handle(task::spawn(async move {
2912            let _signal = signal;
2913            std::future::pending::<()>().await;
2914        }));
2915
2916        let outcome = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT)
2917            .await
2918            .expect("task present");
2919
2920        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2921        assert!(dropped.load(Ordering::Acquire));
2922        assert!(slot.is_none());
2923    }
2924
2925    #[cfg(not(all(feature = "simulation", madsim)))]
2926    #[rstest]
2927    #[tokio::test(start_paused = true)]
2928    async fn singular_task_joins_after_abort_with_paused_time() {
2929        let mut slot = TaskSlot::from_handle(task::spawn(std::future::pending::<()>()));
2930
2931        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2932            .await
2933            .expect("task present");
2934
2935        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
2936        assert!(slot.is_none());
2937    }
2938
2939    #[cfg(not(all(feature = "simulation", madsim)))]
2940    #[rstest]
2941    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2942    async fn singular_task_timeout_preserves_owned_typed_result() {
2943        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2944        let (release_tx, release_rx) = std::sync::mpsc::channel();
2945
2946        let mut slot = TaskSlot::from_handle(tokio::task::spawn_blocking(move || {
2947            let _ = started_tx.send(());
2948            let _ = release_rx.recv();
2949            42
2950        }));
2951
2952        started_rx.await.expect("blocking task should start");
2953
2954        let outcome = finish_task(&mut slot, Duration::ZERO, Duration::ZERO)
2955            .await
2956            .expect("task present");
2957
2958        if !matches!(outcome, TaskJoinOutcome::Incomplete) {
2959            let _ = release_tx.send(());
2960            panic!("expected incomplete task, was {outcome:?}");
2961        }
2962
2963        assert!(slot.is_some());
2964
2965        release_tx
2966            .send(())
2967            .expect("blocking task should be waiting");
2968        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
2969            .await
2970            .expect("task present");
2971
2972        let value = match outcome {
2973            TaskJoinOutcome::Completed(value) => value,
2974            other => panic!("expected completed task after retry, was {other:?}"),
2975        };
2976
2977        assert_eq!(value, 42);
2978        assert!(slot.is_none());
2979    }
2980
2981    #[cfg(not(all(feature = "simulation", madsim)))]
2982    #[rstest]
2983    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2984    async fn singular_task_retry_preserves_forced_abort_classification() {
2985        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
2986        let (release_tx, release_rx) = std::sync::mpsc::channel();
2987
2988        let mut slot = TaskSlot::from_handle(task::spawn(async move {
2989            let _ = started_tx.send(());
2990            let _ = release_rx.recv();
2991            task::yield_now().await;
2992        }));
2993
2994        started_rx.await.expect("blocking task should start");
2995
2996        let outcome = finish_task(&mut slot, Duration::ZERO, Duration::ZERO)
2997            .await
2998            .expect("task present");
2999        assert!(matches!(outcome, TaskJoinOutcome::Incomplete));
3000        assert!(slot.is_some());
3001
3002        release_tx
3003            .send(())
3004            .expect("blocking task should be waiting");
3005        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3006            .await
3007            .expect("task present");
3008
3009        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
3010        assert!(slot.is_none());
3011    }
3012
3013    #[rstest]
3014    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
3015    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
3016    async fn singular_task_reports_unexpected_cancellation() {
3017        let handle = task::spawn(std::future::pending::<()>());
3018        handle.abort();
3019        let mut slot = TaskSlot::from_handle(handle);
3020
3021        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3022            .await
3023            .expect("task present");
3024
3025        let TaskJoinOutcome::Failed(error) = outcome else {
3026            panic!("expected failed task");
3027        };
3028
3029        assert!(error.is_cancelled());
3030        assert!(slot.is_none());
3031    }
3032
3033    #[cfg(not(all(feature = "simulation", madsim)))]
3034    #[rstest]
3035    #[tokio::test]
3036    async fn singular_task_reports_panicked_join() {
3037        let mut slot = TaskSlot::from_handle(task::spawn(async {
3038            panic!("task panic");
3039        }));
3040
3041        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3042            .await
3043            .expect("task should be present");
3044
3045        let TaskJoinOutcome::Failed(error) = outcome else {
3046            panic!("expected failed task");
3047        };
3048
3049        assert!(error.is_panic());
3050        assert!(slot.is_none());
3051    }
3052
3053    #[rstest]
3054    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
3055    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
3056    async fn canceled_singular_finish_preserves_owner_slot() {
3057        let mut slot = TaskSlot::from_handle(task::spawn(std::future::pending::<()>()));
3058
3059        {
3060            let finish = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT);
3061            tokio::pin!(finish);
3062            tokio::select! {
3063                outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
3064                () = task::yield_now() => {}
3065            }
3066        }
3067
3068        assert!(slot.is_some());
3069        let outcome = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT)
3070            .await
3071            .expect("task present");
3072        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
3073        assert!(slot.is_none());
3074    }
3075
3076    #[cfg(not(all(feature = "simulation", madsim)))]
3077    #[rstest]
3078    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3079    async fn canceled_singular_abort_wait_preserves_owner_slot() {
3080        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3081        let (release_tx, release_rx) = std::sync::mpsc::channel();
3082
3083        let mut slot = TaskSlot::from_handle(tokio::task::spawn_blocking(move || {
3084            let _ = started_tx.send(());
3085            let _ = release_rx.recv();
3086            42
3087        }));
3088
3089        started_rx.await.expect("blocking task should start");
3090
3091        {
3092            let finish = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT);
3093            tokio::pin!(finish);
3094            tokio::select! {
3095                outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
3096                () = task::yield_now() => {}
3097            }
3098        }
3099
3100        assert!(slot.is_some());
3101        release_tx
3102            .send(())
3103            .expect("blocking task should be waiting");
3104        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3105            .await
3106            .expect("task present");
3107
3108        let TaskJoinOutcome::Completed(value) = outcome else {
3109            panic!("expected completed task after canceled abort wait, was {outcome:?}");
3110        };
3111
3112        assert_eq!(value, 42);
3113        assert!(slot.is_none());
3114    }
3115
3116    #[cfg(not(all(feature = "simulation", madsim)))]
3117    #[rstest]
3118    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3119    async fn canceled_singular_abort_wait_preserves_abort_classification() {
3120        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3121        let (release_tx, release_rx) = std::sync::mpsc::channel();
3122
3123        let mut slot = TaskSlot::from_handle(task::spawn(async move {
3124            let _ = started_tx.send(());
3125            let _ = release_rx.recv();
3126            task::yield_now().await;
3127        }));
3128
3129        started_rx.await.expect("blocking task should start");
3130
3131        while !slot.abort_requested {
3132            {
3133                let finish = finish_task(&mut slot, Duration::ZERO, TEST_TIMEOUT);
3134                tokio::pin!(finish);
3135                tokio::select! {
3136                    biased;
3137                    outcome = &mut finish => panic!("finish completed unexpectedly: {outcome:?}"),
3138                    () = time::sleep(POLL_INTERVAL) => {}
3139                }
3140            }
3141        }
3142
3143        assert!(slot.is_some());
3144        release_tx
3145            .send(())
3146            .expect("blocking task should be waiting");
3147        let outcome = finish_task(&mut slot, TEST_TIMEOUT, TEST_TIMEOUT)
3148            .await
3149            .expect("task present");
3150
3151        assert!(matches!(outcome, TaskJoinOutcome::Aborted));
3152        assert!(slot.is_none());
3153    }
3154
3155    fn pending_with_drop_signal() -> (
3156        impl Future<Output = ()>,
3157        tokio::sync::oneshot::Receiver<()>,
3158        Arc<AtomicBool>,
3159    ) {
3160        let dropped = Arc::new(AtomicBool::new(false));
3161        let dropped_task = Arc::clone(&dropped);
3162        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3163
3164        let future = async move {
3165            let _drop = DropSignal(dropped_task);
3166            let _ = started_tx.send(());
3167            std::future::pending::<()>().await;
3168        };
3169
3170        (future, started_rx, dropped)
3171    }
3172
3173    async fn wait_for_drop(dropped: &AtomicBool) {
3174        time::timeout(TEST_TIMEOUT, async {
3175            while !dropped.load(Ordering::Acquire) {
3176                task::yield_now().await;
3177            }
3178        })
3179        .await
3180        .expect("task should be dropped");
3181    }
3182
3183    struct DropSignal(Arc<AtomicBool>);
3184
3185    impl Drop for DropSignal {
3186        fn drop(&mut self) {
3187            self.0.store(true, Ordering::Release);
3188        }
3189    }
3190
3191    struct ReentrantDropFuture {
3192        group: Arc<TaskGroup>,
3193        dropped: Arc<AtomicBool>,
3194        polled: Arc<AtomicBool>,
3195    }
3196
3197    struct ReentrantWake {
3198        group: Arc<TaskGroup>,
3199        woke: Arc<AtomicBool>,
3200    }
3201
3202    impl Wake for ReentrantWake {
3203        fn wake(self: Arc<Self>) {
3204            let _ = self.group.is_open();
3205            self.woke.store(true, Ordering::Release);
3206        }
3207    }
3208
3209    impl Future for ReentrantDropFuture {
3210        type Output = ();
3211
3212        fn poll(
3213            self: std::pin::Pin<&mut Self>,
3214            _cx: &mut std::task::Context<'_>,
3215        ) -> std::task::Poll<Self::Output> {
3216            self.polled.store(true, Ordering::Release);
3217            std::task::Poll::Pending
3218        }
3219    }
3220
3221    impl Drop for ReentrantDropFuture {
3222        fn drop(&mut self) {
3223            let _ = self.group.is_open();
3224            self.dropped.store(true, Ordering::Release);
3225        }
3226    }
3227}