Skip to main content

sui_futures/
service.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt;
5use std::panic;
6use std::time::Duration;
7
8use futures::future;
9use futures::future::BoxFuture;
10use futures::future::FutureExt;
11use tap::TapFallible;
12use tokio::signal;
13use tokio::task::JoinSet;
14use tokio::time::timeout;
15use tracing::error;
16use tracing::info;
17
18/// Default grace period for shutdown.
19///
20/// After shutdown signals are sent, tasks have this duration to complete gracefully before being
21/// forcefully aborted.
22pub const GRACE: Duration = Duration::from_secs(30);
23
24/// A collection of related tasks that succeed or fail together, consisting of:
25///
26/// - A set of primary tasks, which control the lifetime of the service in the happy path. When all
27///   primary tasks complete successfully or have been cancelled, the service will start a graceful
28///   shutdown.
29///
30/// - A set of secondary tasks, which run alongside the primary tasks, but do not extend the
31///   service's lifetime (The service will not wait for all secondary tasks to complete or be
32///   cancelled before triggering a shutdown).
33///
34/// - A set of exit signals, which are executed when the service wants to trigger graceful
35///   shutdown.
36///
37/// Any task (primary or secondary) failing by returning an error, or panicking, will also trigger
38/// a graceful shutdown.
39///
40/// If graceful shutdown takes longer than the grace period, or another task fails during shutdown,
41/// all remaining tasks are aborted and dropped immediately. Tasks are expected to clean-up after
42/// themselves when dropped (e.g. if a task has spawned its own sub-tasks, these should also be
43/// aborted when the parent task is dropped).
44#[must_use = "Dropping the service aborts all its tasks immediately"]
45#[derive(Default)]
46pub struct Service {
47    /// Futures that are run when the service is instructed to shutdown gracefully.
48    exits: Vec<BoxFuture<'static, ()>>,
49
50    /// Tasks that control the lifetime of the service in the happy path.
51    fsts: JoinSet<anyhow::Result<()>>,
52
53    /// Tasks that run alongside the primary tasks, but do not extend the service's lifetime.
54    snds: JoinSet<anyhow::Result<()>>,
55}
56
57#[derive(thiserror::Error, Debug)]
58pub enum Error {
59    #[error("Service has been terminated gracefully")]
60    Terminated,
61
62    #[error("Service has been aborted due to ungraceful shutdown")]
63    Aborted,
64
65    #[error(transparent)]
66    Task(anyhow::Error),
67}
68
69impl Service {
70    /// Create a new, empty service.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Add a primary task.
76    ///
77    /// The task will start running in the background immediately, once added. It is expected to
78    /// clean up after itself when it is dropped, which will happen when it is aborted
79    /// (non-graceful shutdown).
80    pub fn spawn(
81        mut self,
82        task: impl Future<Output = anyhow::Result<()>> + Send + 'static,
83    ) -> Self {
84        // Boxing means JoinSet's task machinery is instantiated once rather than per task type.
85        self.fsts.spawn(task.boxed());
86        self
87    }
88
89    /// Add a primary task that aborts immediately on graceful shutdown.
90    ///
91    /// This is useful for tasks that don't need a graceful shutdown.
92    pub fn spawn_aborting(
93        mut self,
94        task: impl Future<Output = anyhow::Result<()>> + Send + 'static,
95    ) -> Self {
96        let h = self.fsts.spawn(task.boxed());
97        self.with_shutdown_signal(async move { h.abort() })
98    }
99
100    /// Add a shutdown signal.
101    ///
102    /// This future will be executed when the service is instructed to shutdown gracefully, before
103    /// a grace period timer starts (in which the task receiving the shutdown signal is expected to
104    /// wind down and exit cleanly).
105    ///
106    /// Evaluation order of shutdown signals is non-determinate.
107    pub fn with_shutdown_signal(mut self, exit: impl Future<Output = ()> + Send + 'static) -> Self {
108        self.exits.push(exit.boxed());
109        self
110    }
111
112    /// Add the tasks and signals from `other` into `self`.
113    pub fn merge(mut self, mut other: Service) -> Self {
114        self.exits.extend(other.exits);
115
116        if !other.fsts.is_empty() {
117            self.fsts.spawn(async move { run(&mut other.fsts).await });
118        }
119
120        if !other.snds.is_empty() {
121            self.snds.spawn(async move { run(&mut other.snds).await });
122        }
123
124        self
125    }
126
127    /// Attach `other` to `self` as a secondary service.
128    ///
129    /// All its tasks (primary and secondary) will be treated as secondary tasks of `self`.
130    pub fn attach(mut self, mut other: Service) -> Self {
131        self.exits.extend(other.exits);
132
133        if !other.fsts.is_empty() {
134            self.snds.spawn(async move { run(&mut other.fsts).await });
135        }
136
137        if !other.snds.is_empty() {
138            self.snds.spawn(async move { run(&mut other.snds).await });
139        }
140
141        self
142    }
143
144    /// Runs the service, waiting for interrupt signals from the operating system to trigger
145    /// graceful shutdown, within the default grace period.
146    pub async fn main(self) -> Result<(), Error> {
147        self.wait_for_shutdown(GRACE, terminate).await
148    }
149
150    /// Waits for an exit condition to trigger shutdown, within `grace` period. Detects the
151    /// following exit conditions:
152    ///
153    /// - All primary tasks complete successfully or are cancelled (returns `Ok(())`).
154    /// - Any task (primary or secondary) fails or panics (returns `Err(Error::Task(_))`).
155    /// - The `terminate` future completes (returns `Err(Error::Terminated)`).
156    ///
157    /// Any tasks that do not complete within the grace period are aborted. Aborted tasks are not
158    /// joined, they are simply dropped (returns `Err(Error::Aborted)` regardless of the primary
159    /// reason for shutting down).
160    async fn wait_for_shutdown<T: Future<Output = ()>>(
161        mut self,
162        grace: Duration,
163        mut terminate: impl FnMut() -> T,
164    ) -> Result<(), Error> {
165        let exec = tokio::select! {
166            res = self.join() => {
167                res.map_err(Error::Task)
168            }
169
170            _ = terminate() => {
171                info!("Termination received");
172                Err(Error::Terminated)
173            }
174        };
175
176        info!("Shutting down gracefully...");
177        tokio::select! {
178            res = timeout(grace, self.shutdown()) => {
179                match res {
180                    Ok(Ok(())) => {},
181                    Ok(Err(_)) => return Err(Error::Aborted),
182                    Err(_) => {
183                        error!("Grace period elapsed, aborting...");
184                        return Err(Error::Aborted);
185                    }
186                }
187            }
188
189            _ = terminate() => {
190                error!("Termination received during shutdown, aborting...");
191                return Err(Error::Aborted);
192            },
193        }
194
195        exec
196    }
197
198    /// Wait until all primary tasks in the service either complete successfully or are cancelled,
199    /// or one task fails.
200    ///
201    /// This operation does not consume the service, so that it can be interacted with further in
202    /// case of an error.
203    pub async fn join(&mut self) -> anyhow::Result<()> {
204        tokio::select! {
205            res = run(&mut self.fsts) => {
206                res.tap_err(|e| error!("Primary task failure: {e:#}"))
207            },
208
209            res = run_secondary(&mut self.snds) => {
210                res.tap_err(|e| error!("Secondary task failure: {e:#}"))
211            }
212        }
213    }
214
215    /// Trigger a graceful shutdown of the service.
216    ///
217    /// Returns with an error if any of the constituent tasks produced an error during shutdown,
218    /// otherwise waits for all tasks (primary and secondy) to complete successfully.
219    pub async fn shutdown(mut self) -> Result<(), Error> {
220        for exit in self.exits {
221            exit.await;
222        }
223        if let Err(e) = future::try_join(run(&mut self.fsts), run(&mut self.snds)).await {
224            error!("Task failure during shutdown: {e:#}");
225            return Err(Error::Task(e));
226        }
227
228        Ok(())
229    }
230}
231
232// SAFETY: `Service` is not `Send` by default because `self.exits` is not `Sync`, but it is only
233// ever accessed through exclusive references (`&mut self` or `self`), so it cannot be accessed
234// through multiple threads simultaneously.
235unsafe impl Sync for Service {}
236
237impl fmt::Debug for Service {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.debug_struct("Service")
240            .field("exits", &self.exits.len())
241            .field("fsts", &self.fsts)
242            .field("snds", &self.snds)
243            .finish()
244    }
245}
246
247/// Wait until all tasks in `tasks` complete successfully or is cancelled, or any individual task
248/// fails or panics.
249async fn run(tasks: &mut JoinSet<anyhow::Result<()>>) -> anyhow::Result<()> {
250    while let Some(res) = tasks.join_next().await {
251        match res {
252            Ok(Ok(())) => continue,
253            Ok(Err(e)) => return Err(e),
254
255            Err(e) => {
256                if e.is_panic() {
257                    panic::resume_unwind(e.into_panic());
258                }
259            }
260        }
261    }
262
263    Ok(())
264}
265
266/// Like `run` but never completes successfully (only propagates errors).
267///
268/// If the secondary tasks do all complete successfully, this future holds off indefinitely, to
269/// give the primary tasks a chance to complete.
270async fn run_secondary(tasks: &mut JoinSet<anyhow::Result<()>>) -> anyhow::Result<()> {
271    run(tasks).await?;
272    std::future::pending().await
273}
274
275/// Waits for various termination signals from the operating system.
276///
277/// On unix systems, this waits for either `SIGINT` or `SIGTERM`, while on other systems it will
278/// only wait for `SIGINT`.
279pub async fn terminate() {
280    tokio::select! {
281        _ = signal::ctrl_c() => {},
282
283        _ = async {
284            #[cfg(unix)]
285            let _ = signal::unix::signal(signal::unix::SignalKind::terminate()).unwrap().recv().await;
286
287            #[cfg(not(unix))]
288            future::pending::<()>().await;
289        } => {}
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use std::sync::Arc;
296
297    use anyhow::bail;
298    use tokio::sync::Notify;
299    use tokio::sync::oneshot;
300
301    use super::*;
302
303    #[tokio::test]
304    async fn test_empty() {
305        // The empty service should exit immediately.
306        Service::new()
307            .wait_for_shutdown(GRACE, std::future::pending)
308            .await
309            .unwrap();
310    }
311
312    #[tokio::test]
313    async fn test_empty_attach_merge() {
314        // Attaching and merging empty services should work without issue.
315        Service::new()
316            .attach(Service::new())
317            .merge(Service::new())
318            .wait_for_shutdown(GRACE, std::future::pending)
319            .await
320            .unwrap();
321    }
322
323    #[tokio::test]
324    async fn test_completion() {
325        let (atx, arx) = oneshot::channel::<()>();
326        let (btx, brx) = oneshot::channel::<()>();
327
328        let svc = Service::new().spawn(async move {
329            let _brx = brx;
330            Ok(arx.await?)
331        });
332
333        // The task has not finished yet (dropping the receiver)
334        assert!(!btx.is_closed());
335
336        // Sending the signal allows the task to complete successfully, which allows the service to
337        // exit, and at that point, the second channel should be closed too.
338        atx.send(()).unwrap();
339        svc.wait_for_shutdown(GRACE, std::future::pending)
340            .await
341            .unwrap();
342        assert!(btx.is_closed());
343    }
344
345    #[tokio::test]
346    async fn test_failure() {
347        let svc = Service::new().spawn(async move { bail!("boom") });
348        let res = svc.wait_for_shutdown(GRACE, std::future::pending).await;
349        assert!(matches!(res, Err(Error::Task(_))));
350    }
351
352    #[tokio::test]
353    #[should_panic]
354    async fn test_panic() {
355        let svc = Service::new().spawn(async move { panic!("boom") });
356        let _ = svc.wait_for_shutdown(GRACE, std::future::pending).await;
357    }
358
359    #[tokio::test]
360    async fn test_graceful_shutdown() {
361        let (atx, arx) = oneshot::channel::<()>();
362        let (btx, brx) = oneshot::channel::<()>();
363
364        let srx = Arc::new(Notify::new());
365        let stx = srx.clone();
366
367        let svc = Service::new()
368            .with_shutdown_signal(async move { atx.send(()).unwrap() })
369            .spawn(async move {
370                arx.await?;
371                btx.send(()).unwrap();
372                Ok(())
373            });
374
375        // The service is now running in the background.
376        let handle =
377            tokio::spawn(svc.wait_for_shutdown(GRACE, move || srx.clone().notified_owned()));
378
379        // Send the shutdown signal, and confirm the task went through its graceful shutdwon
380        // process.
381        stx.notify_one();
382        brx.await.unwrap();
383
384        // The service should exit gracefully now, dropping the receiver it was holding.
385        let res = handle.await.unwrap();
386        assert!(matches!(res, Err(Error::Terminated)));
387    }
388
389    #[tokio::test]
390    async fn test_multiple_tasks() {
391        let (atx, arx) = oneshot::channel::<()>();
392        let (btx, brx) = oneshot::channel::<()>();
393        let (ctx, crx) = oneshot::channel::<()>();
394
395        // Three different tasks each waiting on a oneshot channel. We should be able to unblock
396        // each of them before the service exits.
397        let svc = Service::new()
398            .spawn(async move { Ok(arx.await?) })
399            .spawn(async move { Ok(brx.await?) })
400            .spawn(async move { Ok(crx.await?) });
401
402        let handle = tokio::spawn(svc.wait_for_shutdown(GRACE, std::future::pending));
403
404        atx.send(()).unwrap();
405        tokio::task::yield_now().await;
406
407        btx.send(()).unwrap();
408        tokio::task::yield_now().await;
409
410        ctx.send(()).unwrap();
411        handle.await.unwrap().unwrap();
412    }
413
414    #[tokio::test]
415    async fn test_multiple_task_failure() {
416        let (atx, arx) = oneshot::channel::<()>();
417
418        // The task waiting on the channel (that aborts on shutdown) will never get to finish because
419        // the other task errors out.
420        let svc = Service::new()
421            .spawn_aborting(async move { Ok(arx.await?) })
422            .spawn(async move { bail!("boom") });
423
424        let handle = tokio::spawn(svc.wait_for_shutdown(GRACE, std::future::pending));
425        let res = handle.await.unwrap();
426
427        // The service exits with a task error because one of the tasks failed, and this also
428        // means the other task is aborted before it can complete successfully.
429        assert!(matches!(res, Err(Error::Task(_))));
430        assert!(atx.is_closed());
431    }
432
433    #[tokio::test]
434    async fn test_secondary_stuck() {
435        let (atx, arx) = oneshot::channel::<()>();
436        let (btx, brx) = oneshot::channel::<()>();
437
438        // A secondary task and a primary task.
439        let snd = Service::new().spawn_aborting(async move { Ok(brx.await?) });
440        let svc = Service::new()
441            .spawn(async move { Ok(arx.await?) })
442            .attach(snd);
443
444        let handle = tokio::spawn(svc.wait_for_shutdown(GRACE, std::future::pending));
445
446        // Complete the primary task, and the service as a whole should wrap up.
447        atx.send(()).unwrap();
448        handle.await.unwrap().unwrap();
449        assert!(btx.is_closed());
450    }
451
452    #[tokio::test]
453    async fn test_secondary_complete() {
454        let (atx, arx) = oneshot::channel::<()>();
455        let (btx, brx) = oneshot::channel::<()>();
456        let (mut ctx, crx) = oneshot::channel::<()>();
457
458        // A secondary task and a primary task.
459        let snd = Service::new().spawn(async move {
460            let _crx = crx;
461            brx.await?;
462            Ok(())
463        });
464
465        let svc = Service::new()
466            .spawn(async move { Ok(arx.await?) })
467            .attach(snd);
468
469        let handle = tokio::spawn(svc.wait_for_shutdown(GRACE, std::future::pending));
470
471        // Complete the secondary task, and wait for it to complete (dropping the other channel).
472        btx.send(()).unwrap();
473        ctx.closed().await;
474        tokio::task::yield_now().await;
475
476        // The primary task will not have been cleaned up, so we can send to it, completing that
477        // task, and allowing the service as a whole to complete.
478        atx.send(()).unwrap();
479        handle.await.unwrap().unwrap();
480    }
481
482    #[tokio::test]
483    async fn test_secondary_failure() {
484        let (atx, arx) = oneshot::channel::<()>();
485
486        // A secondary task that fails, with a primary task.
487        let snd = Service::new().spawn(async move { bail!("boom") });
488        let svc = Service::new()
489            .spawn_aborting(async move { Ok(arx.await?) })
490            .attach(snd);
491
492        // Run the service -- it should fail immediately because the secondary task failed,
493        // cleaning up the primary task.
494        let res = svc.wait_for_shutdown(GRACE, std::future::pending).await;
495        assert!(matches!(res, Err(Error::Task(_))));
496        assert!(atx.is_closed());
497    }
498
499    #[tokio::test]
500    #[should_panic]
501    async fn test_secondary_panic() {
502        let (_atx, arx) = oneshot::channel::<()>();
503
504        // A secondary task that fails, with a primary task.
505        let snd = Service::new().spawn(async move { panic!("boom") });
506        let svc = Service::new()
507            .spawn_aborting(async move { Ok(arx.await?) })
508            .attach(snd);
509
510        // When the service runs, the panic from the secondary task will be propagated.
511        let _ = svc.wait_for_shutdown(GRACE, std::future::pending).await;
512    }
513
514    #[tokio::test]
515    async fn test_secondary_graceful_shutdown() {
516        let (atx, arx) = oneshot::channel::<()>();
517        let (btx, brx) = oneshot::channel::<()>();
518        let (ctx, crx) = oneshot::channel::<()>();
519
520        let srx = Arc::new(Notify::new());
521        let stx = srx.clone();
522
523        // A secondary task with a shutdown signal.
524        let snd = Service::new()
525            .with_shutdown_signal(async move { atx.send(()).unwrap() })
526            .spawn(async move {
527                let _crx = crx;
528                Ok(arx.await?)
529            });
530
531        // A primary task which aborts when signalled to shutdown.
532        let svc = Service::new()
533            .spawn_aborting(async move { Ok(brx.await?) })
534            .attach(snd);
535
536        // The service is now running in the background.
537        let handle =
538            tokio::spawn(svc.wait_for_shutdown(GRACE, move || srx.clone().notified_owned()));
539
540        // Confirm that each task is still waiting on their respective channels.
541        assert!(!btx.is_closed());
542        assert!(!ctx.is_closed());
543
544        // Send the shutdown signal - both tasks should be unblocked and complete gracefully.
545        stx.notify_one();
546        let res = handle.await.unwrap();
547        assert!(matches!(res, Err(Error::Terminated)));
548        assert!(btx.is_closed());
549        assert!(ctx.is_closed());
550    }
551
552    #[tokio::test]
553    async fn test_merge() {
554        let (atx, arx) = oneshot::channel::<()>();
555        let (btx, brx) = oneshot::channel::<()>();
556        let (ctx, crx) = oneshot::channel::<()>();
557        let (dtx, drx) = oneshot::channel::<()>();
558        let (etx, erx) = oneshot::channel::<()>();
559        let (ftx, frx) = oneshot::channel::<()>();
560
561        let srx = Arc::new(Notify::new());
562        let stx = srx.clone();
563
564        // Set-up two services -- each with a task that can be shutdown, and a task that's paused,
565        // which will send a message once unpaused.
566        let a = Service::new()
567            .spawn(async move { Ok(arx.await?) })
568            .with_shutdown_signal(async move { ctx.send(()).unwrap() })
569            .spawn(async move {
570                crx.await?;
571                dtx.send(()).unwrap();
572                Ok(())
573            });
574
575        let b = Service::new()
576            .spawn(async move { Ok(brx.await?) })
577            .with_shutdown_signal(async move { etx.send(()).unwrap() })
578            .spawn(async move {
579                erx.await?;
580                ftx.send(()).unwrap();
581                Ok(())
582            });
583
584        // Merge them into a larger service and run it.
585        let svc = Service::new().merge(a).merge(b);
586        let handle =
587            tokio::spawn(svc.wait_for_shutdown(GRACE, move || srx.clone().notified_owned()));
588
589        // Unblock the paused tasks, so they terminate.
590        atx.send(()).unwrap();
591        tokio::task::yield_now().await;
592
593        btx.send(()).unwrap();
594        tokio::task::yield_now().await;
595
596        // Send the shutdown signal - triggering graceful shutdown on the remaining tasks --
597        // confirm that those tasks actually go through the graceful shutdown process.
598        stx.notify_one();
599        drx.await.unwrap();
600        frx.await.unwrap();
601
602        // ...and then the service shuts down.
603        let res = handle.await.unwrap();
604        assert!(matches!(res, Err(Error::Terminated)));
605    }
606
607    #[tokio::test]
608    async fn test_drop_abort() {
609        let (mut atx, arx) = oneshot::channel::<()>();
610        let (mut btx, brx) = oneshot::channel::<()>();
611
612        let svc = Service::new()
613            .spawn(async move { Ok(arx.await?) })
614            .spawn_aborting(async move { Ok(brx.await?) });
615
616        assert!(!atx.is_closed());
617        assert!(!btx.is_closed());
618
619        // Once the service is dropped, its tasks will also be dropped, and the receivers will be
620        // dropped, closing the senders.
621        drop(svc);
622        atx.closed().await;
623        btx.closed().await;
624    }
625
626    #[tokio::test]
627    async fn test_shutdown() {
628        let (atx, arx) = oneshot::channel::<()>();
629        let (btx, brx) = oneshot::channel::<()>();
630
631        let svc = Service::new()
632            .with_shutdown_signal(async move { atx.send(()).unwrap() })
633            .spawn(async move { Ok(arx.await?) })
634            .spawn_aborting(async move { Ok(brx.await?) });
635
636        // We don't need to call `Service::run` to kick off the service's tasks -- they are now
637        // running in the background. We can call `shutdown` to trigger a graceful shutdown, which
638        // should exit cleanly and clean up any unused resources.
639        svc.shutdown().await.unwrap();
640        assert!(btx.is_closed());
641    }
642
643    #[tokio::test]
644    async fn test_error_cascade() {
645        let (atx, arx) = oneshot::channel::<()>();
646
647        // The first task errors immediately, and the second task errors during graceful shutdown.
648        let svc = Service::new()
649            .spawn(async move { bail!("boom") })
650            .with_shutdown_signal(async move { atx.send(()).unwrap() })
651            .spawn(async move {
652                arx.await?;
653                bail!("boom, again")
654            });
655
656        // The service will exit with an abort.
657        let res = svc.wait_for_shutdown(GRACE, std::future::pending).await;
658        assert!(matches!(res, Err(Error::Aborted)));
659    }
660
661    #[tokio::test]
662    async fn test_multiple_errors() {
663        // Both tasks produce an error, one will be picked up during normal execution, and the
664        // other will be picked up during shutdown, resulting in an ungraceful shutdown (abort).
665        let svc = Service::new()
666            .spawn(async move { bail!("boom") })
667            .spawn(async move { bail!("boom, again") });
668
669        // The service will exit with an abort.
670        let res = svc.wait_for_shutdown(GRACE, std::future::pending).await;
671        assert!(matches!(res, Err(Error::Aborted)));
672    }
673
674    #[tokio::test]
675    async fn test_termination_cascade() {
676        // A service with a task that ignores graceful shutdown.
677        let svc = Service::new().spawn(std::future::pending());
678
679        let srx = Arc::new(Notify::new());
680        let stx = srx.clone();
681
682        // The service is now running in the background.
683        let handle =
684            tokio::spawn(svc.wait_for_shutdown(GRACE, move || srx.clone().notified_owned()));
685
686        // Trigger the first termination, which the task will ignore.
687        stx.notify_one();
688        tokio::task::yield_now().await;
689
690        // Trigger the second termination, the service takes over.
691        stx.notify_one();
692        tokio::task::yield_now().await;
693
694        let res = handle.await.unwrap();
695        assert!(matches!(res, Err(Error::Aborted)));
696    }
697
698    #[tokio::test]
699    #[should_panic]
700    async fn test_panic_during_shutdown() {
701        let (atx, arx) = oneshot::channel::<()>();
702
703        let srx = Arc::new(Notify::new());
704        let stx = srx.clone();
705
706        let svc = Service::new()
707            .with_shutdown_signal(async move { atx.send(()).unwrap() })
708            .spawn(async move {
709                arx.await?;
710                panic!("boom")
711            });
712
713        // The service is now running in the background.
714        let handle =
715            tokio::spawn(svc.wait_for_shutdown(GRACE, move || srx.clone().notified_owned()));
716
717        // Send the shutdown signal, the panic gets resumed when the service is awaited.
718        stx.notify_one();
719        let _ = handle.await.unwrap();
720    }
721
722    #[tokio::test(start_paused = true)]
723    async fn test_graceful_shutdown_timeout() {
724        let srx = Arc::new(Notify::new());
725        let stx = srx.clone();
726
727        // A service with a task that ignores graceful shutdown.
728        let svc = Service::new().spawn(std::future::pending());
729
730        let handle =
731            tokio::spawn(svc.wait_for_shutdown(GRACE, move || srx.clone().notified_owned()));
732
733        // Trigger cancellation and then let twice the grace period pass, which should be treated
734        // as an abort.
735        stx.notify_one();
736        tokio::time::advance(GRACE * 2).await;
737
738        let res = handle.await.unwrap();
739        assert!(matches!(res, Err(Error::Aborted)));
740    }
741}