Skip to main content

sova_core/
service.rs

1//! Process-local background services (UDP listeners, task workers, …).
2
3use crate::handler::BoxFuture;
4use crate::state::StateMap;
5use std::sync::Arc;
6use tokio::sync::watch;
7
8/// Unified shutdown signal for [`BackgroundService`]s.
9///
10/// This is a thin wrapper around an internal `tokio::sync::watch` receiver,
11/// but the tokio type does not leak into the plugin contract.
12#[derive(Clone, Debug)]
13pub struct Shutdown {
14    inner: watch::Receiver<bool>,
15}
16
17impl Shutdown {
18    pub(crate) fn new(inner: watch::Receiver<bool>) -> Self {
19        Self { inner }
20    }
21
22    /// Whether shutdown was already triggered.
23    pub fn is_triggered(&self) -> bool {
24        *self.inner.borrow()
25    }
26
27    /// Wait until shutdown is triggered.
28    pub async fn recv(&mut self) {
29        loop {
30            if *self.inner.borrow() {
31                return;
32            }
33            if self.inner.changed().await.is_err() {
34                return;
35            }
36        }
37    }
38}
39
40/// Test-only helper for triggering [`Shutdown`] signals without exposing tokio types.
41#[cfg(any(test, feature = "testing"))]
42#[derive(Clone, Debug)]
43pub struct ShutdownSender(watch::Sender<bool>);
44
45#[cfg(any(test, feature = "testing"))]
46#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
47#[allow(dead_code)]
48#[must_use]
49pub fn shutdown_channel() -> (ShutdownSender, Shutdown) {
50    let (tx, rx) = watch::channel(false);
51    (ShutdownSender(tx), Shutdown::new(rx))
52}
53
54#[cfg(any(test, feature = "testing"))]
55impl ShutdownSender {
56    pub fn send(&self, value: bool) -> bool {
57        self.0.send(value).is_ok()
58    }
59}
60
61/// Long-running work started after `on_startup`, stopped after connection drain.
62///
63/// Services are **process-local** — they are not shared across processes.
64/// Prefer one service per concern (UDP socket, queue worker, …).
65pub trait BackgroundService: Send {
66    fn name(&self) -> &str;
67
68    /// Run until `shutdown` becomes `true` (or the future completes).
69    fn run(
70        self: Box<Self>,
71        state: Arc<StateMap>,
72        shutdown: Shutdown,
73    ) -> BoxFuture<()>;
74}
75
76/// Type-erased service stored on [`crate::App`].
77pub(crate) type BoxedService = Box<dyn BackgroundService>;
78
79/// Wait until shutdown is triggered.
80pub async fn wait_shutdown(mut shutdown: Shutdown) {
81    shutdown.recv().await
82}