libdd_shared_runtime/shared_runtime/mod.rs
1// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`SharedRuntime`] trait and its implementations: [`ForkSafeRuntime`], [`BasicRuntime`],
5//! and [`LocalRuntime`].
6
7pub(crate) mod pausable_worker;
8
9#[cfg(not(target_arch = "wasm32"))]
10mod basic;
11#[cfg(not(target_arch = "wasm32"))]
12mod fork_safe;
13#[cfg(target_arch = "wasm32")]
14mod local;
15
16#[cfg(not(target_arch = "wasm32"))]
17pub use basic::BasicRuntime;
18#[cfg(not(target_arch = "wasm32"))]
19pub use fork_safe::ForkSafeRuntime;
20#[cfg(target_arch = "wasm32")]
21pub use local::LocalRuntime;
22
23use crate::worker::Worker;
24use libdd_capabilities::MaybeSend;
25use libdd_common::MutexExt;
26use pausable_worker::{PausableWorker, PausableWorkerError};
27use std::sync::{Arc, Mutex};
28use std::{fmt, io};
29
30/// A worker registered on a [`SharedRuntime`].
31pub(crate) type BoxedWorker = Box<dyn Worker + Sync>;
32
33#[derive(Debug)]
34pub(crate) struct WorkerEntry {
35 pub(crate) id: u64,
36 pub(crate) restart_on_fork: bool,
37 pub(crate) worker: PausableWorker<BoxedWorker>,
38}
39
40/// Common interface for all [`SharedRuntime`] implementations.
41///
42/// # Choosing an implementation
43///
44/// | Situation | Runtime |
45/// |-----------|---------|
46/// | Native, host process may call `fork(2)` | [`ForkSafeRuntime`] |
47/// | Native, caller owns a tokio runtime and wants to share it | [`BasicRuntime`] |
48/// | Wasm / single-threaded JS event loop | [`LocalRuntime`] |
49///
50/// Sync entry points (e.g. a blocking `build` or `send`) additionally require
51/// `R: `[`BlockingRuntime`], which only the native implementations satisfy.
52pub trait SharedRuntime {
53 /// Creates a new instance of the runtime with default configuration.
54 ///
55 /// Used as a fallback by callers (e.g. [`crate::shared_runtime`] consumers) that want to
56 /// auto-construct a runtime when one was not supplied; concrete runtimes additionally
57 /// expose richer inherent constructors (e.g. `with_worker_threads`, `from_handle`).
58 fn new() -> Result<Self, SharedRuntimeError>
59 where
60 Self: Sized;
61
62 /// Spawns a worker. `restart_on_fork = true` causes `ForkSafeRuntime::after_fork_child`
63 /// to reset and restart it; `false` drops it without calling shutdown. [`BasicRuntime`]
64 /// and [`LocalRuntime`] ignore this flag — they do not implement a fork protocol.
65 fn spawn_worker<T: Worker + Sync + 'static>(
66 &self,
67 worker: T,
68 restart_on_fork: bool,
69 ) -> Result<WorkerHandle, SharedRuntimeError>;
70
71 /// Shuts down all tracked workers. The runtime itself is not torn down — call
72 /// [`ForkSafeRuntime::shutdown`] (native only) to also drop the tokio runtime.
73 fn shutdown_async(&self) -> impl std::future::Future<Output = ()> + MaybeSend + '_
74 where
75 Self: Sync;
76}
77
78/// Error returned by [`BlockingRuntime::block_on_with_timeout`].
79#[derive(Debug)]
80pub enum BlockOnTimeoutError {
81 /// The executor could not be accessed or constructed.
82 Io(io::Error),
83 /// The deadline elapsed before the future completed.
84 TimedOut(std::time::Duration),
85}
86
87impl fmt::Display for BlockOnTimeoutError {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Self::Io(err) => write!(f, "Executor error: {}", err),
91 Self::TimedOut(duration) => write!(f, "Timed out after {:?}", duration),
92 }
93 }
94}
95
96impl std::error::Error for BlockOnTimeoutError {}
97
98impl From<io::Error> for BlockOnTimeoutError {
99 fn from(err: io::Error) -> Self {
100 Self::Io(err)
101 }
102}
103
104/// Returns true if `payload` is the panic that Tokio raises when it polls a timer on a
105/// runtime built without `enable_time`.
106///
107/// Tokio has no fallible constructor for this case. [`BlockingRuntime::block_on_with_timeout`]
108/// matches the panic message to convert the panic into a [`BlockOnTimeoutError`] instead of
109/// letting the panic abort the process.
110#[cfg(not(target_arch = "wasm32"))]
111fn is_timers_disabled_panic(payload: &(dyn std::any::Any + Send)) -> bool {
112 // Tokio runs the timer driver as an internal task and re-raises that task's panic
113 // through `resume_unwind` on a boxed `JoinError` payload. The `JoinError` payload nests
114 // one `Box<dyn Any + Send>` inside the outer payload caught here, so this function
115 // unwraps the inner payload before matching on the message.
116 if let Some(inner) = payload.downcast_ref::<Box<dyn std::any::Any + Send>>() {
117 return is_timers_disabled_panic(inner.as_ref());
118 }
119 let message = payload
120 .downcast_ref::<&str>()
121 .copied()
122 .or_else(|| payload.downcast_ref::<String>().map(String::as_str));
123 matches!(message, Some(message) if message.contains("timers are disabled"))
124}
125
126/// Extension of [`SharedRuntime`] for runtimes that can block the current thread on a future.
127#[cfg(not(target_arch = "wasm32"))]
128pub trait BlockingRuntime: SharedRuntime {
129 /// Drives `f` to completion, blocking the current thread.
130 ///
131 /// Returns an [`io::Error`] if the executor cannot be accessed or constructed.
132 fn block_on<F: std::future::Future>(&self, f: F) -> Result<F::Output, io::Error>;
133
134 /// Drives `f` to completion, blocking the current thread, but gives up once `timeout`
135 /// elapses.
136 ///
137 /// This method drives the deadline on the same executor as [`block_on`](Self::block_on).
138 ///
139 /// The bound is cooperative: the executor can only check the deadline when `f` yields.
140 /// A future that never yields, or that blocks the thread outside of `.await`, can run
141 /// past `timeout` before this method returns.
142 ///
143 /// This method requires a build with unwinding panics. A `panic = "abort"` build cannot
144 /// catch the panic that a timerless runtime raises (see below), and aborts the process
145 /// instead of returning [`BlockOnTimeoutError::Io`].
146 fn block_on_with_timeout<F: std::future::Future>(
147 &self,
148 f: F,
149 timeout: std::time::Duration,
150 ) -> Result<F::Output, BlockOnTimeoutError> {
151 // When the executor has no timer driver (e.g. a caller-supplied runtime built without
152 // `enable_time`), tokio::time::timeout panics instead of returning an error. The
153 // default block_on implementations are tokio-backed, so the panic reaches this
154 // catch_unwind. Catching the panic here converts it to an error, so a misconfigured
155 // executor cannot abort a process that calls block_on_with_timeout across the FFI
156 // boundary.
157 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
158 self.block_on(async move { tokio::time::timeout(timeout, f).await })
159 }));
160 let outcome = match result {
161 Ok(outcome) => outcome,
162 Err(payload) if is_timers_disabled_panic(&payload) => {
163 return Err(BlockOnTimeoutError::Io(io::Error::other(
164 "block_on_with_timeout requires a runtime with timers enabled",
165 )));
166 }
167 Err(payload) => std::panic::resume_unwind(payload),
168 };
169 outcome?.map_err(|_| BlockOnTimeoutError::TimedOut(timeout))
170 }
171}
172
173/// Handle to a worker registered on a [`SharedRuntime`].
174///
175/// This handle can be used to stop the worker.
176///
177/// # Warning
178/// If every clone of this handle is dropped without calling [`WorkerHandle::stop`], the worker
179/// remains registered on the [`SharedRuntime`] and can only be torn down by shutting the
180/// runtime down. Workers are expected to detect that their input channel has been closed and
181/// park themselves to avoid spinning, but they will not be freed until the runtime stops.
182#[must_use = "dropping a WorkerHandle without calling stop() leaks the worker until the SharedRuntime is shut down"]
183#[derive(Clone, Debug)]
184pub struct WorkerHandle {
185 pub(crate) worker_id: u64,
186 pub(crate) workers: Arc<Mutex<Vec<WorkerEntry>>>,
187}
188
189#[derive(Debug)]
190pub enum WorkerHandleError {
191 AlreadyStopped,
192 WorkerError(PausableWorkerError),
193}
194
195impl fmt::Display for WorkerHandleError {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::AlreadyStopped => {
199 write!(f, "Worker has already been stopped")
200 }
201 Self::WorkerError(err) => write!(f, "Worker error: {}", err),
202 }
203 }
204}
205
206impl std::error::Error for WorkerHandleError {}
207
208impl From<PausableWorkerError> for WorkerHandleError {
209 fn from(err: PausableWorkerError) -> Self {
210 Self::WorkerError(err)
211 }
212}
213
214impl WorkerHandle {
215 /// Configure whether this worker restarts in a fork child.
216 ///
217 /// When disabled, the next [`ForkSafeRuntime::after_fork_child`] call drops the worker without
218 /// running its shutdown logic. Language runtimes can disable restart in a managed before-fork
219 /// hook, restore it in the parent hook, and replace the inherited worker in the child.
220 ///
221 /// # Errors
222 /// Returns an error if the worker has already been stopped or dropped.
223 pub fn set_fork_restart(&self, restart_on_fork: bool) -> Result<(), WorkerHandleError> {
224 let mut workers_lock = self.workers.lock_or_panic();
225 let Some(entry) = workers_lock
226 .iter_mut()
227 .find(|entry| entry.id == self.worker_id)
228 else {
229 return Err(WorkerHandleError::AlreadyStopped);
230 };
231 entry.restart_on_fork = restart_on_fork;
232 Ok(())
233 }
234
235 /// Stop the worker and execute the shutdown logic.
236 ///
237 /// # Errors
238 /// Returns an error if the worker has already been stopped.
239 ///
240 /// # Cancel safety
241 /// This function is *NOT* cancel safe and shouldn't be called in [Worker::trigger].
242 /// If cancelled, the stopped worker can end up in an invalid state if a fork occurs while
243 /// stopping.
244 pub async fn stop(self) -> Result<(), WorkerHandleError> {
245 let mut worker = {
246 let mut workers_lock = self.workers.lock_or_panic();
247 let Some(position) = workers_lock
248 .iter()
249 .position(|entry| entry.id == self.worker_id)
250 else {
251 return Err(WorkerHandleError::AlreadyStopped);
252 };
253 let WorkerEntry { worker, .. } = workers_lock.swap_remove(position);
254 worker
255 };
256 worker.pause().await?;
257 worker.shutdown().await;
258 Ok(())
259 }
260}
261
262/// Errors that can occur when using a `SharedRuntime` implementation.
263#[derive(Debug)]
264pub enum SharedRuntimeError {
265 /// The runtime is not available or in an invalid state.
266 RuntimeUnavailable,
267 /// Failed to acquire a lock on internal state.
268 LockFailed(String),
269 /// A worker operation failed.
270 WorkerError(PausableWorkerError),
271 /// Failed to create the tokio runtime.
272 RuntimeCreation(io::Error),
273 /// Shutdown timed out.
274 ShutdownTimedOut(std::time::Duration),
275}
276
277impl fmt::Display for SharedRuntimeError {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 match self {
280 Self::RuntimeUnavailable => {
281 write!(f, "Runtime is not available or in an invalid state")
282 }
283 Self::LockFailed(msg) => write!(f, "Failed to acquire lock: {}", msg),
284 Self::WorkerError(err) => write!(f, "Worker error: {}", err),
285 Self::RuntimeCreation(err) => {
286 write!(f, "Failed to create runtime: {}", err)
287 }
288 Self::ShutdownTimedOut(duration) => {
289 write!(f, "Shutdown timed out after {:?}", duration)
290 }
291 }
292 }
293}
294
295impl std::error::Error for SharedRuntimeError {}
296
297impl From<PausableWorkerError> for SharedRuntimeError {
298 fn from(err: PausableWorkerError) -> Self {
299 SharedRuntimeError::WorkerError(err)
300 }
301}
302
303impl From<io::Error> for SharedRuntimeError {
304 fn from(err: io::Error) -> Self {
305 SharedRuntimeError::RuntimeCreation(err)
306 }
307}