Skip to main content

organix/service/
mod.rs

1mod control;
2mod intercom;
3mod stats;
4mod status;
5
6pub use self::{
7    control::{Control, ControlReader, Controller},
8    intercom::{
9        Intercom, IntercomMsg, IntercomReceiver, IntercomSender, IntercomStats, IntercomStatus,
10        NoIntercom,
11    },
12    stats::Stats,
13    status::{Status, StatusReader, StatusUpdater},
14};
15use crate::{runtime::Runtime, watchdog::WatchdogQuery};
16use async_trait::async_trait;
17use futures_util::future::abortable;
18use std::future::Future;
19use thiserror::Error;
20use tokio::{runtime::Handle, task::JoinHandle};
21use tracing_futures::Instrument as _;
22
23pub type ServiceIdentifier = &'static str;
24
25#[async_trait]
26pub trait Service: Send + Sized + 'static {
27    const SERVICE_IDENTIFIER: ServiceIdentifier;
28
29    type IntercomMsg: IntercomMsg;
30
31    fn prepare(service_state: ServiceState<Self>) -> Self;
32
33    async fn start(self);
34}
35
36pub trait ManageService {
37    const SERVICE_IDENTIFIER: ServiceIdentifier;
38
39    type IntercomMsg: IntercomMsg;
40}
41
42impl<T: Service> ManageService for ServiceManager<T> {
43    const SERVICE_IDENTIFIER: ServiceIdentifier = T::SERVICE_IDENTIFIER;
44
45    type IntercomMsg = T::IntercomMsg;
46}
47
48#[derive(Clone, Debug, Error, PartialEq, Eq)]
49pub enum ServiceError {
50    #[error("Service cannot be started because status is: {status}")]
51    CannotStart { status: Status },
52}
53
54#[derive(Debug, Clone)]
55pub struct StatusReport {
56    pub identifier: ServiceIdentifier,
57    pub status: Status,
58    pub intercom: IntercomStatus,
59    pub started: u64,
60}
61
62pub struct ServiceManager<T: Service> {
63    identifier: ServiceIdentifier,
64
65    intercom_sender: IntercomSender<T::IntercomMsg>,
66    intercom_stats: IntercomStats,
67    started: u64,
68
69    status: StatusReader,
70    controller: Controller,
71    runtime: Handle,
72}
73
74/// not to mistake for `tokio`'s runtime. This is the object that
75/// will hold the service process and all the other associated data.
76/// to allow for a good running activity of the service.
77///
78pub struct ServiceRuntime<T: Service> {
79    service_state: ServiceState<T>,
80
81    status: StatusUpdater,
82    control: ControlReader,
83}
84
85/// this is the object that every services has access to
86///
87/// each service has its own ServiceState. It allows to connect to
88/// other services [`intercom_with`] as well as getting access to the
89/// service's settings or state
90pub struct ServiceState<T: Service> {
91    identifier: ServiceIdentifier,
92    handle: Handle,
93    intercom_receiver: IntercomReceiver<T::IntercomMsg>,
94    watchdog_query: WatchdogQuery,
95    status: StatusReader,
96}
97
98impl<T: Service> ServiceState<T> {
99    /// access the service's Identifier
100    ///
101    /// this is just similar to calling `<T as Service>::SERVICE_IDENTIFIER`
102    pub fn identifier(&self) -> ServiceIdentifier {
103        self.identifier
104    }
105
106    /// open an [`Intercom`] handle with the given service `O`
107    ///
108    /// [`Intercom`]: ./struct.Intercom.html
109    pub fn intercom_with<O: Service>(&self) -> Intercom<O> {
110        self.watchdog_query.intercom::<O>()
111    }
112
113    /// access the `WatchdogQuery` allowing raw command access to all watchdog
114    /// commands.
115    pub fn watchdog_controller(&self) -> &WatchdogQuery {
116        &self.watchdog_query
117    }
118
119    /// access the service's IntercomReceiver end
120    ///
121    /// this is the end that will receive intercom messages from other services
122    pub fn intercom_mut(&mut self) -> &mut IntercomReceiver<T::IntercomMsg> {
123        &mut self.intercom_receiver
124    }
125
126    /// access the status reader of the service. If the status is updated
127    /// to be shutdown then the reader will receive the notification event
128    /// and will be able to prepare for shutdown gracefully
129    pub fn status_reader(&self) -> &StatusReader {
130        &self.status
131    }
132
133    /// access the service's Runtime handle
134    ///
135    /// This object can be cloned and send between tasks allowing for
136    /// other tasks to create their own subtasks and so on
137    pub fn runtime_handle(&self) -> &Handle {
138        &self.handle
139    }
140
141    /// spawn the given future in the context of the Service's Runtime.
142    ///
143    /// While there is no way to enforce the users to actually spawn tasks
144    /// within the Runtime we can at least urge the users to do so and avoid
145    /// using the global runtime context as it may be used for other purposes.
146    pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
147    where
148        F: Future + Send + 'static,
149        F::Output: Send + 'static,
150    {
151        self.runtime_handle().spawn(future)
152    }
153}
154
155impl<T: Service> ServiceManager<T> {
156    pub fn with_runtime(runtime: &mut Runtime) -> Self {
157        let identifier = T::SERVICE_IDENTIFIER;
158
159        let status = StatusReader::new(Status::shutdown());
160        let controller = runtime.block_on(async { Controller::new().await });
161        let (intercom_sender, _, intercom_stats) = intercom::channel();
162
163        Self {
164            identifier,
165            intercom_sender,
166            intercom_stats,
167            status,
168            controller,
169            runtime: runtime.handle().clone(),
170            started: 0,
171        }
172    }
173
174    pub fn intercom(&self) -> IntercomSender<T::IntercomMsg> {
175        self.intercom_sender.clone()
176    }
177
178    pub async fn status(&self) -> StatusReport {
179        StatusReport {
180            identifier: self.identifier,
181            status: self.status.status(),
182            intercom: self.intercom_stats.status().await,
183            started: self.started,
184        }
185    }
186
187    pub fn shutdown(&mut self) {
188        match self.status.status() {
189            Status::Shutdown { .. } | Status::ShuttingDown { .. } => {
190                // Ignore as the node is either shutdown or already shutting
191                // down
192            }
193            Status::Starting { .. } | Status::Started { .. } => {
194                // send only if the node will have a chance to actually read
195                // the command
196                self.controller.send(Control::Shutdown)
197            }
198        }
199    }
200
201    pub fn runtime(
202        &mut self,
203        watchdog_query: WatchdogQuery,
204    ) -> Result<ServiceRuntime<T>, ServiceError> {
205        let status = self.status.status();
206        if !status.is_shutdown() {
207            Err(ServiceError::CannotStart { status })
208        } else {
209            let (intercom_sender, intercom_receiver, intercom_stats) =
210                intercom::channel::<T::IntercomMsg>();
211
212            self.intercom_sender = intercom_sender;
213            self.intercom_stats = intercom_stats;
214            self.started += 1;
215
216            Ok(ServiceRuntime {
217                service_state: ServiceState {
218                    identifier: self.identifier,
219                    handle: self.runtime.clone(),
220                    status: self.status.clone(),
221                    intercom_receiver,
222                    watchdog_query,
223                },
224                status: self.status.updater(),
225                control: self.controller.reader(),
226            })
227        }
228    }
229}
230
231impl<T: Service> ServiceRuntime<T> {
232    pub fn start(self) {
233        let ServiceRuntime {
234            service_state,
235            status,
236            mut control,
237        } = self;
238
239        let service_identifier: &'static str = service_state.identifier;
240
241        status.update(Status::starting());
242
243        let watchdog_query = service_state.watchdog_query.clone();
244        let handle = service_state.handle.clone();
245        let runner = T::prepare(service_state);
246
247        let (runner, abort_handle) = abortable(async move {
248            let span = tracing::info_span!("service", service_identifier);
249            let _enter = span.enter();
250
251            runner.start().in_current_span().await
252        });
253
254        let mut service_join_handle = handle.spawn(runner);
255
256        // the runner (the service) has been started into its current runtime. They must use
257        // the `handle` to spawn new tasks.
258        //
259        // however the control of the service is still spawned in the watchdog current context
260        // so we can perform the management tasks without disrupting the service's runtime
261        watchdog_query.spawn(async move {
262            status.update(Status::started());
263
264            let span = tracing::debug_span!("service control", service_identifier);
265            let _enter = span.enter();
266
267            loop {
268                tokio::select! {
269                    join_result = &mut service_join_handle => {
270                        if let Err(join_error) = join_result {
271                            // TODO: the task could not join, either cancelled
272                            //       or panicked. Ideally we need to document
273                            //       this panic and see what kind of strategy
274                            //       can be applied (can we restart the service?)
275                            //       or is it a fatal panic and we cannot recover?
276
277                            tracing::error!(
278                                "main process failed with following error: {:#?}",
279                                join_error
280                            );
281                        } else {
282                            // nothing to do her, the service already finished and
283                            // returned successfully
284                        }
285                        status.update(Status::shutdown());
286                        break;
287                    }
288                    control = control.updated() => {
289                        match control {
290                            Some(Control::Shutdown) => {
291                                tracing::info!("shutting down...");
292
293                                // updating the status will notify the `StatusReader` in the `ServiceState`
294                                // if watched, the future will yield and the service will be able to prepare
295                                // for the service shutdown and exit gracefully.
296                                status.update(Status::shutting_down());
297                            }
298                            None | Some(Control::Kill) => {
299                                tracing::info!("Terminating...");
300                                status.update(Status::shutdown());
301                                abort_handle.abort();
302                                break;
303                            }
304                        }
305                    }
306                };
307            }
308        });
309    }
310}
311
312impl<T: Service> Drop for ServiceManager<T> {
313    fn drop(&mut self) {
314        if !self.status.status().is_shutdown() {
315            self.controller.send(Control::Kill)
316        }
317    }
318}