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