Skip to main content

apalis_core/task/
task_fn.rs

1//! Utilities for adapting async functions into a task handler.
2//!
3//! The [`task_fn`] helper and the [`TaskFn`] struct in this module allow you to wrap
4//! async functions or closures into a [`TaskFn`] implementation, which can then be
5//! used in service middleware pipelines or other components expecting a [`TaskFn`].
6//!
7//! This is particularly useful when building lightweight, composable services from plain
8//! functions, including those with extracted arguments via [`FromRequest`].
9//!
10//! ## Features
11//!
12//! - Supports functions with up to 16 additional arguments beyond the core request.
13//! - Automatically applies argument extraction using the [`FromRequest`] trait.
14//! - Converts output to responses using the [`IntoResponse`] trait.
15//! - Captures function argument types at compile time via generics for static dispatch.
16//!
17//!
18//! ## Introduction
19//!
20//! The first argument of any task function is the type `Args` which is tied to the backend's task type.
21//! Eg if you are writing a task for an email service, `Args` might be a struct `Email` that includes fields like `user_id`, `subject`, and `message`.
22//!
23//! A rule of thumb is to never store database models as task arguments.
24//!
25//! Instead of doing this:
26//! ```rust
27//! struct User {
28//!     id: String,
29//!     // other fields...
30//! }
31//! struct Email {
32//!     user: User,
33//!     subject: String,
34//!     message: String,
35//! }
36//! ```
37//! Do this:
38//! ```
39//! struct Email {
40//!     user_id: String,
41//!     subject: String,
42//!     message: String,
43//! }
44//! ```
45//!
46//! All the primitive types (e.g. `String`, `u32`) can be used directly as task arguments.
47//!
48//! **Note:**
49//!
50//! > *Some backends like `apalis-cron` offer a specific `Args` type (Tick) for cron jobs while most others like `postgres` use a more generic `Args` type.*
51//!
52//! A guide for extracting complex information from tasks using [`FromRequest`] is available in [step 3](#3-implementing-custom-argument-extraction-with-fromrequest).
53//!
54//! ## Getting started
55//!
56//! Task handlers are async functions that process a task. You can use the [`task_fn`] helper
57//! to wrap your handler into a service.
58//!
59//! ```rust
60//! # use apalis_core::task::data::Data;
61//! #[derive(Clone)]
62//! struct State;
63//!
64//! // A simple handler that takes an id and injected state
65//! async fn handler(id: u32, state: Data<State>) -> String {
66//!     format!("Got id {} with state", id)
67//! }
68//! ```
69//! You would need to inject the state in your worker builder:
70//!
71//! ```rs
72//! let worker = WorkerBuilder::new()
73//!     .backend(in_memory)
74//!     .data(State)
75//!     .build(handler);
76//! ```
77//!
78//! ## Dependency Injection
79//!
80//! `apalis-core` supports default injection for common types in your handler arguments, such as:
81//! - [`WorkerContext`]: Worker context
82//! - [`TaskContext`]: The tasks context including the execution context
83//! - [`Attempt`]: Information about the current attempt
84//! - [`Data<T>`]: Injected data/state
85//! - [`TaskId`]: The unique ID of the task
86//!
87//! Example:
88//! ```rust
89//! # use apalis_core::task::{attempt::Attempt, data::Data, task_id::TaskId, context::TaskContext};
90//! #[derive(Clone)]
91//! struct State;
92//!
93//! async fn process_task(_: u32, attempt: Attempt, ctx: TaskContext, id: TaskId) -> String {
94//!     format!("Attempt {} for task {} with elapsed: {:?}", attempt.current(), id, ctx.elapsed())
95//! }
96//! ```
97//!
98//!
99//! ## Custom argument extraction with [`FromRequest`]
100//!
101//! You can extract custom types from the request by implementing [`FromRequest`].
102//!
103//! Suppose you have a task to send emails, and you want to automatically extract a `User` from the task's `user_id`:
104//!
105//! ```rust
106//! struct Email {
107//!     user_id: String,
108//!     subject: String,
109//!     message: String,
110//! }
111//!
112//! // Implement FromRequest for User
113//! # use apalis_core::task::from_request::FromRequest;
114//! # use apalis_core::task::Task;
115//! # use apalis_core::error::BoxDynError;
116//! # struct User {
117//! #    id: String,
118//! #    // other fields...
119//! # }
120//!
121//! impl FromRequest<Task<Email>> for User {
122//!     type Error = BoxDynError;
123//!     async fn from_request(req: &Task<Email>) -> Result<Self, Self::Error> {
124//!         let user_id = req.args.user_id.clone();
125//!         // Simulate fetching user from DB
126//!         Ok(User { id: user_id })
127//!     }
128//! }
129//!
130//! // Now your handler can take User directly
131//! async fn send_email(email: Email, user: User) -> Result<(), BoxDynError> {
132//!     // Use email and user
133//!     Ok(())
134//! }
135//! ```
136//!
137//! ## How It Works
138//!
139//! - [`task_fn`] wraps your handler into a [`TaskFn`] service.
140//! - Arguments are extracted using [`FromRequest`].
141//! - DI types are injected automatically.
142//! - The handler's output is converted to a response using [`IntoResponse`].
143//!
144//! [`task_fn`]: crate::task::task_fn::task_fn
145//! [`TaskFn`]: crate::task::task_fn::TaskFn
146//! [`FromRequest`]: crate::task::from_request::FromRequest
147//! [`IntoResponse`]: crate::task::into_response::IntoResponse
148//! [`Attempt`]: crate::task::attempt::Attempt
149//! [`Data<T>`]: crate::task::data::Data
150//! [`WorkerContext`]: crate::worker::context::WorkerContext
151//! [`TaskContext`]: crate::task::context::TaskContext
152//! [`TaskId`]: crate::task::task_id::TaskId
153
154use crate::backend::finalize::FinalizeBackend;
155use crate::backend::{Backend, BackendConfig};
156use crate::error::BoxDynError;
157use crate::task::Task;
158use crate::worker::service::{IntoWorkerService, WorkerService};
159use futures_util::FutureExt;
160use futures_util::future::Map;
161use std::fmt;
162use std::future::Future;
163use std::marker::PhantomData;
164use std::task::{Context, Poll};
165use tower_service::Service;
166
167use crate::task::{from_request::FromRequest, into_response::IntoResponse};
168
169/// A helper method to build a [`TaskFn`] from an async function or closure.
170///
171/// # Example
172/// ```rust
173/// # use apalis_core::task::data::Data;
174/// #[derive(Clone)]
175/// struct State {
176///     // db: Arc<DatabaseConnection>,
177/// }
178/// async fn handler(id: u32, state: Data<State>) -> String {
179///     format!("Got id {} with state", id)
180/// }   
181///```
182/// This method can take functions with up to 16 additional arguments beyond the core request.
183///
184/// See Also:
185///
186/// - [`FromRequest`]
187/// - [`IntoResponse`]
188pub fn task_fn<F, Args, FnArgs>(f: F) -> TaskFn<F, Args, FnArgs> {
189    TaskFn {
190        f,
191        req: PhantomData,
192        fn_args: PhantomData,
193    }
194}
195
196/// An executable service implemented by a closure.
197///
198/// See [`task_fn`] for more details.
199pub struct TaskFn<F, Args, FnArgs> {
200    f: F,
201    req: PhantomData<Args>,
202    fn_args: PhantomData<FnArgs>,
203}
204
205impl<T: Copy, Args, FnArgs> Copy for TaskFn<T, Args, FnArgs> {}
206
207impl<T: Clone, Args, FnArgs> Clone for TaskFn<T, Args, FnArgs> {
208    fn clone(&self) -> Self {
209        Self {
210            f: self.f.clone(),
211            req: PhantomData,
212            fn_args: PhantomData,
213        }
214    }
215}
216
217impl<T, Args, FnArgs> fmt::Debug for TaskFn<T, Args, FnArgs> {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.debug_struct("TaskFn")
220            .field("f", &std::any::type_name::<T>())
221            .field(
222                "req",
223                &format_args!("PhantomData<Task<{}>>", std::any::type_name::<Args>(),),
224            )
225            .field(
226                "fn_args",
227                &format_args!("PhantomData<{}>", std::any::type_name::<FnArgs>()),
228            )
229            .finish()
230    }
231}
232
233/// The Future returned from [`TaskFn`] service.
234type FnFuture<F, O, R, E> = Map<F, fn(O) -> std::result::Result<R, E>>;
235
236macro_rules! impl_service_fn {
237    ($($K:ident),+) => {
238        #[allow(unused_parens)]
239        impl<T, F, Args: Send + 'static, R, $($K),+> Service<Task<Args>> for TaskFn<T, Args,  ($($K),+)>
240        where
241            T: FnMut(Args, $($K),+) -> F + Send + Clone + 'static,
242            F: Future + Send,
243            F::Output: IntoResponse<Output = R>,
244            $(
245                $K: FromRequest<Task<Args>> + Send,
246                < $K as FromRequest<Task<Args>> >::Error: std::error::Error + 'static + Send + Sync,
247            )+
248        {
249            type Response = R;
250            type Error = BoxDynError;
251            type Future = futures_util::future::BoxFuture<'static, Result<R, BoxDynError>>;
252
253            fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
254                Poll::Ready(Ok(()))
255            }
256
257            fn call(&mut self, task: Task<Args>) -> Self::Future {
258                let mut svc = self.f.clone();
259                #[allow(non_snake_case)]
260                let fut = async move {
261                    #[allow(clippy::double_parens)]
262                    let results: Result<($($K),+), BoxDynError> = { Ok(($($K::from_request(&task).await.map_err(|e| Box::new(e) as BoxDynError)?),+)) };
263                    match results {
264                        Ok(($($K),+)) => {
265                            let req = task.args;
266                            (svc)(req, $($K),+).map(F::Output::into_response).await
267                        }
268                        Err(e) => Err(e),
269                    }
270                };
271                fut.boxed()
272            }
273        }
274
275        #[allow(unused_parens)]
276        impl<T, Args,  F, R, B, O, $($K),+>
277            IntoWorkerService<B, TaskFn<T, Args,  ($($K),+)>> for T
278        where
279            B: Backend + BackendConfig<Args = Args>,
280            B::Kind: FinalizeBackend<B, Args, Backend = O>,
281            O: Backend<Task = Task<Args>>,
282            T: FnMut(Args, $($K),+) -> F + Send + Clone + 'static,
283            F: Future + Send,
284            Args: Send + 'static,
285            F::Output: IntoResponse<Output = R>,
286            TaskFn<T, Args,  ($($K),+)>: Service<O::Task>,
287
288            $(
289                $K: FromRequest<Task<Args>> + Send,
290                < $K as FromRequest<Task<Args>> >::Error: std::error::Error + 'static + Send + Sync,
291            )+
292        {
293            type Backend = O;
294            type Task = Task<Args>;
295
296            fn into_service(self, backend: B) -> WorkerService<O, TaskFn<T, Args,  ($($K),+)>> {
297                let backend = B::Kind::finalize(backend);
298                WorkerService {
299                    backend,
300                    service: task_fn(self)
301                }
302            }
303        }
304    };
305}
306
307impl<T, F, Args, R> Service<Task<Args>> for TaskFn<T, Args, ()>
308where
309    T: FnMut(Args) -> F,
310    F: Future,
311    F::Output: IntoResponse<Output = R>,
312{
313    type Response = R;
314    type Error = BoxDynError;
315    type Future = FnFuture<F, F::Output, R, BoxDynError>;
316
317    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
318        Poll::Ready(Ok(()))
319    }
320
321    fn call(&mut self, task: Task<Args>) -> Self::Future {
322        let fut = (self.f)(task.args);
323
324        fut.map(F::Output::into_response)
325    }
326}
327
328impl<T, Args, F, R, B, O> IntoWorkerService<B, TaskFn<T, Args, ()>> for T
329where
330    T: FnMut(Args) -> F,
331    F: Future,
332    F::Output: IntoResponse<Output = R>,
333    B: Backend + BackendConfig<Args = Args>,
334    B::Kind: FinalizeBackend<B, Args, Backend = O>,
335    O: Backend<Task = Task<Args>>,
336    Args: Send,
337    TaskFn<T, Args, ()>: Service<O::Task>,
338{
339    type Backend = O;
340    type Task = Task<Args>;
341
342    fn into_service(self, backend: B) -> WorkerService<O, TaskFn<T, Args, ()>> {
343        let backend = B::Kind::finalize(backend);
344        WorkerService {
345            backend,
346            service: task_fn(self),
347        }
348    }
349}
350
351impl<Args, S, B, O> IntoWorkerService<B, S> for S
352where
353    B: Backend + BackendConfig<Args = Args>,
354    B::Kind: FinalizeBackend<B, Args, Backend = O>,
355    O: Backend<Task = Task<Args>>,
356    S: Service<O::Task>,
357{
358    type Backend = O;
359    type Task = Task<Args>;
360    fn into_service(self, backend: B) -> WorkerService<O, S> {
361        let backend = B::Kind::finalize(backend);
362        WorkerService {
363            backend,
364            service: self,
365        }
366    }
367}
368
369impl_service_fn!(A);
370impl_service_fn!(A1, A2);
371impl_service_fn!(A1, A2, A3);
372impl_service_fn!(A1, A2, A3, A4);
373impl_service_fn!(A1, A2, A3, A4, A5);
374impl_service_fn!(A1, A2, A3, A4, A5, A6);
375impl_service_fn!(A1, A2, A3, A4, A5, A6, A7);
376impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8);
377impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8, A9);
378impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10);
379impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11);
380impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12);
381impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13);
382impl_service_fn!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14);
383impl_service_fn!(
384    A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15
385);
386impl_service_fn!(
387    A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16
388);