Skip to main content

saluki_core/runtime/
supervisable.rs

1//! The contract that a supervised process implements.
2//!
3//! A supervisor drives a [`Supervisable`]. The [`Supervisable`] names the process, tells how to shut it down, and
4//! builds the future of the process each time the process starts. This module also contains the types in its signature.
5
6use std::{future::Future, pin::Pin, time::Duration};
7
8use async_trait::async_trait;
9use saluki_common::sync::shutdown::ShutdownHandle;
10use saluki_error::GenericError;
11use snafu::Snafu;
12
13/// A `Future` that represents the execution of a supervised process.
14pub type SupervisorFuture = Pin<Box<dyn Future<Output = Result<(), GenericError>> + Send>>;
15
16/// Initialization errors.
17///
18/// Initialization errors are distinct from runtime errors: they indicate that a process couldn't be started at all
19/// (for example, failed to bind a port, missing configuration). These errors don't trigger restart logic; instead, they
20/// immediately propagate up and fail the supervisor.
21#[derive(Debug, Snafu)]
22#[snafu(context(suffix(false)))]
23pub enum InitializationError {
24    /// The process couldn't be initialized due to an error.
25    #[snafu(display("Process failed to initialize: {}", source))]
26    Failed {
27        /// The underlying error that caused initialization to fail.
28        source: GenericError,
29    },
30}
31
32impl From<GenericError> for InitializationError {
33    fn from(source: GenericError) -> Self {
34        Self::Failed { source }
35    }
36}
37
38/// Strategy for shutting down a process.
39#[derive(Clone, Copy, Debug)]
40pub enum ShutdownStrategy {
41    /// Signals the process, then halts it if it hasn't exited within the given duration.
42    ///
43    /// The duration also covers the children in the process's [scope][crate::runtime::scope], which are halted along
44    /// with it. A halt is reported as an unclean shutdown.
45    Graceful(Duration),
46
47    /// Halts the process and everything it owns immediately, without signalling it.
48    ///
49    /// For work with no terminal condition. Not reported as an unclean shutdown.
50    Brutal,
51}
52
53/// A supervisable process.
54#[async_trait]
55pub trait Supervisable: Send + Sync {
56    /// Returns the name of the process.
57    fn name(&self) -> &str;
58
59    /// Returns the shutdown strategy for the process.
60    fn shutdown_strategy(&self) -> ShutdownStrategy {
61        ShutdownStrategy::Graceful(Duration::from_secs(5))
62    }
63
64    /// Returns whether this process observes the shutdown signal it is given.
65    ///
66    /// The shutdown of a subtree is a _trigger_, not an enforcement. Many workers ignore the signal completely, and
67    /// stop only when they reach their own terminal condition, such as when an input channel closes. If this function
68    /// returns `false`, the supervisor gives the process a [`ShutdownHandle::noop`] instead. But the
69    /// [scope][crate::runtime::scope] of the process still observes the signal for the process, so that it can tell
70    /// whether an exit of one of its children was requested.
71    ///
72    /// It also decides whether a nested supervisor in that scope that
73    /// [stops with its owner][crate::runtime::NestedSupervisorBuilder::stops_with_owner] is told to stop together with
74    /// the process. If this function returns `false`, the nested supervisor stops only when the scope closes.
75    ///
76    /// This says nothing about _whether_ the supervisor waits for the process -- that's
77    /// [`shutdown_strategy`][Self::shutdown_strategy]. A process that ignores the signal is still waited for, up to its
78    /// shutdown timeout.
79    ///
80    /// Defaults to `true`.
81    fn wants_shutdown_signal(&self) -> bool {
82        true
83    }
84
85    /// Returns whether the process can be started again after it has started once.
86    ///
87    /// [`initialize`][Self::initialize] builds the future of the process each time the process starts. Most processes
88    /// can build it again, so their owner can restart them. A process that consumes something when it starts cannot
89    /// build its future a second time. Examples are a connection, or a value that the process received before it ran.
90    /// Such a process returns `false` here.
91    ///
92    /// The owner of a process that returns `false` never restarts it, whatever restart policy the process has. When the
93    /// process stops, its owner reaps it, or fails if it needs the process. See the
94    /// [runtime documentation][crate::runtime#restart-policy] for how this decides the restart policy.
95    ///
96    /// Defaults to `true`.
97    fn can_restart(&self) -> bool {
98        true
99    }
100
101    /// Initializes the process asynchronously.
102    ///
103    /// During initialization, any resources or configuration for the process can be created asynchronously, and the
104    /// same runtime that's used for running the process is used for initialization. The resulting future is expected to
105    /// complete as soon as reasonably possible after `shutdown` resolves.
106    ///
107    /// **Important:** The `process_shutdown` signal must be moved into the returned [`SupervisorFuture`] so the worker
108    /// can respond to supervisor-initiated shutdown. If `process_shutdown` is dropped during initialization, the worker
109    /// will be unable to shut down gracefully and will be halted after the shutdown timeout.
110    ///
111    /// # Errors
112    ///
113    /// If the process can't be initialized, an error is returned.
114    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError>;
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    /// The smallest possible implementation, existing only to be erased below.
122    struct Noop;
123
124    #[async_trait]
125    impl Supervisable for Noop {
126        fn name(&self) -> &str {
127            "noop"
128        }
129
130        async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
131            Ok(Box::pin(async move {
132                process_shutdown.await;
133                Ok(())
134            }))
135        }
136    }
137
138    #[test]
139    fn trait_is_object_safe() {
140        // Producers hand their background work over as `Vec<Box<dyn Supervisable>>`, so object safety is load-bearing
141        // here rather than incidental: a default method taking `self` by value, or a generic one, would break every
142        // one of those call sites.
143        let worker: Box<dyn Supervisable> = Box::new(Noop);
144
145        assert_eq!(worker.name(), "noop");
146        assert!(worker.wants_shutdown_signal());
147        assert!(worker.can_restart());
148        assert!(matches!(
149            worker.shutdown_strategy(),
150            ShutdownStrategy::Graceful(timeout) if timeout == Duration::from_secs(5)
151        ));
152    }
153}