Skip to main content

rust_zero_core/
service.rs

1use std::{error::Error, fmt, future::Future, io, pin::Pin, time::Duration};
2
3use tokio::{
4    sync::watch,
5    task::{JoinError, JoinSet},
6};
7
8type BoxError = Box<dyn Error + Send + Sync>;
9type ServiceFuture = Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>;
10type ServiceStarter = Box<dyn FnOnce(Shutdown) -> ServiceFuture + Send>;
11
12/// A cancellation signal passed to every service in a [`ServiceGroup`].
13#[derive(Clone)]
14pub struct Shutdown {
15    receiver: watch::Receiver<bool>,
16}
17
18impl Shutdown {
19    pub fn is_requested(&self) -> bool {
20        *self.receiver.borrow()
21    }
22
23    pub async fn requested(&mut self) {
24        if self.is_requested() {
25            return;
26        }
27        while self.receiver.changed().await.is_ok() {
28            if self.is_requested() {
29                return;
30            }
31        }
32    }
33}
34
35/// A clonable handle that requests graceful shutdown of a running service group.
36#[derive(Clone)]
37pub struct ShutdownHandle {
38    sender: watch::Sender<bool>,
39}
40
41impl ShutdownHandle {
42    pub fn shutdown(&self) {
43        let _ = self.sender.send(true);
44    }
45
46    pub fn is_shutdown(&self) -> bool {
47        *self.sender.borrow()
48    }
49}
50
51/// Starts and supervises a set of long-running asynchronous services.
52pub struct ServiceGroup {
53    services: Vec<(String, ServiceStarter)>,
54    shutdown_timeout: Duration,
55}
56
57impl Default for ServiceGroup {
58    fn default() -> Self {
59        Self {
60            services: Vec::new(),
61            shutdown_timeout: Duration::from_secs(30),
62        }
63    }
64}
65
66impl ServiceGroup {
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
72        assert!(
73            !timeout.is_zero(),
74            "shutdown timeout must be greater than zero"
75        );
76        self.shutdown_timeout = timeout;
77        self
78    }
79
80    pub fn add<F, Fut, E>(&mut self, name: impl Into<String>, service: F)
81    where
82        F: FnOnce(Shutdown) -> Fut + Send + 'static,
83        Fut: Future<Output = Result<(), E>> + Send + 'static,
84        E: Error + Send + Sync + 'static,
85    {
86        let name = name.into();
87        assert!(!name.trim().is_empty(), "service name cannot be empty");
88        self.services.push((
89            name,
90            Box::new(move |shutdown| {
91                Box::pin(async move {
92                    service(shutdown)
93                        .await
94                        .map_err(|error| Box::new(error) as BoxError)
95                })
96            }),
97        ));
98    }
99
100    pub fn start(self) -> Result<RunningServices, ServiceGroupError> {
101        if self.services.is_empty() {
102            return Err(ServiceGroupError::Empty);
103        }
104
105        let (sender, receiver) = watch::channel(false);
106        let mut tasks = JoinSet::new();
107        let service_count = self.services.len();
108        for (name, start) in self.services {
109            let shutdown = Shutdown {
110                receiver: receiver.clone(),
111            };
112            tasks.spawn(async move {
113                let result = start(shutdown).await;
114                (name, result)
115            });
116        }
117
118        Ok(RunningServices {
119            handle: ShutdownHandle { sender },
120            receiver,
121            tasks,
122            service_count,
123            shutdown_timeout: self.shutdown_timeout,
124        })
125    }
126}
127
128pub struct RunningServices {
129    handle: ShutdownHandle,
130    receiver: watch::Receiver<bool>,
131    tasks: JoinSet<(String, Result<(), BoxError>)>,
132    service_count: usize,
133    shutdown_timeout: Duration,
134}
135
136impl RunningServices {
137    pub fn shutdown_handle(&self) -> ShutdownHandle {
138        self.handle.clone()
139    }
140
141    /// Waits until shutdown is requested or one service exits unexpectedly.
142    ///
143    /// If one service exits first, all siblings are asked to stop before the error is returned.
144    pub async fn wait(mut self) -> Result<(), ServiceGroupError> {
145        if self.handle.is_shutdown() {
146            return self.drain(None).await;
147        }
148
149        tokio::select! {
150            changed = self.receiver.changed() => {
151                if changed.is_err() {
152                    return Err(ServiceGroupError::SignalClosed);
153                }
154                self.drain(None).await
155            }
156            result = self.tasks.join_next() => {
157                let failure = match result {
158                    Some(Ok((name, Ok(())))) => ServiceGroupError::UnexpectedExit(name),
159                    Some(Ok((name, Err(error)))) => ServiceGroupError::ServiceFailed {
160                        name,
161                        message: error.to_string(),
162                    },
163                    Some(Err(error)) => join_error(error),
164                    None => return Ok(()),
165                };
166                self.handle.shutdown();
167                self.service_count = self.service_count.saturating_sub(1);
168                self.drain(Some(failure)).await
169            }
170        }
171    }
172
173    /// Waits until SIGINT/SIGTERM requests shutdown or one service exits unexpectedly.
174    ///
175    /// Receiving a process signal asks every service to stop and then applies the group's
176    /// configured shutdown timeout while draining their tasks. On platforms without Unix
177    /// signals, Ctrl-C is used as the shutdown signal.
178    pub async fn wait_for_signal(self) -> Result<(), ServiceGroupError> {
179        self.wait_for_signal_from(wait_for_shutdown_signal()).await
180    }
181
182    async fn wait_for_signal_from<F>(self, signal: F) -> Result<(), ServiceGroupError>
183    where
184        F: Future<Output = io::Result<ShutdownSignal>>,
185    {
186        let handle = self.shutdown_handle();
187        let wait = self.wait();
188        tokio::pin!(signal);
189        tokio::pin!(wait);
190
191        tokio::select! {
192            result = &mut wait => result,
193            received = &mut signal => {
194                handle.shutdown();
195                match received {
196                    Ok(_) => wait.await,
197                    Err(error) => {
198                        let _ = wait.await;
199                        Err(ServiceGroupError::Signal(error.to_string()))
200                    }
201                }
202            }
203        }
204    }
205
206    async fn drain(
207        &mut self,
208        initial_error: Option<ServiceGroupError>,
209    ) -> Result<(), ServiceGroupError> {
210        let mut first_error = initial_error;
211        let drain = async {
212            while let Some(result) = self.tasks.join_next().await {
213                self.service_count = self.service_count.saturating_sub(1);
214                match result {
215                    Ok((_name, Ok(()))) => {}
216                    Ok((name, Err(error))) if first_error.is_none() => {
217                        first_error = Some(ServiceGroupError::ServiceFailed {
218                            name,
219                            message: error.to_string(),
220                        });
221                    }
222                    Err(error) if first_error.is_none() => {
223                        first_error = Some(join_error(error));
224                    }
225                    _ => {}
226                }
227            }
228        };
229
230        if tokio::time::timeout(self.shutdown_timeout, drain)
231            .await
232            .is_err()
233        {
234            self.tasks.abort_all();
235            return Err(ServiceGroupError::ShutdownTimeout {
236                remaining: self.service_count,
237            });
238        }
239
240        first_error.map_or(Ok(()), Err)
241    }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum ServiceGroupError {
246    Empty,
247    UnexpectedExit(String),
248    ServiceFailed { name: String, message: String },
249    ServicePanicked(String),
250    ShutdownTimeout { remaining: usize },
251    SignalClosed,
252    Signal(String),
253}
254
255impl fmt::Display for ServiceGroupError {
256    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::Empty => formatter.write_str("service group cannot be empty"),
259            Self::UnexpectedExit(name) => {
260                write!(formatter, "service {name} exited before shutdown")
261            }
262            Self::ServiceFailed { name, message } => {
263                write!(formatter, "service {name} failed: {message}")
264            }
265            Self::ServicePanicked(message) => write!(formatter, "service task panicked: {message}"),
266            Self::ShutdownTimeout { remaining } => write!(
267                formatter,
268                "service shutdown timed out with {remaining} task(s) still running"
269            ),
270            Self::SignalClosed => formatter.write_str("service shutdown signal closed"),
271            Self::Signal(message) => {
272                write!(
273                    formatter,
274                    "failed to listen for a process shutdown signal: {message}"
275                )
276            }
277        }
278    }
279}
280
281impl Error for ServiceGroupError {}
282
283fn join_error(error: JoinError) -> ServiceGroupError {
284    ServiceGroupError::ServicePanicked(error.to_string())
285}
286
287/// A process signal that conventionally requests graceful shutdown.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum ShutdownSignal {
290    Interrupt,
291    Terminate,
292}
293
294/// Waits for SIGINT or SIGTERM without terminating the process immediately.
295///
296/// Callers that do not use [`ServiceGroup`] can use this function to connect process lifecycle
297/// events to their own cancellation mechanism.
298#[cfg(unix)]
299pub async fn wait_for_shutdown_signal() -> io::Result<ShutdownSignal> {
300    use tokio::signal::unix::{signal, SignalKind};
301
302    let mut interrupt = signal(SignalKind::interrupt())?;
303    let mut terminate = signal(SignalKind::terminate())?;
304    tokio::select! {
305        _ = interrupt.recv() => Ok(ShutdownSignal::Interrupt),
306        _ = terminate.recv() => Ok(ShutdownSignal::Terminate),
307    }
308}
309
310#[cfg(not(unix))]
311pub async fn wait_for_shutdown_signal() -> io::Result<ShutdownSignal> {
312    tokio::signal::ctrl_c().await?;
313    Ok(ShutdownSignal::Interrupt)
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use std::{io, sync::Arc};
320    use tokio::sync::Notify;
321
322    #[tokio::test]
323    async fn gracefully_stops_all_services() {
324        let stopped = Arc::new(Notify::new());
325        let mut group = ServiceGroup::new().with_shutdown_timeout(Duration::from_secs(1));
326        group.add("http", {
327            let stopped = Arc::clone(&stopped);
328            move |mut shutdown| async move {
329                shutdown.requested().await;
330                stopped.notify_one();
331                Ok::<_, io::Error>(())
332            }
333        });
334
335        let running = group.start().unwrap();
336        let handle = running.shutdown_handle();
337        handle.shutdown();
338        running.wait().await.unwrap();
339        stopped.notified().await;
340    }
341
342    #[tokio::test]
343    async fn one_failure_stops_sibling_services() {
344        let sibling_stopped = Arc::new(Notify::new());
345        let mut group = ServiceGroup::new().with_shutdown_timeout(Duration::from_secs(1));
346        group.add("failing", |_| async {
347            Err::<(), _>(io::Error::other("database unavailable"))
348        });
349        group.add("sibling", {
350            let sibling_stopped = Arc::clone(&sibling_stopped);
351            move |mut shutdown| async move {
352                shutdown.requested().await;
353                sibling_stopped.notify_one();
354                Ok::<_, io::Error>(())
355            }
356        });
357
358        let error = group.start().unwrap().wait().await.unwrap_err();
359        assert_eq!(
360            error,
361            ServiceGroupError::ServiceFailed {
362                name: "failing".to_owned(),
363                message: "database unavailable".to_owned(),
364            }
365        );
366        sibling_stopped.notified().await;
367    }
368
369    #[tokio::test]
370    async fn process_signal_gracefully_stops_all_services() {
371        let stopped = Arc::new(Notify::new());
372        let mut group = ServiceGroup::new().with_shutdown_timeout(Duration::from_secs(1));
373        group.add("worker", {
374            let stopped = Arc::clone(&stopped);
375            move |mut shutdown| async move {
376                shutdown.requested().await;
377                stopped.notify_one();
378                Ok::<_, io::Error>(())
379            }
380        });
381
382        group
383            .start()
384            .unwrap()
385            .wait_for_signal_from(async { Ok(ShutdownSignal::Terminate) })
386            .await
387            .unwrap();
388        stopped.notified().await;
389    }
390
391    #[tokio::test]
392    async fn signal_registration_failure_still_stops_services() {
393        let stopped = Arc::new(Notify::new());
394        let mut group = ServiceGroup::new().with_shutdown_timeout(Duration::from_secs(1));
395        group.add("worker", {
396            let stopped = Arc::clone(&stopped);
397            move |mut shutdown| async move {
398                shutdown.requested().await;
399                stopped.notify_one();
400                Ok::<_, io::Error>(())
401            }
402        });
403
404        let error = group
405            .start()
406            .unwrap()
407            .wait_for_signal_from(async { Err(io::Error::other("signals unavailable")) })
408            .await
409            .unwrap_err();
410        assert_eq!(
411            error,
412            ServiceGroupError::Signal("signals unavailable".to_owned())
413        );
414        stopped.notified().await;
415    }
416
417    #[test]
418    fn rejects_empty_groups() {
419        assert!(matches!(
420            ServiceGroup::new().start(),
421            Err(ServiceGroupError::Empty)
422        ));
423    }
424}