1use std::future::Future;
16use std::sync::atomic::AtomicBool;
17use std::sync::atomic::AtomicU64;
18use std::sync::atomic::AtomicU8;
19use std::sync::atomic::AtomicUsize;
20use std::sync::atomic::Ordering;
21use std::sync::Arc;
22use std::sync::OnceLock;
23use std::time::Duration;
24use std::time::Instant;
25
26use futures::future::join_all;
27use parking_lot::Mutex;
28use parking_lot::MutexGuard;
29use tokio::sync::Notify;
30use tokio_util::sync::CancellationToken;
31
32use crate::error::RuntimeError;
33use crate::error::RuntimeResult;
34use crate::shutdown_deadline::ShutdownDeadline;
35use crate::shutdown_deadline::ABORT_CONFIRMATION_TIMEOUT;
36use crate::task_group::TaskGroup;
37use crate::task_group::TaskGroupId;
38use crate::task_group::TaskKind;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(u8)]
46pub enum OperationOutcome {
47 Completed = 1,
49 Cancelled = 2,
51 DeadlineExceeded = 3,
53 OwnerCancelled = 4,
55 Panicked = 5,
57 Aborted = 6,
59}
60
61impl OperationOutcome {
62 pub const ALL: [Self; 6] = [
64 Self::Completed,
65 Self::Cancelled,
66 Self::DeadlineExceeded,
67 Self::OwnerCancelled,
68 Self::Panicked,
69 Self::Aborted,
70 ];
71
72 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Completed => "completed",
76 Self::Cancelled => "cancelled",
77 Self::DeadlineExceeded => "deadline_exceeded",
78 Self::OwnerCancelled => "owner_cancelled",
79 Self::Panicked => "panicked",
80 Self::Aborted => "aborted",
81 }
82 }
83}
84
85pub trait OperationOutcomeObserver: std::fmt::Debug + Send + Sync {
90 fn on_outcome(&self, kind: TaskKind, outcome: OperationOutcome);
92}
93
94#[derive(Debug, Clone)]
101pub struct OperationContext {
102 inner: Arc<OperationContextInner>,
103 task_kind: TaskKind,
104}
105
106#[derive(Debug, Clone, Copy)]
108pub struct OperationWaitPolicy {
109 graceful: ShutdownDeadline,
110 confirmation: ShutdownDeadline,
111}
112
113impl OperationWaitPolicy {
114 pub fn new(graceful: ShutdownDeadline, confirmation: ShutdownDeadline) -> Self {
119 Self {
120 graceful: graceful.earliest(confirmation),
121 confirmation,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum OperationWaitOutcome {
129 Completed,
131 AbortConfirmed,
133 Unconfirmed {
135 remaining_tasks: usize,
137 },
138}
139
140#[derive(Debug)]
141struct OperationContextInner {
142 id: u64,
144 cancellation: CancellationToken,
145 deadline: Option<Instant>,
146 accepting: AtomicBool,
147 spawn_gate: Mutex<()>,
149 owner_id: AtomicU64,
151 active: AtomicUsize,
153 idle: Notify,
155 outcomes: [AtomicU64; 6],
156 observer: OnceLock<Arc<dyn OperationOutcomeObserver>>,
157}
158
159impl OperationContext {
160 pub async fn wait_with_policy(
173 &self,
174 owner: &TaskGroup,
175 policy: OperationWaitPolicy,
176 ) -> RuntimeResult<OperationWaitOutcome> {
177 self.ensure_owner(owner.id())?;
178 self.close_admission();
179 if self.wait_until_settled(owner, policy.graceful.instant()).await? {
180 return Ok(OperationWaitOutcome::Completed);
181 }
182 let remaining = owner.operation_task_ids(self.inner.id);
183 join_all(
184 remaining
185 .into_iter()
186 .map(|task_id| owner.abort_task_until(task_id, policy.confirmation)),
187 )
188 .await;
189 let remaining_tasks = owner.operation_task_ids(self.inner.id).len();
190 Ok(if remaining_tasks == 0 {
191 OperationWaitOutcome::AbortConfirmed
192 } else {
193 OperationWaitOutcome::Unconfirmed { remaining_tasks }
194 })
195 }
196
197 pub fn new(deadline: Instant, task_kind: TaskKind) -> Self {
199 Self::from_parts(Some(deadline), task_kind)
200 }
201
202 pub fn without_deadline(task_kind: TaskKind) -> Self {
204 Self::from_parts(None, task_kind)
205 }
206
207 fn from_parts(deadline: Option<Instant>, task_kind: TaskKind) -> Self {
208 static NEXT_OPERATION_ID: AtomicU64 = AtomicU64::new(1);
209 Self {
210 inner: Arc::new(OperationContextInner {
211 id: NEXT_OPERATION_ID.fetch_add(1, Ordering::Relaxed),
212 cancellation: CancellationToken::new(),
213 deadline,
214 accepting: AtomicBool::new(true),
215 spawn_gate: Mutex::new(()),
216 owner_id: AtomicU64::new(0),
217 active: AtomicUsize::new(0),
218 idle: Notify::new(),
219 outcomes: std::array::from_fn(|_| AtomicU64::new(0)),
220 observer: OnceLock::new(),
221 }),
222 task_kind,
223 }
224 }
225
226 pub(crate) fn id(&self) -> u64 {
227 self.inner.id
228 }
229
230 pub fn cancellation_token(&self) -> CancellationToken {
232 self.inner.cancellation.clone()
233 }
234
235 pub fn deadline(&self) -> Option<Instant> {
237 self.inner.deadline
238 }
239
240 pub fn task_kind(&self) -> TaskKind {
242 self.task_kind
243 }
244
245 pub fn with_task_kind(&self, task_kind: TaskKind) -> Self {
250 Self {
251 inner: Arc::clone(&self.inner),
252 task_kind,
253 }
254 }
255
256 pub fn is_cancelled(&self) -> bool {
258 self.inner.cancellation.is_cancelled()
259 }
260
261 pub fn cancel(&self) {
263 let _spawn_guard = self.inner.spawn_gate.lock();
264 self.inner.accepting.store(false, Ordering::Release);
265 self.inner.cancellation.cancel();
266 }
267
268 pub fn close_admission(&self) {
271 let _spawn_guard = self.inner.spawn_gate.lock();
272 self.inner.accepting.store(false, Ordering::Release);
273 }
274
275 pub fn active_task_count(&self) -> usize {
277 self.inner.active.load(Ordering::Acquire)
278 }
279
280 pub fn outcomes(&self) -> [(OperationOutcome, u64); 6] {
285 OperationOutcome::ALL.map(|outcome| {
286 (
287 outcome,
288 self.inner.outcomes[outcome as usize - 1].load(Ordering::Acquire),
289 )
290 })
291 }
292
293 pub fn set_outcome_observer(&self, observer: Arc<dyn OperationOutcomeObserver>) -> RuntimeResult<()> {
299 let _gate = self.inner.spawn_gate.lock();
300 if self.inner.owner_id.load(Ordering::Acquire) != 0 || self.inner.observer.set(observer).is_err() {
301 return Err(RuntimeError::context_unavailable(
302 crate::RuntimeOperation::SpawnOperation,
303 ));
304 }
305 Ok(())
306 }
307
308 pub async fn cancel_and_wait(&self, owner: &TaskGroup, timeout: Duration) -> RuntimeResult<bool> {
323 self.cancel();
324 self.wait(owner, timeout).await
325 }
326
327 pub async fn cancel_and_wait_until(&self, owner: &TaskGroup, deadline: ShutdownDeadline) -> RuntimeResult<bool> {
336 self.cancel();
337 self.wait_until(owner, deadline).await
338 }
339
340 pub async fn wait(&self, owner: &TaskGroup, timeout: Duration) -> RuntimeResult<bool> {
357 self.wait_for_tasks(owner, Instant::now() + timeout, ABORT_CONFIRMATION_TIMEOUT)
358 .await
359 }
360
361 pub async fn wait_until(&self, owner: &TaskGroup, deadline: ShutdownDeadline) -> RuntimeResult<bool> {
375 self.wait_for_tasks(owner, deadline.instant(), Duration::ZERO).await
376 }
377
378 async fn wait_for_tasks(
381 &self,
382 owner: &TaskGroup,
383 deadline: Instant,
384 abort_confirmation: Duration,
385 ) -> RuntimeResult<bool> {
386 if self.wait_until_settled(owner, deadline).await? {
387 return Ok(true);
388 }
389 let remaining = owner.operation_task_ids(self.inner.id);
392 join_all(
393 remaining
394 .into_iter()
395 .map(|task_id| owner.abort_task_and_wait(task_id, abort_confirmation)),
396 )
397 .await;
398 Ok(false)
399 }
400
401 async fn wait_until_settled(&self, owner: &TaskGroup, deadline: Instant) -> RuntimeResult<bool> {
402 self.ensure_owner(owner.id())?;
403 loop {
404 let idle = self.inner.idle.notified();
406 if self.inner.active.load(Ordering::Acquire) == 0 {
407 let unsettled = owner.operation_task_ids(self.inner.id);
412 if unsettled.is_empty() {
413 return Ok(true);
414 }
415 let deadline = ShutdownDeadline::at(deadline);
416 let settled = join_all(
417 unsettled
418 .into_iter()
419 .map(|task_id| owner.wait_task_until(task_id, deadline)),
420 )
421 .await;
422 if settled.into_iter().all(|settled| settled) {
423 continue;
424 }
425 break;
426 }
427 if tokio::time::timeout_at(deadline.into(), idle).await.is_err() {
428 break;
429 }
430 }
431
432 Ok(false)
433 }
434
435 pub(crate) fn prepare_spawn(&self, owner_id: TaskGroupId) -> RuntimeResult<OperationTaskRegistration> {
440 self.bind_owner(owner_id)?;
441 if !self.inner.accepting.load(Ordering::Acquire)
442 || self.is_cancelled()
443 || self.inner.deadline.is_some_and(|deadline| deadline <= Instant::now())
444 {
445 return Err(RuntimeError::closed(crate::RuntimeOperation::SpawnOperation));
446 }
447 self.inner.active.fetch_add(1, Ordering::AcqRel);
448 Ok(OperationTaskRegistration::new(Arc::clone(&self.inner), self.task_kind))
449 }
450
451 pub(crate) fn spawn_guard(&self) -> MutexGuard<'_, ()> {
452 self.inner.spawn_gate.lock()
453 }
454
455 async fn run<F>(&self, future: F) -> OperationOutcome
456 where
457 F: Future<Output = ()>,
458 {
459 match self.inner.deadline {
460 Some(deadline) => {
461 tokio::select! {
462 biased;
463 _ = self.inner.cancellation.cancelled() => OperationOutcome::Cancelled,
464 _ = tokio::time::sleep_until(deadline.into()) => OperationOutcome::DeadlineExceeded,
465 _ = future => OperationOutcome::Completed,
466 }
467 }
468 None => {
469 tokio::select! {
470 biased;
471 _ = self.inner.cancellation.cancelled() => OperationOutcome::Cancelled,
472 _ = future => OperationOutcome::Completed,
473 }
474 }
475 }
476 }
477
478 fn bind_owner(&self, owner_id: TaskGroupId) -> RuntimeResult<()> {
479 match self
480 .inner
481 .owner_id
482 .compare_exchange(0, owner_id.as_u64(), Ordering::AcqRel, Ordering::Acquire)
483 {
484 Ok(_) => Ok(()),
485 Err(bound_owner) if bound_owner == owner_id.as_u64() => Ok(()),
486 Err(_) => Err(RuntimeError::internal_failure(crate::RuntimeOperation::OperationOwner)),
487 }
488 }
489
490 fn ensure_owner(&self, owner_id: TaskGroupId) -> RuntimeResult<()> {
491 match self.inner.owner_id.load(Ordering::Acquire) {
492 0 => Ok(()),
493 bound_owner if bound_owner == owner_id.as_u64() => Ok(()),
494 _ => Err(RuntimeError::internal_failure(crate::RuntimeOperation::OperationOwner)),
495 }
496 }
497}
498
499pub(crate) struct OperationTaskRegistration {
500 state: Arc<OperationTaskRegistrationState>,
501}
502
503const TASK_REGISTERED: u8 = 1;
505const TASK_COMPLETED: u8 = 2;
507
508struct OperationTaskRegistrationState {
509 progress: AtomicU8,
513 outcome: AtomicU8,
514 operation: Arc<OperationContextInner>,
515 task_kind: TaskKind,
516}
517
518impl OperationTaskRegistration {
519 fn new(operation: Arc<OperationContextInner>, task_kind: TaskKind) -> Self {
520 Self {
521 state: Arc::new(OperationTaskRegistrationState {
522 progress: AtomicU8::new(0),
523 outcome: AtomicU8::new(0),
524 operation,
525 task_kind,
526 }),
527 }
528 }
529
530 pub(crate) fn guard(&self) -> OperationTaskGuard {
531 OperationTaskGuard {
532 state: Arc::clone(&self.state),
533 outcome: OperationOutcome::Aborted,
534 }
535 }
536
537 pub(crate) fn finish_registration(self) {
540 let previous = self.state.progress.fetch_or(TASK_REGISTERED, Ordering::AcqRel);
541 if previous & TASK_COMPLETED != 0 {
542 self.state.record(self.state.outcome.load(Ordering::Acquire));
543 }
544 }
545}
546
547pub(crate) struct OperationTaskGuard {
548 state: Arc<OperationTaskRegistrationState>,
549 outcome: OperationOutcome,
550}
551
552impl Drop for OperationTaskGuard {
553 fn drop(&mut self) {
554 let outcome = if std::thread::panicking() {
555 OperationOutcome::Panicked
556 } else {
557 self.outcome
558 };
559 self.state.outcome.store(outcome as u8, Ordering::Release);
560 let previous = self.state.progress.fetch_or(TASK_COMPLETED, Ordering::AcqRel);
561 if previous & TASK_REGISTERED != 0 {
562 self.state.record(outcome as u8);
563 }
564 let operation = &self.state.operation;
566 if operation.active.fetch_sub(1, Ordering::AcqRel) == 1 {
567 operation.idle.notify_waiters();
568 }
569 }
570}
571
572impl OperationTaskRegistrationState {
573 fn record(&self, value: u8) {
574 let Some(outcome) = OperationOutcome::ALL
575 .into_iter()
576 .find(|outcome| *outcome as u8 == value)
577 else {
578 return;
579 };
580 self.operation.outcomes[value as usize - 1].fetch_add(1, Ordering::Release);
581 if let Some(observer) = self.operation.observer.get() {
582 observer.on_outcome(self.task_kind, outcome);
583 }
584 }
585}
586
587pub(crate) struct OperationExecution<F> {
589 future: F,
590 finalizer: OperationTaskGuard,
591 operation: OperationContext,
592 owner_cancellation: Option<CancellationToken>,
593}
594
595impl<F: Future<Output = ()>> OperationExecution<F> {
596 pub(crate) fn new(
597 future: F,
598 finalizer: OperationTaskGuard,
599 operation: OperationContext,
600 owner_cancellation: Option<CancellationToken>,
601 ) -> Self {
602 Self {
603 future,
604 finalizer,
605 operation,
606 owner_cancellation,
607 }
608 }
609
610 pub(crate) async fn run(self) {
611 let mut finalizer = self.finalizer;
612 let outcome = match self.owner_cancellation {
613 Some(owner) => tokio::select! {
614 biased;
615 _ = owner.cancelled() => OperationOutcome::OwnerCancelled,
616 outcome = self.operation.run(self.future) => outcome,
617 },
618 None => self.operation.run(self.future).await,
619 };
620 finalizer.outcome = outcome;
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::RuntimeContext;
628
629 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
630 async fn wait_returns_only_after_the_owner_settles_the_tasks() {
631 let context = RuntimeContext::from_current("operation-settlement");
632 let owner = context.service_context("owner").task_group().clone();
633 let operation = OperationContext::without_deadline(TaskKind::Worker);
634 let (finish_tx, finish_rx) = tokio::sync::oneshot::channel::<()>();
635 let task_id = owner
636 .spawn_operation(&operation, "settles-late", async move {
637 let _ = finish_rx.await;
638 })
639 .unwrap();
640
641 let (locked_tx, locked_rx) = std::sync::mpsc::channel();
644 let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
645 let gate_owner = owner.clone();
646 let holder = std::thread::spawn(move || {
647 let _settlement = gate_owner.lock_settlement_for_test();
648 locked_tx.send(()).unwrap();
649 release_rx.recv().unwrap();
650 });
651 locked_rx.recv().unwrap();
652 finish_tx.send(()).unwrap();
653 while operation.active_task_count() != 0 {
654 tokio::task::yield_now().await;
655 }
656
657 let mut wait = Box::pin(operation.wait(&owner, Duration::from_secs(5)));
658 assert!(
659 futures::poll!(wait.as_mut()).is_pending(),
660 "a task the owner has not settled still belongs to the operation"
661 );
662 assert!(owner.contains_task(task_id));
663
664 release_tx.send(()).unwrap();
665 holder.join().unwrap();
666 assert!(wait.await.unwrap());
667 assert!(!owner.contains_task(task_id));
668 assert_eq!(owner.task_count(), 0);
669 }
670
671 struct OutcomeFuture {
672 dropped: Arc<AtomicBool>,
673 ready: bool,
674 panic_poll: bool,
675 panic_drop: bool,
676 }
677
678 impl Future for OutcomeFuture {
679 type Output = ();
680 fn poll(self: std::pin::Pin<&mut Self>, _: &mut std::task::Context<'_>) -> std::task::Poll<()> {
681 assert!(!self.panic_poll, "operation poll panic");
682 if self.ready {
683 std::task::Poll::Ready(())
684 } else {
685 std::task::Poll::Pending
686 }
687 }
688 }
689
690 impl Drop for OutcomeFuture {
691 fn drop(&mut self) {
692 self.dropped.store(true, Ordering::Release);
693 assert!(!self.panic_drop, "operation destructor panic");
694 }
695 }
696
697 #[derive(Debug)]
698 struct RecordingOutcomeObserver {
699 dropped: Arc<AtomicBool>,
700 events: Mutex<Vec<OperationOutcome>>,
701 }
702
703 impl OperationOutcomeObserver for RecordingOutcomeObserver {
704 fn on_outcome(&self, _kind: TaskKind, outcome: OperationOutcome) {
705 assert!(
706 self.dropped.load(Ordering::Acquire),
707 "outcome preceded future destruction"
708 );
709 self.events.lock().push(outcome);
710 }
711 }
712
713 #[tokio::test(start_paused = true)]
714 async fn all_outcomes_are_recorded_once_after_future_destruction() {
715 for expected in OperationOutcome::ALL {
716 let runtime = RuntimeContext::from_current("operation-outcome");
717 let owner = runtime.service_context("operation-owner");
718 let operation = if expected == OperationOutcome::DeadlineExceeded {
719 OperationContext::new(Instant::now() + Duration::from_secs(60), TaskKind::Worker)
720 } else {
721 OperationContext::without_deadline(TaskKind::Worker)
722 };
723 let dropped = Arc::new(AtomicBool::new(false));
724 let observer = Arc::new(RecordingOutcomeObserver {
725 dropped: dropped.clone(),
726 events: Mutex::new(Vec::new()),
727 });
728 operation.set_outcome_observer(observer.clone()).unwrap();
729 let id = owner
730 .task_group()
731 .spawn_operation(
732 &operation,
733 "selected-outcome",
734 OutcomeFuture {
735 dropped,
736 ready: expected == OperationOutcome::Completed,
737 panic_poll: expected == OperationOutcome::Panicked,
738 panic_drop: false,
739 },
740 )
741 .unwrap();
742 match expected {
743 OperationOutcome::Cancelled => operation.cancel(),
744 OperationOutcome::OwnerCancelled => owner.task_group().cancel(),
745 OperationOutcome::DeadlineExceeded => tokio::time::advance(Duration::from_secs(61)).await,
746 OperationOutcome::Aborted => {
747 assert!(owner.task_group().abort_task(id));
748 }
749 OperationOutcome::Completed | OperationOutcome::Panicked => {}
750 }
751 while owner.task_group().task_count() != 0 {
752 tokio::task::yield_now().await;
753 }
754 assert_eq!(*observer.events.lock(), [expected]);
755 assert_eq!(operation.active_task_count(), 0);
756 for (outcome, count) in operation.outcomes() {
757 assert_eq!(count, u64::from(outcome == expected), "{expected:?}: {outcome:?}");
758 }
759 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
760 assert_eq!(report.is_healthy(), expected != OperationOutcome::Panicked);
761 }
762 }
763
764 #[tokio::test]
765 async fn destructor_panic_is_an_operation_failure_and_rejected_work_has_no_outcome() {
766 let runtime = RuntimeContext::from_current("operation-destructor");
767 let owner = runtime.service_context("operation-owner");
768 let operation = OperationContext::without_deadline(TaskKind::Worker);
769 let dropped = Arc::new(AtomicBool::new(false));
770 let observer = Arc::new(RecordingOutcomeObserver {
771 dropped: dropped.clone(),
772 events: Mutex::new(Vec::new()),
773 });
774 operation.set_outcome_observer(observer.clone()).unwrap();
775 owner
776 .task_group()
777 .spawn_operation(
778 &operation,
779 "destructor-panic",
780 OutcomeFuture {
781 dropped,
782 ready: true,
783 panic_poll: false,
784 panic_drop: true,
785 },
786 )
787 .unwrap();
788 while owner.task_group().task_count() != 0 {
789 tokio::task::yield_now().await;
790 }
791 assert_eq!(*observer.events.lock(), [OperationOutcome::Panicked]);
792 runtime.shutdown_tasks(Duration::from_secs(1)).await;
793
794 let rejected = OperationContext::without_deadline(TaskKind::Worker);
795 assert!(owner
796 .task_group()
797 .spawn_operation(&rejected, "closed", async {})
798 .is_err());
799 assert_eq!(rejected.active_task_count(), 0);
800 assert!(rejected.outcomes().into_iter().all(|(_, count)| count == 0));
801 }
802
803 #[tokio::test]
804 async fn completed_operations_leave_no_registry_history() {
805 const TASKS: usize = 1_024;
806
807 let runtime = RuntimeContext::from_current("operation-churn-test");
808 let owner = runtime.service_context("operations");
809 let baseline_components = owner.task_group().component_count();
810 let operation = OperationContext::without_deadline(TaskKind::Worker);
811
812 for _ in 0..TASKS {
813 owner
814 .task_group()
815 .spawn_operation(&operation, "short-operation", async {})
816 .expect("operation task should spawn");
817 }
818
819 tokio::time::timeout(Duration::from_secs(5), async {
820 while operation.active_task_count() != 0 || owner.task_group().task_count() != 0 {
821 tokio::task::yield_now().await;
822 }
823 })
824 .await
825 .expect("operation tasks should complete");
826
827 assert_eq!(owner.task_group().component_count(), baseline_components);
828 assert_eq!(operation.outcomes()[0], (OperationOutcome::Completed, TASKS as u64));
829 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
830 assert!(report.is_healthy(), "{}", report.to_json());
831 }
832
833 #[tokio::test]
834 async fn cancellation_joins_only_operation_tasks() {
835 let runtime = RuntimeContext::from_current("operation-cancellation-test");
836 let owner = runtime.service_context("operations");
837 let first = OperationContext::without_deadline(TaskKind::Worker);
838 let second = OperationContext::without_deadline(TaskKind::Worker);
839
840 owner
841 .task_group()
842 .spawn_operation(&first, "first-operation", std::future::pending())
843 .expect("first operation should spawn");
844 owner
845 .task_group()
846 .spawn_operation(&second, "second-operation", std::future::pending())
847 .expect("second operation should spawn");
848
849 assert!(first
850 .cancel_and_wait(owner.task_group(), Duration::from_secs(1))
851 .await
852 .expect("first operation should use its bound owner"));
853 assert_eq!(first.active_task_count(), 0);
854 assert_eq!(second.active_task_count(), 1);
855 assert_eq!(
856 owner.task_group().lifecycle_state(),
857 crate::TaskGroupLifecycleState::Open
858 );
859
860 assert!(second
861 .cancel_and_wait(owner.task_group(), Duration::from_secs(1))
862 .await
863 .expect("second operation should use its bound owner"));
864 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
865 assert!(report.is_healthy(), "{}", report.to_json());
866 }
867
868 #[tokio::test]
869 async fn wait_confirms_cancellation_of_an_overrunning_operation() {
870 struct DropMarker(Arc<AtomicBool>);
871
872 impl Drop for DropMarker {
873 fn drop(&mut self) {
874 self.0.store(true, Ordering::Release);
875 }
876 }
877
878 let runtime = RuntimeContext::from_current("operation-abort-confirmation-test");
879 let owner = runtime.service_context("operations");
880 let operation = OperationContext::without_deadline(TaskKind::Worker);
881 let started = Arc::new(AtomicBool::new(false));
882 let dropped = Arc::new(AtomicBool::new(false));
883
884 owner
885 .task_group()
886 .spawn_operation(&operation, "overrunning-operation", {
887 let started = Arc::clone(&started);
888 let dropped = Arc::clone(&dropped);
889 async move {
890 let _marker = DropMarker(dropped);
891 started.store(true, Ordering::Release);
892 std::future::pending::<()>().await
893 }
894 })
895 .expect("operation task should spawn");
896
897 tokio::time::timeout(Duration::from_secs(1), async {
898 while !started.load(Ordering::Acquire) {
899 tokio::task::yield_now().await;
900 }
901 })
902 .await
903 .expect("operation task should start");
904
905 assert!(
906 !operation
907 .wait(owner.task_group(), Duration::from_millis(10))
908 .await
909 .expect("operation should use its bound owner"),
910 "an overrunning operation must not report a clean drain"
911 );
912 assert!(
913 dropped.load(Ordering::Acquire),
914 "wait should confirm cancellation before returning"
915 );
916 assert_eq!(operation.active_task_count(), 0);
917
918 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
919 assert!(report.is_healthy(), "{}", report.to_json());
920 }
921
922 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
923 async fn wait_until_returns_at_its_deadline_without_confirming_the_abort() {
924 struct BlockingDrop(Option<std::sync::mpsc::Receiver<()>>);
927
928 impl Drop for BlockingDrop {
929 fn drop(&mut self) {
930 if let Some(release) = self.0.take() {
931 let _ = release.recv();
932 }
933 }
934 }
935
936 let runtime = RuntimeContext::from_current("operation-wait-until-test");
937 let owner = runtime.service_context("operations");
938 let operation = OperationContext::without_deadline(TaskKind::Worker);
939 let started = Arc::new(AtomicBool::new(false));
940 let (release_tx, release_rx) = std::sync::mpsc::channel();
941
942 owner
943 .task_group()
944 .spawn_operation(&operation, "slow-to-drop-operation", {
945 let started = Arc::clone(&started);
946 async move {
947 let _guard = BlockingDrop(Some(release_rx));
948 started.store(true, Ordering::Release);
949 std::future::pending::<()>().await
950 }
951 })
952 .expect("operation task should spawn");
953 tokio::time::timeout(Duration::from_secs(1), async {
954 while !started.load(Ordering::Acquire) {
955 tokio::task::yield_now().await;
956 }
957 })
958 .await
959 .expect("operation task should start");
960
961 let deadline = ShutdownDeadline::after(Duration::from_millis(20));
962 assert!(!operation
963 .wait_until(owner.task_group(), deadline)
964 .await
965 .expect("operation should use its bound owner"));
966 assert_eq!(
967 operation.active_task_count(),
968 1,
969 "wait_until must not wait for the aborted future to be dropped"
970 );
971
972 release_tx.send(()).unwrap();
973 assert!(
974 operation
975 .wait(owner.task_group(), Duration::from_secs(5))
976 .await
977 .expect("operation should use its bound owner"),
978 "the aborted task settles once its future is dropped"
979 );
980 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
981 assert!(report.is_healthy(), "{}", report.to_json());
982 }
983
984 #[tokio::test]
985 async fn cancel_and_wait_until_joins_a_cooperative_operation() {
986 let runtime = RuntimeContext::from_current("operation-cancel-until-test");
987 let owner = runtime.service_context("operations");
988 let operation = OperationContext::without_deadline(TaskKind::Worker);
989 owner
990 .task_group()
991 .spawn_operation(&operation, "pending-operation", std::future::pending())
992 .expect("operation task should spawn");
993
994 assert!(operation
995 .cancel_and_wait_until(owner.task_group(), ShutdownDeadline::after(Duration::from_secs(5)))
996 .await
997 .expect("operation should use its bound owner"));
998 assert_eq!(operation.active_task_count(), 0);
999 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
1000 assert!(report.is_healthy(), "{}", report.to_json());
1001 }
1002
1003 #[tokio::test]
1004 async fn owner_shutdown_cancels_operation_tasks() {
1005 let runtime = RuntimeContext::from_current("operation-owner-shutdown-test");
1006 let owner = runtime.service_context("operations");
1007 let operation = OperationContext::without_deadline(TaskKind::Worker);
1008
1009 owner
1010 .task_group()
1011 .spawn_operation(&operation, "owned-operation", std::future::pending())
1012 .expect("operation task should spawn");
1013
1014 let report = runtime.shutdown_tasks(Duration::from_secs(1)).await;
1015
1016 assert!(report.is_healthy(), "{}", report.to_json());
1017 assert_eq!(operation.active_task_count(), 0);
1018 }
1019}