Skip to main content

apalis_core/task/
context.rs

1//! Task execution context and lifecycle tracking.
2//!
3//! This module provides [`TaskContext`], a lightweight handle for interacting
4//! with a task while it is being executed. A task context is associated with a
5//! single task and can be cloned and shared with sub-tasks.
6//!
7//! Unlike [`WorkerContext`], which represents state shared by all tasks running
8//! on a worker, [`TaskContext`] represents the lifecycle of one task. It can be
9//! used to request cancellation, observe completion, wait for execution to
10//! finish, access the task's [`ExecutionContext`], and track sub-tasks.
11//!
12//! A task context is considered executed once the task has either completed or
13//! been cancelled. Sub-tasks spawned through [`TaskContext::run_until_executed`]
14//! are tied to their parent context and are tracked until they complete.
15//!
16//! # Lifecycle
17//!
18//! A task context progresses through the following states:
19//!
20//! ```text
21//! Running
22//!    ├── cancel() ──> Cancelled
23//!    └── complete() ─> Completed
24//! ```
25//!
26//! Once a task reaches a terminal state, subsequent attempts to cancel or
27//! complete it return a [`TaskStateError`].
28//!
29//! # Waiting for execution
30//!
31//! [`TaskContext::executed`] returns a future that resolves when the task is
32//! either completed or cancelled. The future registers a waker and therefore
33//! does not require polling in a loop.
34//!
35//! # Sub-tasks
36//!
37//! [`TaskContext::run_until_executed`] can be used to associate additional
38//! asynchronous work with a task. A sub-task runs until it completes or its
39//! parent context is executed. Sub-tasks are counted by their parent context,
40//! allowing task execution to account for work spawned from the task.
41//!
42//! # Task state errors
43//!
44//! [`TaskStateError`] describes failures when transitioning a task between
45//! lifecycle states. [`TerminalState`] identifies the terminal state that
46//! prevented the requested transition.
47use futures_util::task::AtomicWaker;
48use std::{
49    borrow::Borrow,
50    fmt,
51    hash::{Hash, Hasher},
52    pin::Pin,
53    sync::{
54        Arc, Weak,
55        atomic::{AtomicUsize, Ordering},
56    },
57    task::{Context, Poll},
58    time::{Duration, Instant},
59};
60
61use crate::{
62    task::from_request::FromRequest,
63    task::{ExecutionContext, Task, data::MissingDataError},
64    worker::context::WorkerContext,
65};
66
67const RUNNING: usize = 0;
68const CANCELLED: usize = 1; // Task was cancelled during polling
69const COMPLETED: usize = 2; // Task was completed via polling
70
71/// The context for a task, which can be used to cancel the task or spawn sub-tasks.
72///
73/// Unlike [`WorkerContext`], which is shared across all tasks,
74/// a [`TaskContext`] is unique to a single task and can be cloned to share with sub-tasks.
75/// A task context is considered "executed" when the task has successfully completed or has been cancelled, and all sub-tasks have also completed.
76#[derive(Clone, Debug)]
77pub struct TaskContext {
78    task_id: Arc<str>,
79    state: Arc<AtomicUsize>,
80    /// Wakes any pending [`WaitForExecutionFuture`] future when `complete() or cancel()` is called.
81    waker: Arc<AtomicWaker>,
82    instant: Instant,
83    inner: Weak<ExecutionContext>,
84    /// The number of sub-tasks currently spawned by this task context.
85    sub_tasks_count: Arc<AtomicUsize>,
86}
87
88impl TaskContext {
89    /// Builds a new task context from the given [`ExecutionContext`].
90    ///
91    /// # Panics
92    ///
93    /// Panics if `ctx.task_id` is `None`; every execution context driving
94    /// a task is expected to carry one.
95    #[must_use]
96    pub(crate) fn new(ctx: &Arc<ExecutionContext>) -> Self {
97        Self {
98            task_id: ctx
99                .task_id
100                .as_ref()
101                .expect("A task id must be included")
102                .to_string()
103                .into(),
104            state: Arc::new(AtomicUsize::new(RUNNING)),
105            waker: Arc::new(AtomicWaker::new()),
106            instant: Instant::now(),
107            inner: Arc::downgrade(ctx),
108            sub_tasks_count: Arc::new(AtomicUsize::new(0)),
109        }
110    }
111
112    /// Requests cancellation of the task.
113    ///
114    /// Wakes any pending [WaitForExecutionFuture] futures waiting for execution to complete
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the task had already reached a terminal state
119    /// (already cancelled or already completed).
120    pub fn cancel(&self) -> Result<(), TaskStateError> {
121        self.state
122            .compare_exchange(RUNNING, CANCELLED, Ordering::AcqRel, Ordering::Acquire)
123            .map_err(|prev| {
124                TaskStateError::AlreadyExecuted(match prev {
125                    CANCELLED => TerminalState::Cancelled,
126                    COMPLETED => TerminalState::Completed,
127                    other => unreachable!("unexpected task state: {other}"),
128                })
129            })?;
130        self.waker.wake();
131        Ok(())
132    }
133
134    /// Marks the task as completed.
135    ///
136    /// Wakes any pending [WaitForExecutionFuture] futures.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the task had already reached a terminal state
141    /// (already cancelled or already completed).
142    pub(crate) fn complete(&self) -> Result<(), TaskStateError> {
143        self.state
144            .compare_exchange(RUNNING, COMPLETED, Ordering::AcqRel, Ordering::Acquire)
145            .map_err(|prev| {
146                TaskStateError::AlreadyTerminated(match prev {
147                    CANCELLED => TerminalState::Cancelled,
148                    COMPLETED => TerminalState::Completed,
149                    other => unreachable!("unexpected task state: {other}"),
150                })
151            })?;
152        self.waker.wake();
153        Ok(())
154    }
155
156    /// Returns whether the task has been completed.
157    #[must_use]
158    pub fn is_completed(&self) -> bool {
159        self.state.load(Ordering::Acquire) == COMPLETED
160    }
161
162    /// Returns whether cancellation has been requested.
163    #[must_use]
164    pub fn is_cancelled(&self) -> bool {
165        self.state.load(Ordering::Acquire) == CANCELLED
166    }
167
168    /// Returns whether there is nothing left to wait on:
169    ///
170    /// If true the task has either completed or been cancelled.
171    #[must_use]
172    pub fn is_executed(&self) -> bool {
173        self.state.load(Ordering::Acquire) != RUNNING
174    }
175
176    /// Returns a future that resolves once the task is executed — either
177    /// completed or cancelled.
178    ///
179    /// Can be awaited standalone or raced against other futures, e.g.:
180    ///
181    /// ```ignore
182    /// futures_util::select! {
183    ///     _ = ctx.executed().fuse() => { /* task done */ }
184    ///     res = some_work.fuse() => { /* completed with `res` */ }
185    /// }
186    /// ```
187    pub fn executed(&self) -> WaitForExecutionFuture {
188        WaitForExecutionFuture {
189            inner: self.clone(),
190        }
191    }
192
193    /// Returns how long the task has been running.
194    #[must_use]
195    pub fn elapsed(&self) -> Duration {
196        self.instant.elapsed()
197    }
198
199    /// Returns the task id for this token.
200    #[must_use]
201    pub fn task_id(&self) -> &str {
202        &self.task_id
203    }
204
205    /// Recovers the execution context for the task, if it's still available.
206    ///
207    /// Returns `None` once the owning [`ExecutionContext`] has been dropped.
208    #[must_use]
209    pub fn execution_context(&self) -> Option<Arc<ExecutionContext>> {
210        self.inner.upgrade()
211    }
212
213    fn start_task(&self) {
214        self.sub_tasks_count.fetch_add(1, Ordering::Relaxed);
215    }
216
217    fn end_task(&self) {
218        self.sub_tasks_count.fetch_sub(1, Ordering::Relaxed);
219    }
220
221    /// Returns the number of sub-tasks currently spawned by this task context.
222    #[must_use]
223    pub fn len(&self) -> usize {
224        self.sub_tasks_count.load(Ordering::Relaxed)
225    }
226
227    /// Returns whether there are any subtasks running.
228    #[must_use]
229    pub fn is_empty(&self) -> bool {
230        self.len() == 0
231    }
232
233    /// Spawns a (sub-task) future that is tied to this task context.
234    ///
235    /// Runs a future to completion, returning its result unless this
236    /// [`TaskContext`] is completed first.
237    ///
238    /// Biased towards completion: if the future resolves and the task
239    /// is completed in the same poll, the future's result wins.
240    pub fn run_until_executed<F>(&self, fut: F) -> SubTaskFuture<F>
241    where
242        F: Future,
243    {
244        self.start_task();
245        SubTaskFuture {
246            parent: self.clone(),
247            future: fut,
248        }
249    }
250}
251
252/// Errors that can occur when transitioning a [`TaskContext`]'s lifecycle state.
253#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
254#[non_exhaustive]
255pub enum TaskStateError {
256    /// The task could not be cancelled because it had already reached a
257    /// terminal state.
258    #[error("task cannot be cancelled: already {0}")]
259    AlreadyExecuted(TerminalState),
260
261    /// The task could not be marked completed because it had already
262    /// reached a terminal state.
263    #[error("task cannot be completed: already {0}")]
264    AlreadyTerminated(TerminalState),
265
266    /// Task could not be found
267    #[error("task cannot be found")]
268    TaskNotFound,
269}
270
271/// The terminal state a task had already reached, when a state transition fails.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum TerminalState {
275    /// Task already canceled
276    Cancelled,
277    /// Task already completed
278    Completed,
279}
280
281impl fmt::Display for TerminalState {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self {
284            Self::Cancelled => write!(f, "cancelled"),
285            Self::Completed => write!(f, "completed"),
286        }
287    }
288}
289
290/// A future that resolves once its parent [`TaskContext`] is executed —
291/// either completed or cancelled.
292///
293/// Unlike polling [`TaskContext::is_executed`] in a loop, this future
294/// registers a waker so it can be awaited on its own — e.g. raced against
295/// other work with `select!` — without needing another future to drive it.
296#[must_use = "futures do nothing unless you `.await` or poll them"]
297#[derive(Debug)]
298pub struct WaitForExecutionFuture {
299    inner: TaskContext,
300}
301
302impl Future for WaitForExecutionFuture {
303    type Output = ();
304
305    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
306        if self.inner.is_executed() {
307            Poll::Ready(())
308        } else {
309            self.inner.waker.register(cx.waker());
310            Poll::Pending
311        }
312    }
313}
314
315/// A future that runs a sub-task tied to a [`TaskContext`].
316///
317/// The sub-task is cancelled if the parent task context is completed before
318/// the future resolves. The sub-task is also tracked by the parent context,
319/// and the parent context will not be considered complete until all sub-tasks
320/// have completed.
321#[must_use = "futures do nothing unless polled"]
322#[pin_project::pin_project(PinnedDrop)]
323#[derive(Debug)]
324pub struct SubTaskFuture<F: Future> {
325    parent: TaskContext,
326    #[pin]
327    future: F,
328}
329
330#[pin_project::pinned_drop]
331impl<F: Future> PinnedDrop for SubTaskFuture<F> {
332    fn drop(self: Pin<&mut Self>) {
333        self.parent.end_task();
334    }
335}
336
337impl<F: Future> Future for SubTaskFuture<F> {
338    type Output = Option<F::Output>;
339
340    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
341        let this = self.project();
342        if let Poll::Ready(res) = this.future.poll(cx) {
343            Poll::Ready(Some(res))
344        } else if this.parent.is_executed() {
345            Poll::Ready(None)
346        } else {
347            Poll::Pending
348        }
349    }
350}
351
352impl PartialEq for TaskContext {
353    fn eq(&self, other: &Self) -> bool {
354        self.task_id() == other.task_id()
355    }
356}
357impl Eq for TaskContext {}
358
359impl Hash for TaskContext {
360    fn hash<H: Hasher>(&self, state: &mut H) {
361        self.task_id().hash(state);
362    }
363}
364
365impl Borrow<str> for TaskContext {
366    fn borrow(&self) -> &str {
367        &self.task_id
368    }
369}
370
371impl<Args: Sync> FromRequest<Task<Args>> for TaskContext {
372    type Error = MissingDataError;
373    async fn from_request(task: &Task<Args>) -> Result<Self, Self::Error> {
374        let worker: &WorkerContext = task.data().get_checked()?;
375        let token = worker.get_task_context(&task.ctx)?;
376        Ok(token)
377    }
378}