Skip to main content

libdd_shared_runtime/shared_runtime/
basic.rs

1// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use super::pausable_worker::{tokio_spawn_fn, PausableWorker};
5use super::{
6    BlockingRuntime, BoxedWorker, SharedRuntime, SharedRuntimeError, WorkerEntry, WorkerHandle,
7};
8use crate::worker::Worker;
9use futures::stream::{FuturesUnordered, StreamExt};
10use libdd_common::MutexExt;
11use std::io;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14use tracing::{debug, error, warn};
15
16/// Non-fork-safe [`SharedRuntime`] implementation.
17///
18/// The internal `Arc<tokio::runtime::Runtime>` is either library-built
19/// ([`BasicRuntime::new`] / [`BasicRuntime::with_worker_threads`]) or
20/// caller-provided ([`BasicRuntime::from_handle`]). The `Arc` keeps the runtime
21/// alive for the lifetime of this struct even if the caller drops their clone of
22/// the handle.
23///
24/// This type does **not** implement the fork protocol. If the process forks,
25/// use [`crate::ForkSafeRuntime`] instead — or handle the fork at the
26/// outer runtime layer that owns the tokio runtime passed in.
27#[derive(Debug)]
28pub struct BasicRuntime {
29    runtime: Arc<tokio::runtime::Runtime>,
30    workers: Arc<Mutex<Vec<WorkerEntry>>>,
31    next_worker_id: AtomicU64,
32}
33
34impl BasicRuntime {
35    /// Creates a new `BasicRuntime` backed by a library-built multi-thread tokio runtime
36    /// with the given number of worker threads.
37    pub fn with_worker_threads(worker_threads: usize) -> Result<Self, SharedRuntimeError> {
38        let runtime = tokio::runtime::Builder::new_multi_thread()
39            .worker_threads(worker_threads)
40            .enable_all()
41            .build()?;
42        Ok(Self::from_handle(Arc::new(runtime)))
43    }
44
45    /// Creates a new `BasicRuntime` wrapping a caller-provided runtime.
46    ///
47    /// The runtime is held via `Arc`, so it stays alive for the lifetime of this struct
48    /// even if the caller drops their clone.
49    pub fn from_handle(runtime: Arc<tokio::runtime::Runtime>) -> Self {
50        Self {
51            runtime,
52            workers: Arc::new(Mutex::new(Vec::new())),
53            next_worker_id: AtomicU64::new(1),
54        }
55    }
56
57    fn push_worker(
58        &self,
59        workers_guard: &mut std::sync::MutexGuard<Vec<WorkerEntry>>,
60        pausable_worker: PausableWorker<BoxedWorker>,
61    ) -> WorkerHandle {
62        let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed);
63        workers_guard.push(WorkerEntry {
64            id: worker_id,
65            restart_on_fork: false,
66            worker: pausable_worker,
67        });
68        WorkerHandle {
69            worker_id,
70            workers: self.workers.clone(),
71        }
72    }
73}
74
75impl SharedRuntime for BasicRuntime {
76    fn new() -> Result<Self, SharedRuntimeError> {
77        Self::with_worker_threads(1)
78    }
79
80    fn spawn_worker<T: Worker + Sync + 'static>(
81        &self,
82        worker: T,
83        restart_on_fork: bool,
84    ) -> Result<WorkerHandle, SharedRuntimeError> {
85        if restart_on_fork {
86            warn!(
87                "restart_on_fork is ignored on BasicRuntime: regular mode is not fork-safe; \
88                 use ForkSafeRuntime if you need fork hooks"
89            );
90        }
91
92        let boxed_worker: BoxedWorker = Box::new(worker);
93        debug!(?boxed_worker, "Spawning worker on BasicRuntime");
94        let mut pausable_worker = PausableWorker::new(boxed_worker);
95
96        // Hold the workers lock across start+push so a concurrent shutdown_async cannot
97        // drain the registry between starting the task and recording it — which would
98        // otherwise leave a live worker behind that shutdown_async never paused.
99        let mut workers_guard = self.workers.lock_or_panic();
100        pausable_worker.start(tokio_spawn_fn(self.runtime.handle()))?;
101        Ok(self.push_worker(&mut workers_guard, pausable_worker))
102    }
103
104    async fn shutdown_async(&self) {
105        debug!("Shutting down all workers on BasicRuntime");
106        let workers = {
107            let mut workers_lock = self.workers.lock_or_panic();
108            std::mem::take(&mut *workers_lock)
109        };
110
111        let futures: FuturesUnordered<_> = workers
112            .into_iter()
113            .map(|mut worker_entry| async move {
114                if let Err(e) = worker_entry.worker.pause().await {
115                    error!("Worker failed to shutdown: {:?}", e);
116                    return;
117                }
118                worker_entry.worker.shutdown().await;
119            })
120            .collect();
121
122        futures.collect::<()>().await;
123    }
124}
125
126impl BlockingRuntime for BasicRuntime {
127    fn block_on<F: std::future::Future>(&self, f: F) -> Result<F::Output, io::Error> {
128        Ok(self.runtime.block_on(f))
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::shared_runtime::BlockOnTimeoutError;
136    use async_trait::async_trait;
137    use std::sync::mpsc::{channel, Receiver, Sender};
138    use std::time::Duration;
139    use tokio::time::sleep;
140
141    #[derive(Debug)]
142    struct TestWorker {
143        state: i32,
144        sender: Sender<i32>,
145    }
146
147    fn make_test_worker() -> (TestWorker, Receiver<i32>) {
148        let (sender, receiver) = channel::<i32>();
149        (TestWorker { state: 0, sender }, receiver)
150    }
151
152    #[async_trait]
153    impl Worker for TestWorker {
154        async fn run(&mut self) {
155            let _ = self.sender.send(self.state);
156            self.state += 1;
157        }
158
159        async fn trigger(&mut self) {
160            sleep(Duration::from_millis(100)).await;
161        }
162
163        async fn shutdown(&mut self) {
164            self.state = -1;
165            let _ = self.sender.send(self.state);
166        }
167    }
168
169    fn new_outer_runtime() -> Arc<tokio::runtime::Runtime> {
170        Arc::new(
171            tokio::runtime::Builder::new_multi_thread()
172                .worker_threads(1)
173                .enable_all()
174                .build()
175                .expect("failed to build outer runtime for BasicRuntime test"),
176        )
177    }
178
179    #[test]
180    fn test_new_lib_built_runtime_spawn_worker_runs() {
181        let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
182        let (worker, receiver) = make_test_worker();
183
184        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
185
186        assert_eq!(
187            receiver
188                .recv_timeout(Duration::from_secs(1))
189                .expect("worker did not run on library-built BasicRuntime"),
190            0
191        );
192    }
193
194    #[test]
195    fn test_with_worker_threads_spawn_worker_runs() {
196        let shared_runtime =
197            BasicRuntime::with_worker_threads(2).expect("BasicRuntime::with_worker_threads");
198        let (worker, receiver) = make_test_worker();
199
200        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
201
202        assert_eq!(
203            receiver
204                .recv_timeout(Duration::from_secs(1))
205                .expect("worker did not run on multi-thread BasicRuntime"),
206            0
207        );
208    }
209
210    #[test]
211    fn test_spawn_worker_ignores_restart_on_fork() {
212        let rt = new_outer_runtime();
213        let shared_runtime = BasicRuntime::from_handle(rt);
214        let (worker, receiver) = make_test_worker();
215
216        let _handle = shared_runtime
217            .spawn_worker(worker, true)
218            .expect("restart_on_fork=true should be silently ignored on BasicRuntime");
219
220        assert_eq!(
221            receiver
222                .recv_timeout(Duration::from_secs(1))
223                .expect("worker did not run when restart_on_fork was passed"),
224            0
225        );
226    }
227
228    #[test]
229    fn test_from_handle_spawn_worker_runs() {
230        let rt = new_outer_runtime();
231        let shared_runtime = BasicRuntime::from_handle(rt);
232        let (worker, receiver) = make_test_worker();
233
234        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
235
236        assert_eq!(
237            receiver
238                .recv_timeout(Duration::from_secs(1))
239                .expect("worker did not run on BasicRuntime"),
240            0
241        );
242    }
243
244    #[test]
245    fn test_shutdown_async_stops_workers_only() {
246        let rt = new_outer_runtime();
247        let shared_runtime = BasicRuntime::from_handle(rt.clone());
248        let (worker, receiver) = make_test_worker();
249
250        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
251        receiver
252            .recv_timeout(Duration::from_secs(1))
253            .expect("worker did not run before shutdown");
254
255        rt.block_on(shared_runtime.shutdown_async());
256
257        // The shutdown sentinel from Worker::shutdown.
258        let mut last = receiver
259            .recv_timeout(Duration::from_secs(1))
260            .expect("shutdown did not send a value");
261        while let Ok(v) = receiver.try_recv() {
262            last = v;
263        }
264        assert_eq!(last, -1);
265        assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
266
267        // The outer runtime must still be usable after shutdown_async.
268        rt.block_on(async { sleep(Duration::from_millis(10)).await });
269    }
270
271    #[test]
272    fn test_block_on_with_timeout_completes() {
273        let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
274
275        let value = shared_runtime
276            .block_on_with_timeout(async { 42 }, Duration::from_secs(1))
277            .expect("block_on_with_timeout should complete before the deadline");
278
279        assert_eq!(value, 42);
280    }
281
282    #[test]
283    fn test_block_on_with_timeout_times_out() {
284        let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
285
286        let err = shared_runtime
287            .block_on_with_timeout(
288                async { sleep(Duration::from_secs(60)).await },
289                Duration::from_millis(10),
290            )
291            .expect_err("block_on_with_timeout should time out before the sleep completes");
292
293        assert!(matches!(err, BlockOnTimeoutError::TimedOut(_)));
294    }
295
296    #[test]
297    fn test_block_on_with_timeout_errors_when_timers_disabled() {
298        // `from_handle` accepts any caller-provided runtime, including one built without
299        // `enable_time`. When Tokio polls a timer on such a runtime, it panics instead of
300        // returning an error. The default `block_on_with_timeout` must catch that panic and
301        // report the panic as `BlockOnTimeoutError::Io` instead of letting the panic escape.
302        let rt = Arc::new(
303            tokio::runtime::Builder::new_current_thread()
304                .build()
305                .expect("failed to build a timerless runtime for this test"),
306        );
307        let shared_runtime = BasicRuntime::from_handle(rt);
308
309        let err = shared_runtime
310            .block_on_with_timeout(async { 42 }, Duration::from_secs(1))
311            .expect_err("block_on_with_timeout should error when timers are disabled");
312
313        assert!(matches!(err, BlockOnTimeoutError::Io(_)));
314    }
315
316    #[test]
317    fn test_block_on_with_timeout_errors_when_timers_disabled_multi_thread() {
318        let rt = Arc::new(
319            tokio::runtime::Builder::new_multi_thread()
320                .worker_threads(1)
321                .build()
322                .expect("failed to build a timerless multi-thread runtime for this test"),
323        );
324        let shared_runtime = BasicRuntime::from_handle(rt);
325
326        let err = shared_runtime
327            .block_on_with_timeout(async { 42 }, Duration::from_secs(1))
328            .expect_err("block_on_with_timeout should error when timers are disabled");
329
330        assert!(matches!(err, BlockOnTimeoutError::Io(_)));
331    }
332
333    #[test]
334    fn test_keeps_runtime_alive_after_caller_drops() {
335        let rt = new_outer_runtime();
336        let shared_runtime = BasicRuntime::from_handle(rt.clone());
337        // Caller drops their clone; the Arc inside BasicRuntime
338        // must keep the runtime alive so spawn_worker still works.
339        drop(rt);
340        let (worker, receiver) = make_test_worker();
341
342        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
343
344        assert_eq!(
345            receiver
346                .recv_timeout(Duration::from_secs(1))
347                .expect("worker did not run after caller dropped its runtime clone"),
348            0
349        );
350    }
351}