Skip to main content

diode_base/
daemon.rs

1use std::any::{TypeId, type_name};
2use std::collections::HashSet;
3use std::marker::PhantomData;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use diode::{
8    AddServiceExt as _, App, AppBuilder, AppContext, Dependencies, Plugin, Service,
9    ServiceDependencyExt as _, StdError,
10};
11use tokio::task::JoinSet;
12
13/// Cooperative cancellation token used to signal daemons to shut down.
14pub use tokio_util::sync::CancellationToken;
15
16use crate::defer;
17
18#[derive(Default)]
19struct DaemonRegistry {
20    daemons: Vec<Arc<dyn DynDaemon>>,
21    types: HashSet<TypeId>,
22}
23
24impl DaemonRegistry {
25    pub fn add_daemon<T>(&mut self, daemon: Arc<T>)
26    where
27        T: Daemon + 'static,
28    {
29        if !self.types.insert(TypeId::of::<T>()) {
30            panic!("Daemon {} already added", type_name::<T>());
31        }
32        self.daemons.push(daemon);
33    }
34
35    pub fn has_daemon<T>(&self) -> bool
36    where
37        T: Daemon + 'static,
38    {
39        self.types.contains(&TypeId::of::<T>())
40    }
41
42    pub async fn run_daemons(
43        &self,
44        app: Arc<App>,
45        shutdown: CancellationToken,
46    ) -> Result<(), StdError> {
47        let span = tracing::info_span!("daemons");
48        let mut futures = JoinSet::new();
49        tracing::info!(parent: &span, "Daemons starting");
50        for daemon in self.daemons.iter() {
51            let shutdown = shutdown.child_token();
52            let app = app.clone();
53            let daemon = daemon.clone();
54            futures.spawn(async move { daemon.run(&app, shutdown).await });
55        }
56        tracing::info!(parent: &span, "Daemons running");
57        defer! {
58            tracing::info!(parent: &span, "Daemons stopped");
59        };
60        let first_result = futures.join_next().await;
61        shutdown.cancel();
62        if let Some(result) = first_result {
63            result.map_err(Box::new)??;
64            while let Some(result) = futures.join_next().await {
65                result.map_err(Box::new)??;
66            }
67        }
68        Ok(())
69    }
70}
71
72/// A long-running background task managed by the application.
73///
74/// Daemons are started together by [`RunDaemonsExt::run_daemons`] and run until
75/// the shared [`CancellationToken`] is cancelled (for example on shutdown) or
76/// until any one of them returns, at which point the rest are signalled to stop.
77/// Each daemon is given its own child cancellation token.
78///
79/// The default [`run`](Daemon::run) implementation does nothing and just waits
80/// for cancellation, which is handy for a daemon that only needs to keep a
81/// component alive.
82///
83/// Register a daemon with [`AddDaemonExt::add_daemon`] (a concrete instance) or
84/// [`AddDaemonServiceExt::add_daemon_service`] (resolved from a [`Service`]).
85pub trait Daemon: Send + Sync {
86    /// Runs the daemon until `shutdown` is cancelled.
87    ///
88    /// Implementations should return promptly once the token is cancelled.
89    /// Returning `Err` causes [`RunDaemonsExt::run_daemons`] to stop the other
90    /// daemons and surface the error.
91    fn run(
92        &self,
93        app: &App,
94        shutdown: CancellationToken,
95    ) -> impl Future<Output = Result<(), StdError>> + Send {
96        let _ = app;
97        async move {
98            shutdown.cancelled_owned().await;
99            Ok(())
100        }
101    }
102}
103
104#[async_trait]
105trait DynDaemon: Send + Sync {
106    async fn run(&self, app: &App, shutdown: CancellationToken) -> Result<(), StdError>;
107}
108
109#[async_trait]
110impl<T> DynDaemon for T
111where
112    T: Daemon,
113{
114    async fn run(&self, app: &App, shutdown: CancellationToken) -> Result<(), StdError> {
115        self.run(app, shutdown).await
116    }
117}
118
119/// Runs every registered [`Daemon`] until shutdown.
120pub trait RunDaemonsExt {
121    /// Runs all registered daemons concurrently.
122    ///
123    /// Returns once the `shutdown` token is cancelled or the first daemon
124    /// returns; in either case the remaining daemons are signalled to stop and
125    /// awaited. Returns immediately if no daemons were registered.
126    ///
127    /// # Errors
128    ///
129    /// Returns the first error reported by a daemon, or the join error if a
130    /// daemon task panics.
131    fn run_daemons(
132        self,
133        shutdown: CancellationToken,
134    ) -> impl Future<Output = Result<(), StdError>> + Send;
135}
136
137impl RunDaemonsExt for App {
138    async fn run_daemons(self, shutdown: CancellationToken) -> Result<(), StdError> {
139        Arc::new(self).run_daemons(shutdown).await
140    }
141}
142
143impl RunDaemonsExt for Arc<App> {
144    async fn run_daemons(self, shutdown: CancellationToken) -> Result<(), StdError> {
145        match self.get_component_ref::<DaemonRegistry>() {
146            Some(v) => v.run_daemons(self.clone(), shutdown).await,
147            None => Ok(()),
148        }
149    }
150}
151
152/// Registers concrete [`Daemon`] instances on the application.
153///
154/// This extension lives on [`AppContext`], so daemons can be registered both
155/// while configuring the builder and from within a plugin's `build`. A daemon is
156/// identified by its type.
157pub trait AddDaemonExt {
158    /// Registers `daemon` to be run by [`RunDaemonsExt::run_daemons`].
159    ///
160    /// # Panics
161    ///
162    /// Panics if a daemon of type `T` is already registered. Guard with
163    /// [`has_daemon`](AddDaemonExt::has_daemon) when the same type may be
164    /// registered more than once.
165    fn add_daemon<T>(&self, daemon: impl Into<Arc<T>>)
166    where
167        T: Daemon + 'static;
168
169    /// Returns whether a daemon of type `T` is registered.
170    fn has_daemon<T>(&self) -> bool
171    where
172        T: Daemon + 'static;
173}
174
175impl AddDaemonExt for AppContext {
176    fn add_daemon<T>(&self, daemon: impl Into<Arc<T>>)
177    where
178        T: Daemon + 'static,
179    {
180        if !self.has_component::<DaemonRegistry>() {
181            self.add_component(DaemonRegistry::default());
182        }
183        self.get_component_mut::<DaemonRegistry>()
184            .unwrap()
185            .add_daemon(daemon.into());
186    }
187
188    fn has_daemon<T>(&self) -> bool
189    where
190        T: Daemon + 'static,
191    {
192        self.get_component_ref::<DaemonRegistry>()
193            .is_some_and(|registry| registry.has_daemon::<T>())
194    }
195}
196
197struct DaemonServiceProvider<T>(PhantomData<T>);
198
199impl<T> Plugin for DaemonServiceProvider<T>
200where
201    T: Service<Handle = Arc<T>> + Daemon + 'static,
202{
203    async fn build(&self, ctx: &AppContext) -> Result<(), StdError> {
204        let handle = ctx.get_component::<T::Handle>().unwrap();
205        ctx.add_daemon::<T>(handle);
206        Ok(())
207    }
208
209    fn dependencies(&self) -> Dependencies {
210        T::dependencies().service::<T>()
211    }
212}
213
214/// Registers daemons resolved from the dependency-injection container.
215///
216/// The daemon type `T` is a [`Service`]: it is built by the container (together
217/// with its dependencies) and its handle is registered as a [`Daemon`]. The
218/// service is added automatically if it is not already present.
219pub trait AddDaemonServiceExt {
220    /// Registers the [`Service`] `T` to be run as a daemon.
221    ///
222    /// # Panics
223    ///
224    /// Panics if `T` is already registered as a daemon service (its provider
225    /// plugin would be added twice); guard with
226    /// [`has_daemon_service`](AddDaemonServiceExt::has_daemon_service) when this
227    /// can happen. Building the [`App`] additionally panics if `T` is registered
228    /// both as a daemon service and as an instance via
229    /// [`AddDaemonExt::add_daemon`].
230    fn add_daemon_service<T>(&mut self) -> &mut Self
231    where
232        T: Service<Handle = Arc<T>> + Daemon + 'static;
233
234    /// Returns whether `T` is registered as a daemon service.
235    fn has_daemon_service<T>(&self) -> bool
236    where
237        T: Service<Handle = Arc<T>> + Daemon + 'static;
238}
239
240impl AddDaemonServiceExt for AppBuilder {
241    fn add_daemon_service<T>(&mut self) -> &mut Self
242    where
243        T: Service<Handle = Arc<T>> + Daemon + 'static,
244    {
245        if !self.has_service::<T>() {
246            self.add_service::<T>();
247        }
248        self.add_plugin(DaemonServiceProvider::<T>(PhantomData));
249        self
250    }
251
252    fn has_daemon_service<T>(&self) -> bool
253    where
254        T: Service<Handle = Arc<T>> + Daemon + 'static,
255    {
256        self.has_plugin::<DaemonServiceProvider<T>>()
257    }
258}