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