Skip to main content

apalis_core/task/
mod.rs

1//! Utilities for creating and managing tasks.
2//!
3//! The [`Task`] component encapsulates a unit of work to be executed,
4//! along with its associated context, metadata, and execution status. The [`ExecutionContext`]
5//! struct contains metadata, attempt tracking, extensions, and scheduling information for each task.
6//!
7//! # Overview
8//!
9//! In `apalis`, tasks are designed to represent discrete units of work that can be scheduled, retried, and tracked
10//! throughout their lifecycle. Each task consists of arguments (`args`) describing the work to be performed,
11//! and an [`ExecutionContext`] containing metadata and control information.
12//!
13//! ## [`Task`]
14//!
15//! The [`Task`] struct is generic over:
16//! - `Args`: The type of arguments or payload for the task.
17//!
18//! ## [`ExecutionContext`]
19//!
20//! The [`ExecutionContext`] struct provides the following:
21//! - `task_id`: Optionally stores a unique identifier for the task.
22//! - `data`: An [`Extensions`] container for storing arbitrary per-task data (e.g., middleware extensions).
23//! - `attempt`: Tracks how many times the task has been attempted.
24//! - `metadata`: Custom metadata for the task, provided by the backend or user.
25//! - `status`: The current [`Status`] of the task (e.g., Pending, Running, Completed, Failed).
26//! - `run_at`: The UNIX timestamp (in seconds) when the task should be run.
27//! - `done_at`: The UNIX timestamp (in seconds) when the task completed, if it has.
28//! - `lock_at`: The UNIX timestamp (in seconds) when the task was locked for processing, if applicable.
29//! - `lock_by`: An identifier for the worker or process currently holding the lock on the task.
30//! - `idempotency_key`: An optional key used to enforce job uniqueness.
31//! - `max_attempts`: The maximum number of attempts allowed before the task is considered failed.
32//! - `priority`: An optional priority value used to influence scheduling order.
33//! - `queue`: The queue the task belongs to, if applicable.
34//! - `runs`: A history of all runs recorded for the task. (*experimental*)
35//!
36//! The execution context is essential for tracking the state and metadata of a task as it moves through
37//! the system. It enables features such as retries, scheduling, locking, prioritization, and extensibility
38//! via the `Extensions` type.
39//!
40//! # Modules
41//!
42//! - [`attempt`]: Tracks the number of attempts a task has been executed.
43//! - [`builder`]: Utilities for constructing tasks.
44//! - [`data`]: Data types for task payloads.
45//! - [`extensions`]: Extension storage for tasks.
46//! - [`metadata`]: Ctxdata types for tasks.
47//! - [`status`]: Status tracking for tasks.
48//! - [`task_id`]: Types for uniquely identifying tasks.
49//!
50//! # Examples
51//!
52//! ## Creating a new task with default metadata
53//!
54//! ```rust
55//! # use apalis_core::task::{Task, ExecutionContext};
56//! # use apalis_core::task::builder::TaskBuilder;
57//! let task: Task<String> = TaskBuilder::new("my work".to_string()).build();
58//! ```
59//!
60//! ## Creating a task with custom metadata
61//!
62//! ```rust
63//! # use apalis_core::task::{Task, ExecutionContext};
64//! # use apalis_core::task::builder::TaskBuilder;
65//! # use apalis_core::task::metadata::Metadata;
66//! # use apalis_core::task::metadata::MetadataStore;
67//! #
68//! #[derive(Debug, PartialEq)]
69//! struct RequestId(String);
70//!
71//! impl Metadata for RequestId {
72//!     type Error = std::convert::Infallible;
73//!
74//!     fn inject(&self, metadata: &mut MetadataStore) -> Result<(), Self::Error> {
75//!         let _ = metadata.insert("request_id", self.0.clone());
76//!         Ok(())
77//!     }
78//!
79//!     fn extract(metadata: &MetadataStore) -> Result<Self, Self::Error> {
80//!         Ok(Self(
81//!             metadata
82//!                 .get("request_id")
83//!                 .unwrap_or_default()
84//!                 .to_owned(),
85//!         ))
86//!     }
87//! }
88//!
89//! let task: Task<String> = TaskBuilder::new("important work".to_string())
90//!     .metadata(&RequestId("user_id".to_string()))
91//!     .build();
92//! ```
93//!
94//! ## Accessing and modifying the execution context
95//!
96//! ```rust
97//! # use apalis_core::task::builder::TaskBuilder;
98//! use apalis_core::task::{Task, ExecutionContext, status::Status};
99//! let mut task: TaskBuilder<_> = TaskBuilder::new("work".to_string());
100//! task = task.status(Status::Running);
101//! ```
102//!
103//! ## Using Extensions for per-task data
104//!
105//! ```rust
106//! # use apalis_core::task::builder::TaskBuilder;
107//! use apalis_core::task::{Task, extensions::Extensions};
108//! #[derive(Debug, Clone, PartialEq)]
109//! pub struct TracingId(String);
110//! let mut extensions = Extensions::default();
111//! extensions.insert(TracingId("abc123".to_owned()));
112//! let task: Task<String> = TaskBuilder::new("work".to_string()).with_data(extensions).build();
113//! assert_eq!(task.data().get::<TracingId>(), Some(&TracingId("abc123".to_owned())));
114//! ```
115//!
116//! # See Also
117//!
118//! - [`Task`]: Represents a unit of work to be executed.
119//! - [`ExecutionContext`]: Holds metadata, status, and control information for a task.
120//! - [`Extensions`]: Type-safe storage for per-task data.
121//! - [`Status`]: Enum representing the lifecycle state of a task.
122//! - [`Attempt`]: Tracks the number of execution attempts for a task.
123//! - [`TaskId`]: Unique identifier type for tasks.
124//! - [`FromRequest`]: Trait for extracting data from task contexts.
125//! - [`IntoResponse`]: Trait for converting tasks into response types.
126//! - [`TaskBuilder`]: Fluent builder for constructing tasks with optional configuration.
127//!
128//! [`TaskBuilder`]: crate::task::builder::TaskBuilder
129//! [`IntoResponse`]: crate::task::into_response::IntoResponse
130//! [`FromRequest`]: crate::task::from_request::FromRequest
131
132use std::{fmt::Debug, ops::Deref, sync::Arc};
133
134use crate::{
135    backend::queue::Queue,
136    task::{
137        attempt::Attempt,
138        builder::TaskBuilder,
139        extensions::Extensions,
140        from_request::FromRequest,
141        metadata::{Metadata, MetadataStore},
142        runs::Run,
143        status::{AtomicStatus, Status},
144        task_id::TaskId,
145    },
146};
147
148pub mod attempt;
149pub mod builder;
150#[cfg(feature = "task-context")]
151pub mod context;
152pub mod data;
153pub mod extensions;
154pub mod from_request;
155pub mod into_response;
156pub mod metadata;
157pub mod runs;
158pub mod status;
159pub mod task_fn;
160pub mod task_id;
161
162/// Represents a task which will be executed
163/// Should be considered a single unit of work
164#[derive(Debug, Clone, Default)]
165pub struct Task<Args> {
166    /// The argument task part
167    pub args: Args,
168    /// ExecutionContext of the task eg id, attempts and context
169    ctx: Arc<ExecutionContext>,
170}
171
172/// Execution context of a `Task`
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct ExecutionContext {
175    /// The task's id if allocated
176    task_id: Option<TaskId>,
177
178    /// The tasks's extensions
179    #[cfg_attr(feature = "serde", serde(skip))]
180    data: Extensions,
181
182    /// The tasks's attempts
183    /// Keeps track of the number of attempts a task has been worked on
184    attempt: Attempt,
185
186    /// The task status that is wrapped in an atomic status
187    status: AtomicStatus,
188
189    /// The time a task should be run
190    run_at: Option<u64>,
191
192    /// The time the task was completed, if applicable
193    done_at: Option<u64>,
194
195    /// The time the task was locked, if applicable
196    lock_at: Option<u64>,
197
198    /// Identifier of the worker/process that currently holds the lock on this task
199    lock_by: Option<String>,
200
201    /// Adds a unique key to enforce job uniqueness when used
202    idempotency_key: Option<String>,
203
204    /// Metadata associated with the task
205    metadata: MetadataStore,
206
207    /// The maximum number of attempts allowed for the task
208    max_attempts: Option<usize>,
209
210    /// The priority of the task, which can be used for scheduling
211    priority: Option<usize>,
212
213    /// The queue to which the task belongs, if applicable
214    queue: Option<Queue>,
215
216    /// A list of all runs for this task
217    runs: Vec<Run>,
218}
219
220impl ExecutionContext {
221    /// Returns the task's ID, if one has been allocated.
222    #[must_use]
223    pub fn task_id(&self) -> Option<&TaskId> {
224        self.task_id.as_ref()
225    }
226
227    /// Returns the extensions associated with the task.
228    #[must_use]
229    pub fn data(&self) -> &Extensions {
230        &self.data
231    }
232
233    /// Returns the number of attempts made to execute the task.
234    #[must_use]
235    pub fn attempt(&self) -> usize {
236        self.attempt.current()
237    }
238
239    /// Get the atomic attempt
240    #[must_use]
241    pub fn raw_attempt(&self) -> &Attempt {
242        &self.attempt
243    }
244
245    /// Returns the current status of the task.
246    #[must_use]
247    pub fn status(&self) -> Status {
248        self.status.load()
249    }
250
251    /// Get the atomic status
252    #[must_use]
253    pub fn raw_status(&self) -> &AtomicStatus {
254        &self.status
255    }
256
257    /// Returns the time at which the task is scheduled to run.
258    ///
259    /// Returns `None` if no run time has been specified.
260    #[must_use]
261    pub fn run_at(&self) -> Option<u64> {
262        self.run_at
263    }
264
265    /// Returns the time at which the task was completed.
266    ///
267    /// Returns `None` if the task has not been completed.
268    #[must_use]
269    pub fn done_at(&self) -> Option<u64> {
270        self.done_at
271    }
272
273    /// Returns the time at which the task was locked.
274    ///
275    /// Returns `None` if the task is not locked.
276    #[must_use]
277    pub fn lock_at(&self) -> Option<u64> {
278        self.lock_at
279    }
280
281    /// Returns the identifier of the worker or process currently holding
282    /// the lock on this task.
283    ///
284    /// Returns `None` if the task is not currently locked.
285    #[must_use]
286    pub fn lock_by(&self) -> Option<&str> {
287        self.lock_by.as_deref()
288    }
289
290    /// Returns the idempotency key associated with the task.
291    ///
292    /// Returns `None` if no idempotency key has been specified.
293    #[must_use]
294    pub fn idempotency_key(&self) -> Option<&str> {
295        self.idempotency_key.as_deref()
296    }
297
298    /// Returns the metadata associated with the task.
299    #[must_use]
300    pub fn metadata(&self) -> &MetadataStore {
301        &self.metadata
302    }
303
304    /// Returns the maximum number of attempts allowed for the task.
305    ///
306    /// Returns `None` if no maximum has been specified.
307    #[must_use]
308    pub fn max_attempts(&self) -> Option<usize> {
309        self.max_attempts
310    }
311
312    /// Returns the priority of the task.
313    ///
314    /// Returns `None` if no priority has been specified.
315    #[must_use]
316    pub fn priority(&self) -> Option<usize> {
317        self.priority
318    }
319
320    /// Returns the queue to which the task belongs.
321    ///
322    /// Returns `None` if the task is not associated with a queue.
323    #[must_use]
324    pub fn queue(&self) -> Option<&Queue> {
325        self.queue.as_ref()
326    }
327
328    /// Returns the runs recorded for this task.
329    #[must_use]
330    pub fn runs(&self) -> &[Run] {
331        &self.runs
332    }
333}
334
335impl<Args> Task<Args> {
336    /// Creates a new task given some args
337    ///
338    /// Used to easily create a ready task.
339    /// Please prefer to use [`TaskBuilder`] if you need to modify [`ExecutionContext`]
340    pub fn new(args: Args) -> Self {
341        Self {
342            args,
343            ctx: Default::default(),
344        }
345    }
346    /// Returns the execution context associated with the task.
347    #[must_use]
348    pub fn ctx(&self) -> &Arc<ExecutionContext> {
349        &self.ctx
350    }
351}
352
353impl Default for ExecutionContext {
354    fn default() -> Self {
355        Self {
356            task_id: None,
357            data: Extensions::default(),
358            attempt: Attempt::default(),
359            status: AtomicStatus::new(Status::Pending),
360            run_at: None,
361            done_at: None,
362            lock_at: None,
363            lock_by: None,
364            idempotency_key: None,
365            metadata: MetadataStore::default(),
366            max_attempts: None,
367            priority: None,
368            queue: None,
369            runs: Vec::new(),
370        }
371    }
372}
373
374impl Debug for ExecutionContext {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("ExecutionContext")
377            .field("task_id", &self.task_id)
378            .field("data", &"<Extensions>")
379            .field("attempt", &self.attempt)
380            .field("status", &self.status.load())
381            .field("run_at", &self.run_at)
382            .field("done_at", &self.done_at)
383            .field("lock_at", &self.lock_at)
384            .field("lock_by", &self.lock_by)
385            .field("idempotency_key", &self.idempotency_key)
386            .field("metadata", &self.metadata)
387            .field("max_attempts", &self.max_attempts)
388            .field("runs", &self.runs)
389            .field("priority", &self.priority)
390            .field("queue", &self.queue)
391            .finish()
392    }
393}
394
395impl Clone for ExecutionContext {
396    fn clone(&self) -> Self {
397        Self {
398            task_id: self.task_id.clone(),
399            data: self.data.clone(),
400            attempt: self.attempt.clone(),
401            status: self.status.clone(),
402            run_at: self.run_at,
403            done_at: self.done_at,
404            lock_at: self.lock_at,
405            lock_by: self.lock_by.clone(),
406            idempotency_key: self.idempotency_key.clone(),
407            metadata: self.metadata.clone(),
408            runs: self.runs.clone(),
409            max_attempts: self.max_attempts,
410            priority: self.priority,
411            queue: self.queue.clone(),
412        }
413    }
414}
415
416impl<Args> Task<Args> {
417    /// Take the task into its parts
418    #[must_use]
419    pub fn take(self) -> (Args, Arc<ExecutionContext>) {
420        (self.args, self.ctx)
421    }
422
423    /// Extract a value of type `T` from the task's context
424    ///
425    /// Uses [FromRequest] trait to extract the value.
426    #[must_use = "An extracted value should be used or handled to avoid unused value warnings."]
427    pub async fn extract<T: FromRequest<Self>>(&self) -> Result<T, T::Error> {
428        T::from_request(self).await
429    }
430
431    /// Maps the `args` field using the provided function, consuming the task.
432    pub fn map_args<F, NewArgs>(self, f: F) -> Task<NewArgs>
433    where
434        F: FnOnce(Args) -> NewArgs,
435    {
436        Task {
437            args: f(self.args),
438            ctx: self.ctx,
439        }
440    }
441
442    /// Maps the `args` field using the provided function, consuming the task.
443    #[must_use = "A mapped task should be used or handled to avoid unused value warnings."]
444    pub fn try_map_args<F, NewArgs, Err>(self, f: F) -> Result<Task<NewArgs>, Err>
445    where
446        F: FnOnce(Args) -> Result<NewArgs, Err>,
447    {
448        Ok(Task {
449            args: f(self.args)?,
450            ctx: self.ctx,
451        })
452    }
453
454    /// Maps the `execution_context` using the provided function, consuming the task
455    #[must_use]
456    pub fn map_context<F>(self, f: F) -> Self
457    where
458        F: FnOnce(Arc<ExecutionContext>) -> Arc<ExecutionContext>,
459    {
460        Self {
461            args: self.args,
462            ctx: f(self.ctx),
463        }
464    }
465
466    /// Modifies the id type of the task, consuming the task
467    ///
468    /// See [`crate::backend::ext::pipe`]
469    #[must_use]
470    #[doc(hidden)]
471    pub fn map_id_type(self) -> Self {
472        let mut ctx = Arc::unwrap_or_clone(self.ctx);
473        let _ = ctx.metadata.insert(
474            "apalis_core.transform.old_id",
475            ctx.task_id.map(|id| id.to_string()).unwrap_or_default(),
476        );
477        Self {
478            args: self.args,
479            ctx: Arc::new(ExecutionContext {
480                task_id: None,
481                data: ctx.data,
482                attempt: ctx.attempt,
483                status: ctx.status,
484                run_at: ctx.run_at,
485                done_at: ctx.done_at,
486                lock_at: ctx.lock_at,
487                lock_by: ctx.lock_by,
488                idempotency_key: ctx.idempotency_key,
489                metadata: ctx.metadata,
490                max_attempts: ctx.max_attempts,
491                priority: ctx.priority,
492                queue: ctx.queue,
493                runs: ctx.runs,
494            }),
495        }
496    }
497
498    /// Converts the task into a [`TaskBuilder`]
499    #[must_use = "Converting a task into a builder allows for further modifications before rebuilding the task."]
500    pub fn into_builder(self) -> TaskBuilder<Args> {
501        TaskBuilder {
502            args: self.args,
503            ctx: Arc::unwrap_or_clone(self.ctx),
504        }
505    }
506
507    /// Inject data into the execution context via [`Arc::make_mut`]
508    ///
509    /// If an extension of this type already existed, it will
510    /// be returned.
511    pub fn inject_data<D>(&mut self, data: D) -> Option<D>
512    where
513        D: Send + Clone + Sync + 'static,
514    {
515        let ctx = Arc::make_mut(&mut self.ctx);
516        ctx.data.insert(data)
517    }
518
519    /// Inject metadata into the execution context via [`Arc::make_mut`]
520    pub fn inject_metadata<M>(&mut self, value: &M) -> Result<(), M::Error>
521    where
522        M: Metadata,
523    {
524        let ctx = Arc::make_mut(&mut self.ctx);
525        value.inject(&mut ctx.metadata)?;
526        Ok(())
527    }
528
529    /// Build a new task with ready parts
530    ///
531    /// Usually you wanna use [TaskBuilder] so this is considered an internal api
532    #[doc(hidden)]
533    pub fn new_with_ctx(args: Args, ctx: Arc<ExecutionContext>) -> Self {
534        Self { args, ctx }
535    }
536}
537
538impl<Args> Deref for Task<Args> {
539    type Target = ExecutionContext;
540
541    fn deref(&self) -> &Self::Target {
542        &self.ctx
543    }
544}
545
546impl<A: Send + 'static> From<A> for Task<A> {
547    fn from(value: A) -> Self {
548        Self {
549            args: value,
550            ctx: Default::default(),
551        }
552    }
553}