Skip to main content

temporalio_workflow/
cancellation.rs

1use crate::runtime::SdkWakeGuard;
2use futures_util::{FutureExt, future::FusedFuture};
3use std::{
4    cell::{Cell, RefCell},
5    collections::BTreeMap,
6    future,
7    rc::{Rc, Weak},
8    task::{Poll, Waker},
9};
10
11type CancellationCallback = Rc<dyn Fn(Option<String>)>;
12
13#[derive(derive_more::Debug, Default)]
14struct WorkflowCancellationState {
15    cancelled: Cell<bool>,
16    reason: RefCell<Option<String>>,
17    wakers: RefCell<Vec<Waker>>,
18    next_callback_id: Cell<u64>,
19    #[debug(skip)]
20    callbacks: RefCell<BTreeMap<u64, CancellationCallback>>,
21}
22
23impl WorkflowCancellationState {
24    fn cancel(&self, reason: Option<String>) {
25        if self.cancelled.replace(true) {
26            return;
27        }
28        *self.reason.borrow_mut() = reason.clone();
29
30        let _guard = SdkWakeGuard::new();
31        for waker in self.wakers.borrow_mut().drain(..) {
32            waker.wake();
33        }
34        let callbacks = std::mem::take(&mut *self.callbacks.borrow_mut());
35        for callback in callbacks.into_values() {
36            callback(reason.clone());
37        }
38    }
39}
40
41/// A deterministic cancellation token for workflow operations.
42///
43/// Tokens created with [`WorkflowCancellationToken::new`] are detached from workflow
44/// cancellation. Use [`WorkflowCancellationToken::child_token`] to create a token that is
45/// cancelled when its parent is cancelled.
46#[derive(Clone, Debug)]
47pub struct WorkflowCancellationToken {
48    inner: Rc<WorkflowCancellationState>,
49}
50
51impl Default for WorkflowCancellationToken {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl WorkflowCancellationToken {
58    /// Create a detached cancellation token.
59    pub fn new() -> Self {
60        Self {
61            inner: Default::default(),
62        }
63    }
64
65    /// Create a token that is cancelled when this token is cancelled.
66    pub fn child_token(&self) -> Self {
67        let child = Self::new();
68        let weak_child = Rc::downgrade(&child.inner);
69        self.register(move |reason| {
70            if let Some(child) = weak_child.upgrade() {
71                child.cancel(reason);
72            }
73        });
74        child
75    }
76
77    /// Cancel this token without a reason.
78    pub fn cancel(&self) {
79        self.inner.cancel(None);
80    }
81
82    /// Cancel this token with a reason.
83    pub fn cancel_with_reason(&self, reason: impl Into<String>) {
84        self.inner.cancel(Some(reason.into()));
85    }
86
87    /// Return whether this token has been cancelled.
88    pub fn is_cancelled(&self) -> bool {
89        self.inner.cancelled.get()
90    }
91
92    /// Return the first cancellation reason, if one was provided.
93    pub fn reason(&self) -> Option<String> {
94        self.inner.reason.borrow().clone()
95    }
96
97    /// Return a future that resolves when this token is cancelled.
98    pub fn cancelled(&self) -> impl FusedFuture<Output = ()> + '_ {
99        future::poll_fn(move |cx| {
100            if self.is_cancelled() {
101                Poll::Ready(())
102            } else {
103                self.inner.wakers.borrow_mut().push(cx.waker().clone());
104                Poll::Pending
105            }
106        })
107        .fuse()
108    }
109
110    pub(crate) fn register(
111        &self,
112        callback: impl Fn(Option<String>) + 'static,
113    ) -> WorkflowCancellationRegistration {
114        if self.is_cancelled() {
115            callback(self.reason());
116            return WorkflowCancellationRegistration::default();
117        }
118
119        let id = self.inner.next_callback_id.get();
120        self.inner.next_callback_id.set(id + 1);
121        self.inner
122            .callbacks
123            .borrow_mut()
124            .insert(id, Rc::new(callback));
125
126        WorkflowCancellationRegistration {
127            token: Rc::downgrade(&self.inner),
128            callback_id: Some(id),
129        }
130    }
131}
132
133#[derive(Debug, Default)]
134pub(crate) struct WorkflowCancellationRegistration {
135    token: Weak<WorkflowCancellationState>,
136    callback_id: Option<u64>,
137}
138
139impl WorkflowCancellationRegistration {
140    pub(crate) fn unregister(&mut self) {
141        let Some(callback_id) = self.callback_id.take() else {
142            return;
143        };
144        if let Some(token) = self.token.upgrade() {
145            token.callbacks.borrow_mut().remove(&callback_id);
146        }
147    }
148}
149
150/// Returned when a cancellable workflow wait is cancelled.
151#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
152#[error("Workflow wait cancelled")]
153pub struct WorkflowCancellationError {
154    reason: Option<String>,
155}
156
157impl WorkflowCancellationError {
158    pub(crate) fn new(reason: Option<String>) -> Self {
159        Self { reason }
160    }
161
162    /// Return the cancellation reason, if one was provided.
163    pub fn reason(&self) -> Option<&str> {
164        self.reason.as_deref()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn child_cancellation_is_downward_and_first_reason_wins() {
174        let parent = WorkflowCancellationToken::new();
175        let child = parent.child_token();
176
177        child.cancel_with_reason("child");
178        parent.cancel_with_reason("parent");
179
180        assert_eq!(child.reason().as_deref(), Some("child"));
181        assert_eq!(parent.reason().as_deref(), Some("parent"));
182    }
183
184    #[test]
185    fn child_inherits_reason() {
186        let parent = WorkflowCancellationToken::new();
187        let child = parent.child_token();
188
189        parent.cancel_with_reason("parent");
190
191        assert_eq!(child.reason().as_deref(), Some("parent"));
192        assert_eq!(parent.reason().as_deref(), Some("parent"));
193    }
194    #[test]
195    fn parent_cancellation_ignores_dropped_child_with_callback() {
196        let parent = WorkflowCancellationToken::new();
197        let callback_called = Rc::new(Cell::new(false));
198        let child = parent.child_token();
199        let callback_called_ref = callback_called.clone();
200        child.register(move |_| callback_called_ref.set(true));
201        drop(child);
202
203        parent.cancel_with_reason("parent");
204
205        assert!(parent.is_cancelled());
206        assert_eq!(parent.reason().as_deref(), Some("parent"));
207        assert!(!callback_called.get());
208    }
209
210    #[test]
211    fn detached_token_does_not_follow_an_unrelated_token() {
212        let root = WorkflowCancellationToken::new();
213        let detached = WorkflowCancellationToken::new();
214
215        root.cancel();
216
217        assert!(root.is_cancelled());
218        assert!(!detached.is_cancelled());
219    }
220}