Skip to main content

weida_runtime/
exec.rs

1//! The one surface onto the async runtime: tasks, timers and DNS.
2
3use std::time::Duration;
4
5use tokio::runtime::Handle;
6use tokio::task::JoinHandle;
7use weida_core::Error;
8
9/// A library's whole surface onto the async runtime: tasks, timers and DNS.
10///
11/// An `Exec` is a Tokio handle and nothing more. It never owns the runtime,
12/// so a task holding one can neither keep the runtime alive nor drop it from
13/// inside itself. Cloning is a handle clone.
14///
15/// **Why a type rather than plain `tokio::spawn`.** A library that calls
16/// `tokio::spawn`, `tokio::time` or `tokio::net::lookup_host` directly
17/// demands that *its caller* be inside a Tokio reactor, on the very thread
18/// that called it. Holding an `Exec` moves that requirement into the library:
19/// the reactor is wherever the `Exec` points, the caller may drive the
20/// returned futures on any executor — `futures::executor::block_on`
21/// included — and a grep for those three names over the library's own `src`
22/// is the proof that no path escaped
23/// (`docs/ARCHITECTURE.md` §5).
24#[derive(Clone)]
25pub struct Exec {
26    handle: Handle,
27}
28
29impl Exec {
30    /// Wraps the runtime `handle` names.
31    ///
32    /// For a process that already runs a reactor somewhere other than the
33    /// calling thread: nothing has to be ambient, and the caller's own
34    /// executor is never consulted.
35    pub fn from_handle(handle: Handle) -> Exec {
36        Exec { handle }
37    }
38
39    /// The ambient handle.
40    ///
41    /// Fails when the calling thread is not inside a Tokio runtime. Failing
42    /// here — at construction — beats failing later at an unrelated call
43    /// site.
44    pub fn current() -> Result<Exec, Error> {
45        Handle::try_current()
46            .map(Exec::from_handle)
47            .map_err(|_| Error::Runtime("no ambient tokio runtime to run on".into()))
48    }
49
50    /// Creates a multi-thread Tokio runtime with `worker_threads` workers,
51    /// named `thread_name`, and returns a handle onto it beside the
52    /// [`OwnedReactor`] that keeps it alive.
53    ///
54    /// This is the constructor for a library whose users have no reactor at
55    /// all, which is most of a synchronous protocol library's audience: the
56    /// library owns the reactor its sockets and timers need, and the caller
57    /// keeps its own executor — or none. The reactor dies with the returned
58    /// [`OwnedReactor`], so a library holds it beside every handle it hands
59    /// out and drops it last.
60    ///
61    /// Fails when `worker_threads` is `0`, or when the OS refuses the
62    /// threads.
63    pub fn owned(worker_threads: usize, thread_name: &str) -> Result<(Exec, OwnedReactor), Error> {
64        if worker_threads == 0 {
65            return Err(Error::Runtime("worker_threads must be at least 1".into()));
66        }
67        let runtime = tokio::runtime::Builder::new_multi_thread()
68            .enable_all()
69            .worker_threads(worker_threads)
70            .thread_name(thread_name)
71            .build()
72            .map_err(Error::Io)?;
73        let exec = Exec::from_handle(runtime.handle().clone());
74        Ok((exec, OwnedReactor(Some(runtime))))
75    }
76
77    /// Enters the runtime context, for the constructors that register a
78    /// socket with the reactor as they are built — `quinn`'s endpoints,
79    /// `tokio::net`'s listeners. Held around the constructor only, never
80    /// across an await.
81    pub fn enter(&self) -> tokio::runtime::EnterGuard<'_> {
82        self.handle.enter()
83    }
84
85    /// Spawns a task on the runtime. Works from any thread, with or without
86    /// an ambient reactor — which is why a library holding an `Exec` never
87    /// calls `tokio::spawn`.
88    pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
89    where
90        F: Future + Send + 'static,
91        F::Output: Send + 'static,
92    {
93        self.handle.spawn(future)
94    }
95
96    /// A timer on this runtime's wheel. The `Sleep` is created inside the
97    /// runtime context, so the returned future may be awaited anywhere.
98    ///
99    /// Every clock a protocol has is one of these: a reconnect interval, a
100    /// handshake deadline, a heartbeat, a connect timeout, a send or receive
101    /// timeout, a linger budget.
102    pub fn sleep(&self, duration: Duration) -> tokio::time::Sleep {
103        let _guard = self.handle.enter();
104        tokio::time::sleep(duration)
105    }
106
107    /// Awaits `future`, giving up after `limit`.
108    ///
109    /// `None` means the future had not finished; it is dropped, so whatever
110    /// it held is released. This is the only place an await is bounded on
111    /// wall-clock time, for the same reason [`Exec::sleep`] lives here: the
112    /// timer belongs to the runtime, not to the caller.
113    pub async fn within<F: Future>(&self, limit: Duration, future: F) -> Option<F::Output> {
114        let deadline = self.sleep(limit);
115        tokio::select! {
116            output = future => Some(output),
117            () = deadline => None,
118        }
119    }
120
121    /// Resolves `host:port` through the **system** resolver.
122    ///
123    /// The convenience the competitor libraries use: a foreign-protocol client
124    /// dials what its own configuration names, and none of them has a reason
125    /// to let an application replace name resolution. weida's own dial path
126    /// goes through [`crate::Resolver`] instead, because a `weida://`
127    /// authority may name a set and what a set means is the deployment's
128    /// decision (`docs/decisions/0020-cluster-and-discovery.md` §4.2).
129    ///
130    /// One implementation, two entry points: this delegates to
131    /// [`crate::SystemResolver`].
132    pub async fn resolve(
133        &self,
134        host: &str,
135        port: u16,
136        max_addresses: usize,
137    ) -> Result<Vec<std::net::SocketAddr>, Error> {
138        use crate::resolve::Resolver;
139        crate::resolve::SystemResolver
140            .resolve(self, host, Some(port), max_addresses)
141            .await
142    }
143}
144
145impl std::fmt::Debug for Exec {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("Exec").finish_non_exhaustive()
148    }
149}
150
151/// Keeps a Tokio runtime created by [`Exec::owned`] alive for as long as
152/// whatever created it.
153///
154/// Hold it beside every handle onto that runtime and drop it last. Dropping
155/// it shuts the runtime down **in the background** rather than blocking,
156/// because the last owner may go out of scope on one of that runtime's own
157/// worker threads, where dropping a Tokio runtime panics.
158pub struct OwnedReactor(Option<tokio::runtime::Runtime>);
159
160impl Drop for OwnedReactor {
161    fn drop(&mut self) {
162        if let Some(runtime) = self.0.take() {
163            // The last owner may go out of scope on one of this runtime's own
164            // worker threads. Dropping a Tokio runtime there panics; shutting
165            // it down in the background does not.
166            runtime.shutdown_background();
167        }
168    }
169}
170
171impl std::fmt::Debug for OwnedReactor {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct("OwnedReactor").finish_non_exhaustive()
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn an_exec_without_an_ambient_reactor_fails() {
183        let err = Exec::current().unwrap_err();
184        assert!(matches!(err, Error::Runtime(_)), "{err:?}");
185    }
186
187    #[test]
188    fn zero_worker_threads_is_rejected() {
189        let err = Exec::owned(0, "test").unwrap_err();
190        assert!(matches!(err, Error::Runtime(_)), "{err:?}");
191    }
192
193    /// The whole point of `owned`: no ambient reactor, on this thread or any
194    /// other, and the library's tasks still run — driven here by an executor
195    /// that is not Tokio at all.
196    #[test]
197    fn an_owned_reactor_needs_no_ambient_one() {
198        assert!(Handle::try_current().is_err());
199        let (exec, reactor) = Exec::owned(1, "test").expect("owned reactor");
200        let joined = exec.spawn(async { 7u8 });
201        assert_eq!(futures::executor::block_on(joined).expect("task"), 7);
202        drop(reactor);
203    }
204
205    #[tokio::test]
206    async fn within_gives_up_on_a_future_that_never_finishes() {
207        let exec = Exec::current().expect("ambient runtime");
208        assert!(
209            exec.within(Duration::from_millis(1), std::future::pending::<()>())
210                .await
211                .is_none()
212        );
213        assert_eq!(
214            exec.within(Duration::from_secs(30), async { 3 }).await,
215            Some(3)
216        );
217    }
218}