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