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}