Skip to main content

apalis_core/worker/
mod.rs

1//! Utilities for building and running workers.
2//!
3//! A `Worker` polls tasks from a backend, executes them using
4//! a service, emits lifecycle events, and handles graceful shutdowns. A worker is typically
5//! constructed using a [`WorkerBuilder`](crate::worker::builder).
6//!
7//! # Features
8//! - Pluggable backends for task queues (e.g., in-memory, Redis).
9//! - Middleware support for task processing.
10//! - Stream or future-based worker execution modes.
11//! - Built-in event system for logging or metrics.
12//! - Task tracking and controlled worker readiness.
13//!
14//! # Lifecycle
15//!
16//! ```mermaid
17//! graph TD
18//!     A[Start Worker] --> B[Initialize Context & Heartbeat]
19//!     B --> C[Poll Backend for Tasks]
20//!     C --> D{Task Available?}
21//!     D -- Yes --> E[Execute Task via Service Stack]
22//!     E --> F[Emit Events]
23//!     F --> C
24//!     D -- No --> F
25//!     F --> G{Shutdown Signal?}
26//!     G -- Yes --> H[Graceful Shutdown]
27//!     G -- No --> C
28//! ```
29//! Worker lifecycle is composed of several stages:
30//! - Initialize context and heartbeat
31//! - Poll backend for tasks
32//! - Execute tasks via service stack
33//! - Emit events (Idle, Success, Error, HeartBeat)
34//! - Graceful shutdown on signal or stop
35//!
36//! # Examples
37//!
38//! ## Run as a future
39//! ```rust,no_run
40//! # use apalis_core::{worker::builder::WorkerBuilder, backend::memory::MemoryStorage};
41//! # use apalis_core::error::BoxDynError;
42//! # use apalis_core::backend::TaskSink;
43//!
44//! #[tokio::main]
45//! async fn main() -> Result<(), BoxDynError> {
46//!     let mut storage = MemoryStorage::new();
47//!     for i in 0..5 {
48//!         storage.push(i).await?;
49//!     }
50//!
51//!     async fn handler(task: u32) {
52//!         println!("Processing task: {task}");
53//!     }
54//!
55//!     let worker = WorkerBuilder::new("worker-1")
56//!         .backend(storage)
57//!         .build(handler);
58//!
59//!     worker.run().await?;
60//!     Ok(())
61//! }
62//! ```
63//!
64//! ## Runner as a stream
65//! The `stream` interface yields worker events (e.g., `Success`, `Error`) while running:
66//! ```rust,no_run
67//! # use apalis_core::worker::builder::WorkerBuilder;
68//! # use apalis_core::backend::memory::MemoryStorage;
69//! # use futures_util::StreamExt;
70//! # #[tokio::main]
71//! # async fn main() {
72//! #   let mut storage = MemoryStorage::new();
73//! #   async fn handler(task: u32) {
74//! #        println!("Processing task: {task}");
75//! #    }
76//! #   let worker = WorkerBuilder::new("worker-1")
77//! #        .backend(storage)
78//! #        .build(handler);
79//! let mut stream = worker.stream();
80//! while let Some(evt) = stream.next().await {
81//!     println!("Event: {:?}", evt);
82//! }
83//! # }
84//! ```
85use crate::backend::{Backend, BackendConfig};
86use crate::error::{BoxDynError, WorkerError};
87use crate::monitor::shutdown::Shutdown;
88use crate::task::Task;
89use crate::task::data::Data;
90use crate::worker::call_all::CallAllUnordered;
91use crate::worker::context::WorkerContext;
92use crate::worker::event::{Event, RawEventListener};
93use crate::worker::lifecycle::{LifecycleLayer, LifecycleService};
94use crate::worker::stream::WorkerStream;
95use futures_util::{Future, FutureExt, Stream, StreamExt, TryFutureExt};
96use std::fmt::{self};
97use std::marker::PhantomData;
98use tower_layer::{Layer, Stack};
99use tower_service::Service;
100
101pub mod builder;
102pub mod call_all;
103pub mod context;
104pub mod event;
105pub mod ext;
106pub(crate) mod handle;
107pub mod lifecycle;
108pub mod service;
109mod state;
110mod stream;
111
112/// A worker polls a backend and processes tasks using a service.
113///
114/// Its the core component responsible for task polling, execution, and lifecycle management.
115///
116/// # Example
117/// Basic example:
118/// ```rust,no_run
119/// # use apalis_core::error::BoxDynError;
120/// # use apalis_core::backend::memory::MemoryStorage;
121/// # use apalis_core::worker::builder::WorkerBuilder;
122/// # use apalis_core::backend::TaskSink;
123///
124/// #[tokio::main]
125/// async fn main() -> Result<(), BoxDynError> {
126///     let mut storage = MemoryStorage::new();
127///     for i in 0..5 {
128///         storage.push(i).await?;
129///     }
130///
131///     async fn handler(task: u32) {
132///         println!("Processing task: {task}");
133///     }
134///
135///     let worker = WorkerBuilder::new("worker-1")
136///         .backend(storage)
137///         .build(handler);
138///
139///     worker.run().await?;
140///     Ok(())
141/// }
142/// ```
143/// See [module level documentation](self) for more details.
144#[must_use = "Workers must be run or streamed to execute tasks"]
145pub struct Worker<Args, Backend, Svc, Middleware> {
146    pub(crate) context: WorkerContext,
147    pub(crate) backend: Backend,
148    pub(crate) service: Svc,
149    pub(crate) middleware: Middleware,
150    pub(crate) task_marker: PhantomData<Args>,
151    pub(crate) shutdown: Option<Shutdown>,
152    pub(crate) event_handler: RawEventListener,
153}
154
155impl<Args, B, Svc, Middleware> fmt::Debug for Worker<Args, B, Svc, Middleware>
156where
157    Svc: fmt::Debug,
158    B: fmt::Debug,
159{
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.debug_struct("Worker")
162            .field("service", &self.service)
163            .field("backend", &self.backend)
164            .finish()
165    }
166}
167
168impl<Args, B, Svc, M> Worker<Args, B, Svc, M> {
169    /// Build a worker that is ready for execution
170    pub fn new(context: WorkerContext, backend: B, service: Svc, layers: M) -> Self {
171        Self {
172            context,
173            backend,
174            service,
175            middleware: layers,
176            task_marker: PhantomData,
177            shutdown: None,
178            event_handler: Box::new(|_, _| {}),
179        }
180    }
181}
182
183impl<Args, S, M, FB> Worker<Args, FB, S, M>
184where
185    FB: BackendConfig + Backend<Task = Task<FB::Args>> + Send + Unpin,
186    S: Service<Task<FB::Args>> + Send + 'static,
187    FB::Args: Send + 'static,
188    Args: Send + 'static,
189    FB::Error: Into<BoxDynError> + Send + 'static,
190    M: Layer<LifecycleService<S>>,
191    FB::Layer: Layer<<M as Layer<LifecycleService<S>>>::Service>,
192    <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service:
193        Service<Task<FB::Args>>,
194    <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service: Send + 'static,
195    <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
196        Task<<FB as BackendConfig>::Args>,
197    >>::Future: Send,
198    <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
199        Task<<FB as BackendConfig>::Args>,
200    >>::Error: Into<BoxDynError> + Send + Sync + 'static,
201    <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
202        Task<<FB as BackendConfig>::Args>,
203    >>::Response: Send + Sync + 'static,
204{
205    /// Run the worker until completion
206    ///
207    /// # Example
208    /// ```no_run
209    /// # use apalis_core::error::BoxDynError;
210    /// # use apalis_core::backend::memory::MemoryStorage;
211    /// # use apalis_core::backend::TaskSink;
212    /// # use apalis_core::worker::builder::WorkerBuilder;
213    ///
214    /// #[tokio::main]
215    /// async fn main() -> Result<(), BoxDynError> {
216    ///     let mut storage = MemoryStorage::new();
217    ///     for i in 0..5 {
218    ///         storage.push(i).await?;
219    ///     }
220    ///
221    ///     async fn handler(task: u32) {
222    ///         println!("Processing task: {task}");
223    ///     }
224    ///
225    ///     let worker = WorkerBuilder::new("worker-1")
226    ///         .backend(storage)
227    ///         .build(handler);
228    ///
229    ///     worker.run().await?;
230    ///     Ok(())
231    /// }
232    /// ```
233    pub async fn run(self) -> Result<(), WorkerError> {
234        let mut stream = self.stream();
235        while let Some(res) = stream.next().await {
236            match res {
237                Ok(_) => {}
238                Err(e) => return Err(e),
239            }
240        }
241        Ok(())
242    }
243
244    /// Run the worker until a shutdown signal future is complete.
245    pub async fn run_until<Fut, Err>(mut self, signal: Fut) -> Result<(), WorkerError>
246    where
247        Fut: Future<Output = Result<(), Err>> + Send + 'static,
248        FB: Send,
249        M: Send,
250        Err: Into<WorkerError> + Send + 'static,
251    {
252        let shutdown = self.shutdown.take().unwrap_or_default();
253        let terminator = shutdown.shutdown_after(signal);
254        let c = self.context.clone();
255        let worker = self.run();
256        futures_util::try_join!(
257            worker,
258            terminator.map_ok(|_| c.stop()).map_err(|e| e.into()),
259        )
260        .map(|_| ())
261    }
262
263    /// Run the worker until a shutdown signal future is complete.
264    ///
265    /// *Note*: Using this function requires you to call `ctx.stop()` in the future to completely stop the worker.
266    ///
267    /// This can also be very powerful with pausing and resuming the worker using the context.
268    ///
269    /// # Example
270    ///
271    /// ```rust,no_run
272    ///
273    /// # use apalis_core::{worker::builder::WorkerBuilder, backend::memory::MemoryStorage};
274    /// # use apalis_core::error::BoxDynError;
275    /// # use apalis_core::backend::TaskSink;
276    /// # use std::time::Duration;
277    /// # use tokio::time::sleep;
278    /// # use apalis_core::error::WorkerError;
279    ///
280    /// #[tokio::main]
281    /// async fn main() -> Result<(), BoxDynError> {
282    ///     let mut storage = MemoryStorage::new();
283    ///     for i in 0..5 {
284    ///         storage.push(i).await?;
285    ///     }
286    ///     async fn handler(task: u32) {
287    ///         println!("Processing task: {task}");
288    ///     }
289    ///     let worker = WorkerBuilder::new("worker-1")
290    ///         .backend(storage)
291    ///         .build(handler);
292    ///     worker.run_with_ctx(|ctx| async move {
293    ///         sleep(Duration::from_secs(1)).await;
294    ///         ctx.stop()?;
295    ///         Ok(())
296    ///     }).await?;
297    ///     Ok(())
298    /// }
299    /// ```
300    pub async fn run_with_ctx<F, Fut>(mut self, mut fut: F) -> Result<(), WorkerError>
301    where
302        F: FnMut(WorkerContext) -> Fut + Send + 'static,
303        Fut: Future<Output = Result<(), WorkerError>> + Send,
304        FB: Send,
305        M: Send,
306    {
307        let shutdown = self.shutdown.take().unwrap_or_default();
308        let terminator = shutdown.shutdown_after(fut(self.context.clone()));
309        let worker = self.run().boxed();
310        futures_util::try_join!(terminator.map_ok(|_| ()), worker).map(|_| ())
311    }
312
313    /// Returns a stream that will yield events as they occur within the worker's lifecycle
314    ///
315    /// # Example
316    ///
317    /// ```rust,no_run
318    /// # use apalis_core::error::BoxDynError;
319    /// # use apalis_core::backend::memory::MemoryStorage;
320    /// # use apalis_core::worker::builder::WorkerBuilder;
321    /// # use apalis_core::backend::TaskSink;
322    /// # use futures_util::StreamExt;
323    /// #[tokio::main]
324    /// async fn main() -> Result<(), BoxDynError> {
325    ///     let mut storage = MemoryStorage::new();
326    ///     for i in 0..5 {
327    ///         storage.push(i).await?;
328    ///     }
329    ///     async fn handler(task: u32) {
330    ///         println!("Processing task: {task}");
331    ///     }
332    ///     let worker = WorkerBuilder::new("worker-1")
333    ///         .backend(storage)
334    ///         .build(handler);
335    ///     let mut stream = worker.stream();
336    ///     while let Some(evt) = stream.next().await {
337    ///         println!("Event: {:?}", evt);
338    ///     }
339    ///     Ok(())
340    /// }
341    /// ```
342    pub fn stream(
343        mut self,
344    ) -> impl Stream<Item = Result<Event, WorkerError>> + use<Args, S, M, FB> {
345        let ctx = &mut self.context;
346        ctx.bind_service::<M::Service>();
347        let mut backend = self.backend;
348        let event_handler = self.event_handler;
349        ctx.add_listener(event_handler);
350        let backend_middleware = backend.middleware(ctx);
351
352        struct ServiceBuilder<L> {
353            layer: L,
354        }
355
356        impl<L> ServiceBuilder<L> {
357            fn layer<T>(self, layer: T) -> ServiceBuilder<Stack<T, L>> {
358                ServiceBuilder {
359                    layer: Stack::new(layer, self.layer),
360                }
361            }
362            fn service<S>(&self, service: S) -> L::Service
363            where
364                L: Layer<S>,
365            {
366                self.layer.layer(service)
367            }
368        }
369
370        let svc = ServiceBuilder {
371            layer: Data::new(ctx.clone()),
372        };
373        let service = svc
374            // backend middleware should be the next layer so it can observe all requests released by user middleware
375            .layer(backend_middleware)
376            // pass the user defined middleware
377            .layer(self.middleware)
378            // A lifecycle service that
379            // - when all layers are ready, inform the worker its ready to accept tasks
380            // - Track all tasks to allow graceful shutdowns
381            // - increment the attempt count on the first poll
382            // - Generate a task token
383            .layer(LifecycleLayer::new(ctx.clone()))
384            .service(self.service);
385        let w = ctx.clone();
386        StreamExt::inspect(
387            WorkerStream::new(service, backend, ctx),
388            move |res| match &res {
389                Ok(e) => {
390                    w.emit_event(e);
391                }
392                Err(e) => {
393                    error!("WorkerError: {e}");
394                }
395            },
396        )
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use std::{
403        future::ready,
404        io::ErrorKind,
405        ops::Deref,
406        sync::{
407            Arc,
408            atomic::{AtomicUsize, Ordering},
409        },
410        time::Duration,
411    };
412
413    use super::*;
414    use crate::{
415        backend::{TaskSink, memory::MemoryStorage},
416        task::ExecutionContext,
417        worker::{
418            builder::WorkerBuilder,
419            ext::{
420                ack::{Acknowledge, AcknowledgementExt},
421                circuit_breaker::CircuitBreaker,
422                event_listener::EventListenerExt,
423                long_running::LongRunningExt,
424            },
425        },
426    };
427    use futures_channel::mpsc::SendError;
428    use futures_core::future::BoxFuture;
429
430    #[cfg(feature = "task-context")]
431    use crate::task::context::TaskContext;
432
433    const ITEMS: u32 = 100;
434
435    #[tokio::test]
436    async fn basic_worker_run() {
437        let mut in_memory = MemoryStorage::new();
438        for i in 0..ITEMS {
439            in_memory.push(i).await.unwrap();
440        }
441
442        #[derive(Clone, Debug, Default)]
443        struct Count(Arc<AtomicUsize>);
444
445        impl Deref for Count {
446            type Target = Arc<AtomicUsize>;
447            fn deref(&self) -> &Self::Target {
448                &self.0
449            }
450        }
451
452        async fn task(
453            task: u32,
454            worker: WorkerContext,
455            count: Data<Count>,
456            #[cfg(feature = "task-context")] ctx: TaskContext,
457        ) -> Result<(), BoxDynError> {
458            #[cfg(feature = "task-context")]
459            tokio::spawn(ctx.run_until_executed(async {
460                tokio::time::sleep(Duration::from_secs(3)).await;
461                // Because the task stops after 2 seconds
462                println!("This is never called");
463            }));
464            tokio::time::sleep(Duration::from_secs(2)).await;
465            count.fetch_add(1, Ordering::Relaxed);
466            if task == ITEMS - 1 {
467                worker.stop().unwrap();
468                return Err("Worker stopped!")?;
469            }
470
471            #[cfg(feature = "task-context")]
472
473            println!("Elapsed: {:?}", ctx.elapsed());
474            Ok(())
475        }
476
477        #[derive(Debug, Clone)]
478        struct MyAcknowledger;
479
480        impl Acknowledge<()> for MyAcknowledger {
481            type Error = SendError;
482            type Future = BoxFuture<'static, Result<(), SendError>>;
483            fn ack(
484                &mut self,
485                res: &Result<(), BoxDynError>,
486                ctx: &ExecutionContext,
487            ) -> Self::Future {
488                println!("{res:?}, {ctx:?}");
489                // Call webhook with the result and ctx?
490                ready(Ok(())).boxed()
491            }
492        }
493
494        let worker = WorkerBuilder::new("rango-tango")
495            .backend(in_memory)
496            .data(Count::default())
497            .break_circuit()
498            .long_running()
499            .ack_with(MyAcknowledger)
500            .on_event(|wrk, ev| {
501                println!("On Event = {ev:?} from {}", wrk.name());
502            })
503            .build(task);
504        worker.run().await.unwrap();
505    }
506
507    #[tokio::test]
508    async fn basic_worker_stream() {
509        let mut in_memory = MemoryStorage::new();
510
511        for i in 0..ITEMS {
512            in_memory.push(i).await.unwrap();
513        }
514
515        #[derive(Clone, Debug, Default)]
516        struct Count(Arc<AtomicUsize>);
517
518        impl Deref for Count {
519            type Target = Arc<AtomicUsize>;
520            fn deref(&self) -> &Self::Target {
521                &self.0
522            }
523        }
524
525        async fn task(task: u32, count: Data<Count>, worker: WorkerContext) {
526            tokio::time::sleep(Duration::from_secs(1)).await;
527            count.fetch_add(1, Ordering::Relaxed);
528            if task == ITEMS - 1 {
529                worker.stop().unwrap();
530            }
531        }
532        let worker = WorkerBuilder::new("rango-tango")
533            .backend(in_memory)
534            .data(Count::default())
535            .break_circuit()
536            .long_running()
537            .on_event(|wrk, ev| {
538                println!("CTX {:?}, On Event = {ev:?}", wrk.name());
539            })
540            .build(task);
541        let mut event_stream = worker.stream();
542        while let Some(Ok(ev)) = event_stream.next().await {
543            println!("On Event = {ev:?}");
544        }
545    }
546
547    #[tokio::test]
548    async fn with_shutdown_signal() {
549        let mut in_memory = MemoryStorage::new();
550        for i in 0..ITEMS {
551            in_memory.push(i).await.unwrap();
552        }
553
554        async fn task(_: u32) -> Result<(), BoxDynError> {
555            Ok(())
556        }
557
558        let worker = WorkerBuilder::new("rango-tango")
559            .backend(in_memory)
560            .on_event(|wrk, ev| {
561                println!("On Event = {ev:?} from {}", wrk.name());
562            })
563            .build(task);
564        let signal = async {
565            let ctrl_c = tokio::signal::ctrl_c().map_err(|e| e.into());
566            let timeout = tokio::time::sleep(Duration::from_secs(5)).map(|_| {
567                Err::<(), WorkerError>(WorkerError::IoError(std::io::Error::new(
568                    ErrorKind::Other,
569                    "Timeout",
570                )))
571            });
572            let _ = futures_util::try_join!(ctrl_c, timeout)?;
573            Ok::<(), WorkerError>(())
574        };
575        let res = worker.run_until(signal).await;
576        match res {
577            Err(WorkerError::IoError(_)) => {
578                println!("Worker exited gracefully");
579            }
580            _ => panic!("Expected graceful exit error"),
581        }
582    }
583}