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}