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}