Skip to main content

apalis_core/monitor/
mod.rs

1//! # Monitor
2//!
3//! `Monitor` provides centralized coordination and lifecycle management for multiple workers.
4//! It is responsible for executing, monitoring, and gracefully shutting down all registered workers in a robust and customizable manner.
5//!
6//! ## Features
7//!
8//! - Register and run one or more workers.
9//! - Handle graceful shutdown with optional timeout.
10//! - Register custom event handlers to observe worker events (e.g. task received, completed, errored).
11//! - Integrate shutdown with system signals (e.g. `SIGINT`, `SIGTERM`) or custom triggers.
12//! - **Restart Strategy:** Configure custom logic to automatically restart workers on failure.
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! # use apalis_core::monitor::Monitor;
18//! # use apalis_core::worker::builder::WorkerBuilder;
19//! # use apalis_core::backend::memory::MemoryStorage;
20//! # use apalis_core::task::Task;
21//! # use apalis_core::backend::TaskSink;
22//! use tower::service_fn;
23//! # use std::time::Duration;
24//!
25//! #[tokio::main]
26//! async fn main() {
27//!
28//!     async fn task(req: u32) -> Result<u32, std::io::Error> {
29//!         println!("Processing task: {:?}", req);
30//!         Ok::<_, std::io::Error>(req)
31//!     }
32//!
33//!     let monitor = Monitor::new()
34//!         .on_event(|worker, event| println!("{}: {:?}", worker.name(), event))
35//!         .register(move |_| {
36//!             WorkerBuilder::new("demo-worker")
37//!                 .backend(MemoryStorage::new())
38//!                 .build(task)
39//!          });
40//!
41//!     // Start monitor and run all registered workers
42//!     monitor.run().await.unwrap();
43//! }
44//! ```
45//!
46//! ## Graceful Shutdown with Timeout
47//!
48//! To force shutdown after a certain duration, use the `shutdown_timeout` method:
49//!
50//! ```ignore
51//! # use apalis_core::monitor::Monitor;
52//! # use std::time::Duration;
53//! let monitor = Monitor::new()
54//!     .shutdown_timeout(Duration::from_secs(10))
55//!     .run_with_signal(signal::ctrl_c())
56//!     .await
57//!     .unwrap();
58//! ```
59//!
60//! This ensures that if any worker hangs or takes too long to finish, the monitor will shut down after 10 seconds.
61//!
62//! ## Restarting Workers on Failure
63//!
64//! You can configure the monitor to restart workers when they fail, using custom logic:
65//!
66//! ```rust
67//! use apalis_core::monitor::Monitor;
68//!
69//! let monitor = Monitor::new()
70//!     .should_restart(|worker, error| {
71//!         let restarts = worker.restarts();
72//!         println!("Worker failed: {error:?}, attempt: {restarts}");
73//!         restarts < 3 // Run up to 3 times
74//!     });
75//! ```
76//!
77//! ## Observing Worker Events
78//!
79//! Register event handlers to observe worker lifecycle events:
80//!
81//! ```rust
82//! use apalis_core::monitor::Monitor;
83//!
84//! let monitor = Monitor::new()
85//!     .on_event(|worker, event| println!("Worker {}: {:?}", worker.name(), event));
86//! ```
87//!
88//! ## Registering Multiple Workers
89//!
90//! You can register multiple workers by building and registering each worker individually.
91//! This is useful when you want to run several workers with the same configuration:
92//!
93//! `rust
94//! # use apalis_core::worker::builder::WorkerBuilder;
95//! # use apalis_core::monitor::Monitor;
96//! # use apalis_core::backend::memory::MemoryStorage;
97//! # async fn task(_: u32) {}
98//! let mut monitor = Monitor::new();
99//!
100//! for index in 0..3 {
101//!     monitor = monitor.register(|restarts|
102//!         WorkerBuilder::new(format!("worker-{index}-run-{restarts}"))
103//!             .backend(MemoryStorage::new())
104//!             .build(task),
105//!     );
106//! }
107//! `.
108//!
109//! ## Example: Full Monitor Usage
110//!
111//! ```rust
112//! # use std::time::Duration;
113//! # use apalis_core::worker::builder::WorkerBuilder;
114//! # use apalis_core::monitor::Monitor;
115//! # use apalis_core::backend::memory::MemoryStorage;
116//! # async fn example() {
117//! # async fn task(_: u32) {}
118//! let monitor = Monitor::new()
119//!     .register(|index|
120//!         WorkerBuilder::new(format!("worker-{index}"))
121//!         .backend(MemoryStorage::new())
122//!         .build(task)
123//!     )
124//!     .should_restart(|wrk, _| wrk.restarts() < 5)
125//!     .on_event(|worker, event| println!("Event: {:?}", event))
126//!     .shutdown_timeout(Duration::from_secs(10));
127//! let ctx = monitor.context();
128//! monitor.run().await.unwrap();
129//! # }
130//! ```
131//!
132//! ## Error Handling
133//!
134//! If any worker fails, the monitor will return a `MonitorError` containing details about the failure.
135//! You can inspect the error to see which workers failed and why.
136//!
137//! [Worker]: crate::worker::Worker
138
139use std::{
140    fmt::{self, Debug, Formatter},
141    pin::Pin,
142    sync::{Arc, RwLock},
143    task::{Context, Poll},
144};
145
146use futures_util::{
147    Future, FutureExt, StreamExt,
148    future::{BoxFuture, Shared},
149    stream::FuturesUnordered,
150};
151use tower_layer::Layer;
152use tower_service::Service;
153
154use crate::{
155    backend::{Backend, BackendConfig, future::BoxSyncFuture},
156    error::{BoxDynError, WorkerError},
157    monitor::{context::MonitorContext, shutdown::Shutdown},
158    task::Task,
159    worker::{
160        Worker,
161        context::WorkerContext,
162        event::{Event, EventHandlerBuilder},
163        lifecycle::LifecycleService,
164    },
165};
166
167pub mod context;
168pub mod shutdown;
169
170type WorkerFactory = Box<
171    dyn Fn(&mut WorkerContext) -> BoxFuture<'static, Result<(), WorkerError>>
172        + 'static
173        + Send
174        + Sync,
175>;
176
177type ShouldRestart =
178    Arc<RwLock<Option<Box<dyn Fn(&WorkerContext, &WorkerError) -> bool + 'static + Send + Sync>>>>;
179
180#[pin_project::pin_project]
181/// A worker that is monitored by the `Monitor`.
182struct MonitoredWorker {
183    factory: WorkerFactory,
184    worker: WorkerContext,
185    #[pin]
186    fut: Option<BoxSyncFuture<Result<(), WorkerError>>>,
187    should_restart: ShouldRestart,
188}
189
190/// Represents errors that occurred in a monitored worker, including its context and the error itself.
191#[derive(Debug)]
192pub struct MonitoredWorkerError {
193    ctx: WorkerContext,
194    error: WorkerError,
195}
196
197impl Future for MonitoredWorker {
198    type Output = Result<(), MonitoredWorkerError>;
199
200    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
201        use std::panic::{AssertUnwindSafe, catch_unwind};
202
203        loop {
204            let mut this = self.as_mut().project();
205            let worker = this.worker;
206            let name = worker.name().to_owned();
207            debug!(?name, "polling worker");
208
209            // There is no future to poll.
210            if this.fut.is_none() {
211                let fut = (this.factory)(worker);
212                debug!(?name, "created new future");
213                this.fut.set(Some(fut.into()));
214            }
215
216            // Worker is running but shutdown has been called
217            if worker.is_running() && worker.is_shutting_down() {
218                info!(?name, "worker shutting down, stopping");
219                if let Err(err) = worker.stop() {
220                    error!(?name, ?err, "failed to stop worker");
221                    return Poll::Ready(Err(MonitoredWorkerError {
222                        ctx: worker.clone(),
223                        error: err,
224                    }));
225                }
226                continue;
227            }
228
229            let fut = this
230                .fut
231                .as_mut()
232                .get_mut()
233                .as_mut()
234                .expect("future was just set above; this should be unreachable");
235
236            let poll_result =
237                catch_unwind(AssertUnwindSafe(|| fut.poll_unpin(cx))).map_err(|err| {
238                    let err = if let Some(s) = err.downcast_ref::<&str>() {
239                        (*s).to_owned()
240                    } else if let Some(s) = err.downcast_ref::<String>() {
241                        s.clone()
242                    } else {
243                        "Unknown panic".to_owned()
244                    };
245                    WorkerError::PanicError(err)
246                });
247
248            match poll_result {
249                Ok(Poll::Pending) => return Poll::Pending,
250                Ok(Poll::Ready(Ok(()))) => return Poll::Ready(Ok(())),
251                Ok(Poll::Ready(Err(error))) | Err(error) => {
252                    warn!(?name, ?error, "worker future ended with error");
253                    let _fut = this.fut.as_mut().get_mut().take();
254
255                    let should_restart = match this.should_restart.read() {
256                        Ok(guard) => guard,
257                        Err(poison_err) => {
258                            error!(?name, ?poison_err, "should_restart lock poisoned");
259                            return Poll::Ready(Err(MonitoredWorkerError {
260                                ctx: worker.clone(),
261                                error,
262                            }));
263                        }
264                    };
265
266                    match should_restart.as_ref().map(|s| s.as_ref()) {
267                        Some(cb) => {
268                            if !(cb)(worker, &error) {
269                                info!(?name, "restart callback declined restart");
270                                if let Err(stop_err) = worker.stop() {
271                                    error!(?name, ?stop_err, "failed to stop worker");
272                                }
273                                return Poll::Ready(Err(MonitoredWorkerError {
274                                    ctx: worker.clone(),
275                                    error,
276                                }));
277                            }
278                            info!(?name, "restarting worker");
279                            if let Err(restart_err) = worker.restart() {
280                                error!(?name, ?restart_err, "failed to restart worker");
281                                return Poll::Ready(Err(MonitoredWorkerError {
282                                    ctx: worker.clone(),
283                                    error,
284                                }));
285                            }
286                        }
287                        _ => {
288                            return Poll::Ready(Err(MonitoredWorkerError {
289                                ctx: worker.clone(),
290                                error,
291                            }));
292                        }
293                    }
294                }
295            }
296        }
297    }
298}
299
300/// A monitor for coordinating and managing a collection of workers.
301#[derive(Default)]
302pub struct Monitor {
303    workers: Vec<MonitoredWorker>,
304    terminator: Option<Shared<BoxFuture<'static, ()>>>,
305    shutdown: Shutdown,
306    event_handler: EventHandlerBuilder,
307    should_restart: ShouldRestart,
308}
309
310impl Debug for Monitor {
311    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
312        f.debug_struct("Monitor")
313            .field("shutdown", &"[Graceful shutdown listener]")
314            .field("workers", &self.workers.len())
315            .finish()
316    }
317}
318
319impl Monitor {
320    fn run_worker<Args, S, FB, M>(
321        worker: Worker<Args, FB, S, M>,
322    ) -> BoxFuture<'static, Result<(), WorkerError>>
323    where
324        FB: BackendConfig + Backend<Task = Task<FB::Args>> + Send + Unpin + 'static,
325        S: Service<Task<FB::Args>> + Send + 'static,
326        FB::Args: Send + 'static,
327        Args: Send + 'static,
328        FB::Error: Into<BoxDynError> + Send + 'static,
329        M: Layer<LifecycleService<S>> + 'static,
330        FB::Layer: Layer<<M as Layer<LifecycleService<S>>>::Service>,
331        <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service:
332            Service<Task<FB::Args>>,
333        <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service: Send + 'static,
334        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
335            Task<<FB as BackendConfig>::Args>,
336        >>::Future: Send,
337        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
338            Task<<FB as BackendConfig>::Args>,
339        >>::Error: Into<BoxDynError> + Send + Sync + 'static,
340        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
341            Task<<FB as BackendConfig>::Args>,
342        >>::Response: Send + Sync + 'static,
343    {
344        let mut stream = worker.stream();
345        async move {
346            loop {
347                match stream.next().await {
348                    Some(Err(e)) => return Err(e),
349                    None => return Ok(()),
350                    _ => (),
351                }
352            }
353        }
354        .boxed()
355    }
356    /// Registers a worker into the monitor registry.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use apalis_core::monitor::Monitor;
362    /// # use apalis_core::backend::memory::MemoryStorage;
363    /// # use apalis_core::worker::builder::WorkerBuilder;
364    /// # async fn example() {
365    /// let monitor = Monitor::new();
366    /// monitor
367    /// .register(|restarts| {
368    ///     WorkerBuilder::new("example-worker")
369    ///         .backend(MemoryStorage::new())
370    ///         .build(|args: u32| async {})
371    /// })
372    /// .run()
373    /// .await;
374    /// # }
375    /// ```
376    #[must_use]
377    pub fn register<Args, S, FB, M>(
378        mut self,
379        factory: impl Fn(usize) -> Worker<Args, FB, S, M> + 'static + Send + Sync,
380    ) -> Self
381    where
382        FB: BackendConfig + Backend<Task = Task<FB::Args>> + Send + Unpin + 'static,
383        S: Service<Task<FB::Args>> + Send + 'static,
384        FB::Args: Send + 'static,
385        Args: Send + 'static,
386        FB::Error: Into<BoxDynError> + Send + 'static,
387        M: Layer<LifecycleService<S>> + 'static,
388        FB::Layer: Layer<<M as Layer<LifecycleService<S>>>::Service>,
389        <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service:
390            Service<Task<FB::Args>>,
391        <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service: Send + 'static,
392        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
393            Task<<FB as BackendConfig>::Args>,
394        >>::Future: Send,
395        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
396            Task<<FB as BackendConfig>::Args>,
397        >>::Error: Into<BoxDynError> + Send + Sync + 'static,
398        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
399            Task<<FB as BackendConfig>::Args>,
400        >>::Response: Send + Sync + 'static,
401    {
402        let shutdown = Some(self.shutdown.clone());
403        let handler = self.event_handler.clone();
404
405        let (fut, worker) = {
406            let mut w = factory(0);
407            let ctx = &mut w.context;
408            ctx.bind_service::<M::Service>();
409            ctx.add_listener(move |ctx, ev| {
410                let handlers = handler.read();
411                if let Ok(handlers) = handlers {
412                    for h in handlers.iter() {
413                        h(ctx, ev);
414                    }
415                }
416            });
417            ctx.shutdown = shutdown;
418            let c = ctx.clone();
419            let fut = Self::run_worker(w).into();
420            (Some(fut), c)
421        };
422
423        let worker = MonitoredWorker {
424            fut,
425            factory: Box::new(move |worker| {
426                let mut new_worker = factory(worker.restarts());
427
428                // Replace current worker name with new name
429                if worker.name().ne(new_worker.context.name()) {
430                    let name = Arc::make_mut(&mut worker.name);
431                    *name = new_worker.context.name().to_owned();
432                }
433
434                // Reuse the current context
435                new_worker.context = worker.clone();
436
437                (Self::run_worker(new_worker)) as _
438            }),
439            worker,
440            should_restart: self.should_restart.clone(),
441        };
442        self.workers.push(worker);
443        self
444    }
445
446    /// Registers N workers into the monitor registry.
447    ///
448    /// # Examples
449    ///
450    /// ```
451    /// use apalis_core::monitor::Monitor;
452    /// # use apalis_core::backend::memory::MemoryStorage;
453    /// # use apalis_core::worker::builder::WorkerBuilder;
454    /// # async fn example() {
455    /// let monitor = Monitor::new();
456    /// monitor
457    /// .register_n(5, |index, restarts| {
458    ///     WorkerBuilder::new("example-worker")
459    ///         .backend(MemoryStorage::new())
460    ///         .build(|_: u32| async {})
461    /// })
462    /// .run()
463    /// .await;
464    /// # }
465    /// ```
466    pub fn register_n<Args, S, FB, M>(
467        mut self,
468        count: usize,
469        factory: impl Fn(usize, usize) -> Worker<Args, FB, S, M> + 'static + Send + Sync,
470    ) -> Self
471    where
472        FB: BackendConfig + Backend<Task = Task<FB::Args>> + Send + Unpin + 'static,
473        S: Service<Task<FB::Args>> + Send + 'static,
474        FB::Args: Send + 'static,
475        Args: Send + 'static,
476        FB::Error: Into<BoxDynError> + Send + 'static,
477        M: Layer<LifecycleService<S>> + 'static,
478        FB::Layer: Layer<<M as Layer<LifecycleService<S>>>::Service>,
479        <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service:
480            Service<Task<FB::Args>>,
481        <FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service: Send + 'static,
482        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
483            Task<<FB as BackendConfig>::Args>,
484        >>::Future: Send,
485        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
486            Task<<FB as BackendConfig>::Args>,
487        >>::Error: Into<BoxDynError> + Send + Sync + 'static,
488        <<FB::Layer as Layer<<M as Layer<LifecycleService<S>>>::Service>>::Service as Service<
489            Task<<FB as BackendConfig>::Args>,
490        >>::Response: Send + Sync + 'static,
491    {
492        let factory = Arc::new(factory);
493        for index in 0..count {
494            let f = factory.clone();
495            self = self.register(move |id| f(index, id));
496        }
497        self
498    }
499
500    /// Runs the monitor and all its registered workers until they have all completed or a shutdown signal is received.
501    ///
502    /// # Arguments
503    ///
504    /// * `signal` - A `Future` that resolves when a shutdown signal is received.
505    ///
506    /// # Errors
507    ///
508    /// If the monitor fails to shutdown gracefully, an `std::io::Error` will be returned.
509    ///
510    /// # Remarks
511    ///
512    /// If a timeout has been set using the `Monitor::shutdown_timeout` method, the monitor
513    /// will wait for all workers to complete up to the timeout duration before exiting.
514    /// If the timeout is reached and workers have not completed, the monitor will exit forcefully.
515    pub async fn run_with_signal<S>(self, signal: S) -> Result<(), MonitorError>
516    where
517        S: Send + Future<Output = std::io::Result<()>>,
518    {
519        let shutdown = self.shutdown.clone();
520        let ctx = self.context();
521        let shutdown_after = async move {
522            let res = signal.await;
523            let _ = ctx.shutdown();
524            res
525        };
526        if let Some(terminator) = self.terminator {
527            let _res = futures_util::future::select(
528                Self::run_all_workers(self.workers, shutdown).boxed(),
529                async {
530                    let res = shutdown_after.await;
531                    terminator.await;
532                    res.map_err(MonitorError::ShutdownSignal)
533                }
534                .boxed(),
535            )
536            .await;
537        } else {
538            let runner = self.run();
539            let res = futures_util::join!(shutdown_after, runner); // If no terminator is provided, we wait for both the shutdown call and all workers to complete
540            match res {
541                (Ok(_), Ok(_)) => {
542                    // All good
543                }
544                (Err(e), Ok(_) | Err(_)) => return Err(e.into()),
545                (Ok(_), Err(e)) => return Err(e),
546            }
547        }
548        Ok(())
549    }
550
551    /// Runs the monitor and all its registered workers until they have all completed.
552    ///
553    /// # Errors
554    ///
555    /// If the monitor fails to run gracefully, an `std::io::Error` will be returned.
556    ///
557    /// # Remarks
558    ///
559    /// If all workers have completed execution, then by default the monitor will start a shutdown
560    pub async fn run(self) -> Result<(), MonitorError> {
561        let shutdown = self.shutdown.clone();
562        let shutdown_future = self.shutdown.boxed().map(|_| ());
563        let (result, _) = futures_util::join!(
564            Self::run_all_workers(self.workers, shutdown),
565            shutdown_future,
566        );
567
568        result
569    }
570    async fn run_all_workers(
571        workers: Vec<MonitoredWorker>,
572        shutdown: Shutdown,
573    ) -> Result<(), MonitorError> {
574        // `FuturesUnordered` only polls a worker whose waker fired, so every shutdown path
575        // must wake the workers explicitly; `MonitorContext::shutdown` does that.
576        let results: Vec<_> = FuturesUnordered::from_iter(workers).collect().await;
577
578        shutdown.start_shutdown();
579
580        let mut errors = Vec::new();
581        // Check if any worker errored
582        for r in results {
583            match r {
584                Ok(_) => {}
585                Err(MonitoredWorkerError { ctx, error }) => {
586                    errors.push(MonitoredWorkerError { ctx, error })
587                }
588            }
589        }
590        if !errors.is_empty() {
591            return Err(MonitorError::ExitError(ExitError(errors)));
592        }
593        Ok(())
594    }
595
596    /// Handles all workers' events emitted
597    #[must_use]
598    pub fn on_event<F: Fn(&WorkerContext, &Event) + Send + Sync + 'static>(self, f: F) -> Self {
599        let _ = self.event_handler.write().map(|mut res| {
600            let _ = res.insert(Box::new(f));
601        });
602        self
603    }
604}
605
606impl Monitor {
607    /// Creates a new monitor instance.
608    ///
609    /// # Returns
610    ///
611    /// A new monitor instance, with an empty collection of workers.
612    #[must_use]
613    pub fn new() -> Self {
614        Self::default()
615    }
616
617    /// Sets a timeout duration for the monitor's shutdown process.
618    ///
619    /// # Arguments
620    ///
621    /// * `duration` - The timeout duration.
622    ///
623    /// # Returns
624    ///
625    /// The monitor instance, with the shutdown timeout duration set.
626    #[cfg(feature = "sleep")]
627    #[must_use]
628    pub fn shutdown_timeout(self, duration: std::time::Duration) -> Self {
629        self.with_terminator(crate::timer::sleep(duration))
630    }
631
632    /// Sets a future that will start being polled when the monitor's shutdown process starts.
633    ///
634    /// After shutdown has been initiated, the `terminator` future will be run, and if it completes
635    /// before all tasks are completed the shutdown process will complete, thus finishing the
636    /// shutdown even if there are outstanding tasks. This can be useful for using a timeout or
637    /// signal (or combination) to force a full shutdown even if one or more tasks are taking
638    /// longer than expected to finish.
639    #[must_use]
640    pub fn with_terminator(mut self, fut: impl Future<Output = ()> + Send + 'static) -> Self {
641        self.terminator = Some(fut.boxed().shared());
642        self
643    }
644
645    /// Allows controlling the restart strategy for workers
646    #[must_use]
647    pub fn should_restart<F>(self, cb: F) -> Self
648    where
649        F: Fn(&WorkerContext, &WorkerError) -> bool + Send + Sync + 'static,
650    {
651        let _ = self.should_restart.write().map(|mut res| {
652            let _ = res.insert(Box::new(cb));
653        });
654        self
655    }
656
657    /// Gets an instance of the Monitor's context
658    ///
659    /// Used to:
660    /// 1. Expose monitor to http and cli tools
661    /// 2. Shutdown the monitor and workers
662    #[must_use]
663    pub fn context(&self) -> MonitorContext {
664        MonitorContext {
665            shutdown: self.shutdown.clone(),
666            workers: self.workers.iter().map(|w| w.worker.clone()).collect(),
667        }
668    }
669}
670
671/// Error type for monitor operations.
672#[derive(Debug, thiserror::Error)]
673#[non_exhaustive]
674pub enum MonitorError {
675    /// Error occurred while running one or more workers
676    #[error("Worker errors:\n{0}")]
677    ExitError(#[from] ExitError),
678
679    /// Error occurred while waiting for shutdown signal
680    #[error("Shutdown signal error: {0}")]
681    ShutdownSignal(#[from] std::io::Error),
682}
683
684impl fmt::Debug for ExitError {
685    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
686        std::fmt::Display::fmt(&self, f)
687    }
688}
689
690/// Represents errors that occurred in a monitored worker, including its context and the error itself.
691#[derive(thiserror::Error)]
692pub struct ExitError(pub Vec<MonitoredWorkerError>);
693
694impl std::fmt::Display for ExitError {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        writeln!(f, "MonitoredErrors:")?;
697        for worker in &self.0 {
698            writeln!(f, " - Worker `{}`: {}", worker.ctx.name(), worker.error)?;
699        }
700        Ok(())
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use crate::{
708        backend::{TaskSink, dequeue::VecDequeBackend},
709        task::task_id::TaskId,
710        worker::context::WorkerContext,
711    };
712    use core::panic;
713    use std::{io, time::Duration};
714
715    use tokio::time::sleep;
716    use tower::limit::ConcurrencyLimitLayer;
717
718    use crate::{monitor::Monitor, worker::builder::WorkerBuilder};
719
720    #[tokio::test]
721    async fn basic_with_workers() {
722        let mut backend = VecDequeBackend::new();
723
724        for i in 0..10 {
725            backend.push(i).await.unwrap();
726        }
727
728        let monitor: Monitor = Monitor::new();
729        let monitor = monitor.register(move |index| {
730            WorkerBuilder::new(format!("rango-tango-{index}"))
731                .backend(backend.clone())
732                .build(move |r: u32, id: TaskId, w: WorkerContext| async move {
733                    println!("{id:?}, {}", w.name());
734                    tokio::time::sleep(Duration::from_secs(index as u64)).await;
735                    Ok::<_, io::Error>(r)
736                })
737        });
738        let m = monitor.context();
739        tokio::spawn(async move {
740            sleep(Duration::from_millis(1500)).await;
741            m.shutdown().unwrap();
742        });
743        monitor.run().await.unwrap();
744    }
745    #[tokio::test]
746    async fn test_monitor_run() {
747        let mut backend = VecDequeBackend::new();
748
749        for i in 0..10 {
750            backend.push(i).await.unwrap();
751        }
752
753        let monitor: Monitor = Monitor::new()
754            .register(move |index| {
755                WorkerBuilder::new(format!("rango-tango-{index}"))
756                    .backend(backend.clone())
757                    .layer(ConcurrencyLimitLayer::new(1))
758                    .build(move |r| async move {
759                        sleep(Duration::from_millis(1000)).await;
760                        if r % 2 == 0 {
761                            panic!("Brrr")
762                        }
763                    })
764            })
765            .should_restart(|worker, e| {
766                let restarts = worker.restarts();
767                println!(
768                    "Encountered error in {} with {e:?} for attempt {restarts}",
769                    worker.name()
770                );
771                if restarts < 3 {
772                    return true;
773                }
774                false
775            })
776            .on_event(|wrk, e| {
777                println!("{}: {e:?}", wrk.name());
778            });
779        assert_eq!(monitor.workers.len(), 1);
780        let m = monitor.context();
781
782        tokio::spawn(async move {
783            sleep(Duration::from_millis(10000)).await;
784            m.shutdown().unwrap();
785        });
786
787        let result = monitor.run().await;
788        dbg!(&result);
789        assert!(
790            result.is_err_and(|e| matches!(e, MonitorError::ExitError(_))),
791            "Monitor did not return an error as expected"
792        );
793    }
794
795    #[tokio::test]
796    async fn test_monitor_register_multiple() {
797        let mut int_backend = VecDequeBackend::new();
798
799        let mut str_backend = VecDequeBackend::new();
800
801        for i in 0..10 {
802            int_backend.push(i).await.unwrap();
803        }
804
805        for i in 0..10 {
806            str_backend.push(i.to_string()).await.unwrap();
807        }
808
809        let monitor: Monitor = Monitor::new();
810
811        assert_send_sync::<Monitor>();
812
813        let monitor = monitor.on_event(|wrk, e| {
814            println!("{:?}, {e:?}", wrk.name());
815        });
816        let monitor = monitor
817            .register(move |index| {
818                WorkerBuilder::new(format!("worker0-{index}"))
819                    .backend(int_backend.clone())
820                    .layer(ConcurrencyLimitLayer::new(1))
821                    .build(
822                        move |request: i32, id: TaskId, w: WorkerContext| async move {
823                            println!("{id:?}, {}", w.name());
824                            tokio::time::sleep(Duration::from_secs(1)).await;
825                            Ok::<_, io::Error>(request)
826                        },
827                    )
828            })
829            .register(move |index| {
830                WorkerBuilder::new(format!("worker1-{index}"))
831                    .backend(str_backend.clone())
832                    .layer(ConcurrencyLimitLayer::new(1))
833                    .build(
834                        move |request: String, id: TaskId, w: WorkerContext| async move {
835                            println!("{id:?}, {}", w.name());
836                            tokio::time::sleep(Duration::from_secs(1)).await;
837                            Ok::<_, io::Error>(request)
838                        },
839                    )
840            });
841        assert_eq!(monitor.workers.len(), 2);
842        let m = monitor.context();
843
844        tokio::spawn(async move {
845            sleep(Duration::from_millis(5000)).await;
846            m.shutdown().unwrap();
847        });
848
849        let result = monitor.run().await;
850        assert!(result.is_ok());
851    }
852
853    /// Regression: shutting down a monitor of idle workers must wake every one of them.
854    ///
855    /// The monitor polls a worker only when its waker fires, and signalling shutdown just
856    /// flips a shared flag, so unless every worker is woken explicitly the idle ones are
857    /// never re-polled and never observe it. Historically this only showed above 30
858    /// workers, because `join_all` re-polled every future while it held 30 or fewer;
859    /// `FuturesUnordered` has no such fast path, so the count here is just the old
860    /// threshold kept for reference.
861    #[tokio::test]
862    async fn shutdown_wakes_idle_workers() {
863        use crate::backend::memory::MemoryStorage;
864
865        const WORKERS: usize = 31;
866
867        let mut monitor = Monitor::new();
868        for index in 0..WORKERS {
869            monitor = monitor.register(move |_| {
870                WorkerBuilder::new(format!("idle-{index}"))
871                    // An empty `MemoryStorage` never yields a task, so the worker parks
872                    // exactly like a cron worker waiting for a distant tick.
873                    .backend(MemoryStorage::<u32>::new())
874                    .build(|_: u32| async {})
875            });
876        }
877
878        let ctx = monitor.context();
879        tokio::spawn(async move {
880            sleep(Duration::from_millis(100)).await;
881            ctx.shutdown().unwrap();
882        });
883
884        let result = tokio::time::timeout(Duration::from_secs(5), monitor.run()).await;
885
886        let exit = result.expect("monitor did not shut down idle workers");
887        exit.unwrap();
888    }
889
890    /// Shutting down many idle workers must not cut short the one worker that is busy.
891    #[tokio::test]
892    async fn signal_shutdown_drains_active_work_with_many_idle_workers() {
893        use crate::backend::memory::MemoryStorage;
894        use std::sync::atomic::{AtomicBool, Ordering};
895
896        const IDLE_WORKERS: usize = 40;
897
898        let mut busy_backend = VecDequeBackend::new();
899        busy_backend.push(1u32).await.unwrap();
900
901        let completed = Arc::new(AtomicBool::new(false));
902        let flag = completed.clone();
903
904        let mut monitor = Monitor::new().register(move |_| {
905            let flag = flag.clone();
906            WorkerBuilder::new("busy")
907                .backend(busy_backend.clone())
908                .build(move |_: u32| {
909                    let flag = flag.clone();
910                    async move {
911                        sleep(Duration::from_millis(300)).await;
912                        flag.store(true, Ordering::SeqCst);
913                    }
914                })
915        });
916
917        for index in 0..IDLE_WORKERS {
918            monitor = monitor.register(move |_| {
919                WorkerBuilder::new(format!("idle-{index}"))
920                    .backend(MemoryStorage::<u32>::new())
921                    .build(|_: u32| async {})
922            });
923        }
924
925        let signal = async {
926            sleep(Duration::from_millis(50)).await;
927            Ok(())
928        };
929
930        let result =
931            tokio::time::timeout(Duration::from_secs(5), monitor.run_with_signal(signal)).await;
932
933        let exit = result.expect("monitor did not shut down idle workers");
934        exit.unwrap();
935        assert!(
936            completed.load(Ordering::SeqCst),
937            "shutdown cut short the in-flight task"
938        );
939    }
940
941    fn assert_send_sync<T: Send + Sync>() {}
942}