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#[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#[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
51pub 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum ShutdownSignal {
290 Interrupt,
291 Terminate,
292}
293
294#[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}