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