Skip to main content

ruststream/runtime/
main_runtime.rs

1//! [`MainRuntime`]: the app's runtime as a handler reaches it, and the explicit way to send work
2//! there from a dedicated thread.
3
4use std::future::Future;
5
6use tokio::runtime::Handle;
7use tokio::task::{JoinError, JoinHandle};
8
9/// The runtime the app runs on, as a handler reaches it: `Ctx(main): Ctx<MainRuntime>` as a
10/// parameter, or [`Context::main_runtime`](super::Context::main_runtime).
11///
12/// A handler on dedicated threads (`threads(n)`) runs on its own thread, and so does everything
13/// it leaves behind: a plain `tokio::spawn`, a timer, an `and_after` continuation, an `after(..)`
14/// hook and the timer of a `retry_after` copy. `MainRuntime` is the one explicit way to send work
15/// to the app's runtime instead: [`spawn`](Self::spawn) starts it there and moves on,
16/// [`run`](Self::run) runs it there and hands its output back. What is sent must be `Send`.
17/// A plain `tokio::spawn` on a dedicated thread lives as long as that thread: a task still
18/// pending when the subscription ends is dropped with the thread's runtime.
19/// On any other placement the handler already runs on this runtime.
20///
21/// # Examples
22///
23/// ```
24/// # #[cfg(all(feature = "macros", feature = "json"))]
25/// # mod demo {
26/// use ruststream::prelude::*;
27///
28/// #[derive(serde::Deserialize)]
29/// struct Job {
30///     id: u64,
31/// }
32///
33/// async fn notify(_id: u64) {}
34///
35/// async fn lookup(id: u64) -> u64 {
36///     id * 2
37/// }
38///
39/// #[subscriber("jobs", threads(4))]
40/// async fn index(job: &Job, Ctx(main): Ctx<MainRuntime>) -> HandlerOutcome {
41///     // The notification leaves the thread; the handler does not wait for it.
42///     main.spawn(notify(job.id));
43///     // The lookup runs on the app's runtime, and its answer comes back here.
44///     match main.run(lookup(job.id)).await {
45///         Ok(_answer) => HandlerOutcome::ack(),
46///         Err(_) => HandlerOutcome::retry(),
47///     }
48/// }
49/// # }
50/// ```
51#[derive(Debug, Clone)]
52pub struct MainRuntime(Handle);
53
54impl MainRuntime {
55    /// The runtime the calling task runs on: a subscription captures it when it opens, inside
56    /// the app's startup.
57    pub(crate) fn current() -> Self {
58        Self(Handle::current())
59    }
60
61    /// Wraps `handle`.
62    #[cfg(test)]
63    pub(crate) const fn new(handle: Handle) -> Self {
64        Self(handle)
65    }
66
67    /// Starts `future` as a task of the app's runtime and returns its handle; the handler does
68    /// not wait for it unless it awaits the handle.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use ruststream::IncomingMessage;
74    /// use ruststream::runtime::{Context, HandlerOutcome};
75    ///
76    /// async fn notify(_id: u64) {}
77    ///
78    /// async fn handle<M: IncomingMessage>(_msg: &M, ctx: &mut Context<'_>) -> HandlerOutcome {
79    ///     ctx.main_runtime().spawn(notify(7));
80    ///     HandlerOutcome::ack()
81    /// }
82    /// ```
83    pub fn spawn<Work>(&self, future: Work) -> JoinHandle<Work::Output>
84    where
85        Work: Future + Send + 'static,
86        Work::Output: Send + 'static,
87    {
88        self.0.spawn(future)
89    }
90
91    /// Runs `future` as a task of the app's runtime and resolves to its output.
92    ///
93    /// # Errors
94    ///
95    /// Returns the [`JoinError`] of a task that panicked, or that the runtime cancelled because
96    /// it is shutting down.
97    ///
98    /// # Cancel safety
99    ///
100    /// Dropping the returned future stops waiting, not the work: the task runs to its end on the
101    /// app's runtime.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use ruststream::IncomingMessage;
107    /// use ruststream::runtime::{Context, HandlerOutcome};
108    ///
109    /// async fn lookup(id: u64) -> u64 {
110    ///     id * 2
111    /// }
112    ///
113    /// async fn handle<M: IncomingMessage>(_msg: &M, ctx: &mut Context<'_>) -> HandlerOutcome {
114    ///     match ctx.main_runtime().run(lookup(7)).await {
115    ///         Ok(14) => HandlerOutcome::ack(),
116    ///         _ => HandlerOutcome::retry(),
117    ///     }
118    /// }
119    /// ```
120    pub fn run<Work>(
121        &self,
122        future: Work,
123    ) -> impl Future<Output = Result<Work::Output, JoinError>> + Send + 'static
124    where
125        Work: Future + Send + 'static,
126        Work::Output: Send + 'static,
127    {
128        self.0.spawn(future)
129    }
130
131    /// The Tokio handle of the app's runtime, for what the two methods above do not cover
132    /// (`block_on` from a thread outside any runtime, the runtime's metrics).
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use ruststream::IncomingMessage;
138    /// use ruststream::runtime::{Context, HandlerOutcome};
139    ///
140    /// async fn handle<M: IncomingMessage>(_msg: &M, ctx: &mut Context<'_>) -> HandlerOutcome {
141    ///     // The app's runtime metrics, read from a handler on a thread of its own.
142    ///     let metrics = ctx.main_runtime().as_handle().metrics();
143    ///     tracing::info!(
144    ///         workers = metrics.num_workers(),
145    ///         alive = metrics.num_alive_tasks(),
146    ///         "app runtime"
147    ///     );
148    ///     HandlerOutcome::ack()
149    /// }
150    /// ```
151    #[must_use]
152    pub const fn as_handle(&self) -> &Handle {
153        &self.0
154    }
155}