temporalio_workflow/
cancellation.rs1use 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#[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 pub fn new() -> Self {
60 Self {
61 inner: Default::default(),
62 }
63 }
64
65 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 pub fn cancel(&self) {
79 self.inner.cancel(None);
80 }
81
82 pub fn cancel_with_reason(&self, reason: impl Into<String>) {
84 self.inner.cancel(Some(reason.into()));
85 }
86
87 pub fn is_cancelled(&self) -> bool {
89 self.inner.cancelled.get()
90 }
91
92 pub fn reason(&self) -> Option<String> {
94 self.inner.reason.borrow().clone()
95 }
96
97 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#[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 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}