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}