Skip to main content

rocketmq_runtime/
operation.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Why an accepted operation task finished, after its future was destroyed.
41///
42/// Completion means normal return, not business success. Explicit aborts are
43/// distinct from a cancellation branch selected by the task.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(u8)]
46pub enum OperationOutcome {
47    /// The future returned normally.
48    Completed = 1,
49    /// The operation-local cancellation branch won.
50    Cancelled = 2,
51    /// The operation's deadline branch won.
52    DeadlineExceeded = 3,
53    /// The component owner's cancellation branch won.
54    OwnerCancelled = 4,
55    /// Polling or destroying the future panicked.
56    Panicked = 5,
57    /// The future was discarded before selecting another terminal outcome.
58    Aborted = 6,
59}
60
61impl OperationOutcome {
62    /// All outcomes in counter snapshot order.
63    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    /// Stable, low-cardinality diagnostic label.
73    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
85/// Observes task outcomes after future destruction.
86///
87/// Callbacks must be short, nonblocking and panic-free, including during panic
88/// unwinding. They must not flush exporters or retain request data.
89pub trait OperationOutcomeObserver: std::fmt::Debug + Send + Sync {
90    /// Records one terminal task outcome using bounded labels.
91    fn on_outcome(&self, kind: TaskKind, outcome: OperationOutcome);
92}
93
94/// Cancellation, deadline, and task-class metadata for one bounded operation.
95///
96/// An operation is not a lifecycle owner and does not create a child
97/// [`TaskGroup`]. Tasks spawned for the operation remain registered with a
98/// fixed component owner while this context supplies operation-local
99/// cancellation and bounded join semantics.
100#[derive(Debug, Clone)]
101pub struct OperationContext {
102    inner: Arc<OperationContextInner>,
103    task_kind: TaskKind,
104}
105
106/// Absolute deadlines for graceful completion and subsequent abort confirmation.
107#[derive(Debug, Clone, Copy)]
108pub struct OperationWaitPolicy {
109    graceful: ShutdownDeadline,
110    confirmation: ShutdownDeadline,
111}
112
113impl OperationWaitPolicy {
114    /// Creates a policy whose graceful phase cannot extend past confirmation.
115    ///
116    /// Passing the same deadline requests aborts at expiry without extending
117    /// the wait. An earlier confirmation deadline also tightens the grace phase.
118    pub fn new(graceful: ShutdownDeadline, confirmation: ShutdownDeadline) -> Self {
119        Self {
120            graceful: graceful.earliest(confirmation),
121            confirmation,
122        }
123    }
124}
125
126/// Whether an operation's task destruction was confirmed when waiting stopped.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum OperationWaitOutcome {
129    /// All tasks settled during the graceful phase.
130    Completed,
131    /// All tasks settled after aborts were requested.
132    AbortConfirmed,
133    /// The confirmation deadline expired with tasks still registered.
134    Unconfirmed {
135        /// Tasks whose final destruction and owner bookkeeping remain unconfirmed.
136        remaining_tasks: usize,
137    },
138}
139
140#[derive(Debug)]
141struct OperationContextInner {
142    /// Process-unique id; the owner's task registry tags operation tasks with it.
143    id: u64,
144    cancellation: CancellationToken,
145    deadline: Option<Instant>,
146    accepting: AtomicBool,
147    /// Serializes admission with `close_admission` and `cancel`.
148    spawn_gate: Mutex<()>,
149    /// Id of the component owner, or zero before the first submission.
150    owner_id: AtomicU64,
151    /// Accepted tasks whose futures have not yet been destroyed.
152    active: AtomicUsize,
153    /// Woken when `active` drops to zero.
154    idle: Notify,
155    outcomes: [AtomicU64; 6],
156    observer: OnceLock<Arc<dyn OperationOutcomeObserver>>,
157}
158
159impl OperationContext {
160    /// Closes operation admission and asynchronously waits using explicit deadlines.
161    ///
162    /// Tasks unfinished at the graceful deadline are asked to abort. The second
163    /// deadline bounds confirmation of future destruction and owner settlement.
164    /// An unconfirmed outcome leaves those tasks visible to the owner's report.
165    /// Deadlines bound cooperative waiting; blocked executor threads can delay
166    /// polling and this method does not promise a hard wall-clock return time.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error without closing admission if `owner` is not this
171    /// operation's bound component owner.
172    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    /// Creates an operation with an absolute deadline.
198    pub fn new(deadline: Instant, task_kind: TaskKind) -> Self {
199        Self::from_parts(Some(deadline), task_kind)
200    }
201
202    /// Creates an operation that remains active until explicitly cancelled.
203    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    /// Returns the operation-local cancellation token.
231    pub fn cancellation_token(&self) -> CancellationToken {
232        self.inner.cancellation.clone()
233    }
234
235    /// Returns the absolute deadline, when one was configured.
236    pub fn deadline(&self) -> Option<Instant> {
237        self.inner.deadline
238    }
239
240    /// Returns the task classification applied to operation tasks.
241    pub fn task_kind(&self) -> TaskKind {
242        self.task_kind
243    }
244
245    /// Returns a view of this operation with a different task classification.
246    ///
247    /// Cancellation, deadline, owner binding, and active-task tracking remain
248    /// shared with the original context.
249    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    /// Returns whether cancellation has been requested.
257    pub fn is_cancelled(&self) -> bool {
258        self.inner.cancellation.is_cancelled()
259    }
260
261    /// Requests cancellation for every task in this operation.
262    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    /// Closes task admission without cancelling tasks that were already
269    /// accepted.
270    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    /// Returns the number of operation tasks that have not completed.
276    pub fn active_task_count(&self) -> usize {
277        self.inner.active.load(Ordering::Acquire)
278    }
279
280    /// Reads counters without retaining individual task history.
281    ///
282    /// Counters share this operation's lifetime and are individually atomic;
283    /// completions can occur between fields in this snapshot.
284    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    /// Binds an observer before the first submission attempt.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error after owner binding or an earlier observer installation.
298    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    /// Cancels this operation and waits for all tasks registered with `owner`.
309    ///
310    /// Returns `true` when every task completed before the shared timeout.
311    /// Tasks still running at the deadline are aborted and awaited for a
312    /// confirmation window of at most one second beyond it. `false` does not
313    /// distinguish a confirmed abort from work whose destruction is still
314    /// unconfirmed. Use [`Self::wait_with_policy`] after [`Self::cancel`] for
315    /// explicit deadlines and completion evidence. Timers require the runtime
316    /// to make progress; they are not a hard wall-clock return guarantee.
317    ///
318    /// # Errors
319    ///
320    /// Returns an error when `owner` differs from the component owner used to
321    /// spawn the operation.
322    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    /// Cancels this operation and bounds its asynchronous wait by `deadline`.
328    ///
329    /// Behaves like [`Self::wait_until`] after requesting cancellation.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error when `owner` differs from the component owner used to
334    /// spawn the operation.
335    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    /// Waits until no operation task is active and `owner` has settled every
341    /// one, without requesting cancellation. Tasks still running at the
342    /// deadline are aborted and awaited for a confirmation window of at most
343    /// one second, so the call can return up to one second after `timeout`.
344    ///
345    /// Tasks accepted during the wait are waited for too; close admission
346    /// first when new tasks may still be submitted. Returns `true` when every
347    /// task finished before the deadline; `owner` then no longer lists any of
348    /// them. `false` does not confirm destruction. Use [`Self::wait_until`] to
349    /// avoid an extra confirmation allowance, or [`Self::wait_with_policy`]
350    /// to distinguish confirmed aborts from unconfirmed work.
351    ///
352    /// # Errors
353    ///
354    /// Returns an error when `owner` differs from the component owner used to
355    /// spawn the operation.
356    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    /// Waits for this operation's tasks with no allowance beyond `deadline`.
362    ///
363    /// Tasks still running at the deadline are asked to abort and the call
364    /// returns `false` at once, without confirming that their futures were
365    /// dropped. They stay registered with `owner` until they are, so the
366    /// owner's shutdown report accounts for any that remain. This matches
367    /// [`TaskGroup::shutdown_until`], which also does not extend its deadline.
368    /// Runtime starvation or synchronous destructors can delay timer polling.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error when `owner` differs from the component owner used to
373    /// spawn the operation.
374    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    /// Waits until `deadline`, then aborts what remains and waits up to
379    /// `abort_confirmation` for those futures to be dropped.
380    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        // A zero confirmation window only requests the aborts. Legacy callers
390        // retain their boolean outcome and relative confirmation allowance.
391        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            // Registered before the check, so a wakeup in between is not lost.
405            let idle = self.inner.idle.notified();
406            if self.inner.active.load(Ordering::Acquire) == 0 {
407                // A task stops counting as active when its future is
408                // destroyed; its owner settles it just afterwards. Wait for
409                // that too, so a caller never finds a finished task still
410                // registered with the owner.
411                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    /// Admits one task: binds the owner and counts the task as active.
436    ///
437    /// The caller holds [`Self::spawn_guard`], so admission cannot interleave
438    /// with `close_admission` or `cancel`.
439    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
503/// Set once the owner accepted the task.
504const TASK_REGISTERED: u8 = 1;
505/// Set once the task's future was destroyed.
506const TASK_COMPLETED: u8 = 2;
507
508struct OperationTaskRegistrationState {
509    /// `TASK_REGISTERED` and `TASK_COMPLETED` bits. Both sides set their bit
510    /// with one read-modify-write, so exactly one sees the other bit and
511    /// records the outcome.
512    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    /// Marks the task as accepted by its owner. A task the owner rejected
538    /// never records an outcome.
539    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        // Counters are published before a waiter can observe the operation idle.
565        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
587// Also destroys the user future first when an accepted task is never polled.
588pub(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        // Hold the owner's settlement gate on another thread, so the task's
642        // future can be destroyed while the owner cannot yet settle it.
643        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        // Dropping the aborted future blocks until released, so a wait that
925        // returns before the release cannot have confirmed the abort.
926        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}