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 async_trait::async_trait;
136    use std::sync::mpsc::{channel, Receiver, Sender};
137    use std::time::Duration;
138    use tokio::time::sleep;
139
140    #[derive(Debug)]
141    struct TestWorker {
142        state: i32,
143        sender: Sender<i32>,
144    }
145
146    fn make_test_worker() -> (TestWorker, Receiver<i32>) {
147        let (sender, receiver) = channel::<i32>();
148        (TestWorker { state: 0, sender }, receiver)
149    }
150
151    #[async_trait]
152    impl Worker for TestWorker {
153        async fn run(&mut self) {
154            let _ = self.sender.send(self.state);
155            self.state += 1;
156        }
157
158        async fn trigger(&mut self) {
159            sleep(Duration::from_millis(100)).await;
160        }
161
162        async fn shutdown(&mut self) {
163            self.state = -1;
164            let _ = self.sender.send(self.state);
165        }
166    }
167
168    fn new_outer_runtime() -> Arc<tokio::runtime::Runtime> {
169        Arc::new(
170            tokio::runtime::Builder::new_multi_thread()
171                .worker_threads(1)
172                .enable_all()
173                .build()
174                .expect("failed to build outer runtime for BasicRuntime test"),
175        )
176    }
177
178    #[test]
179    fn test_new_lib_built_runtime_spawn_worker_runs() {
180        let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
181        let (worker, receiver) = make_test_worker();
182
183        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
184
185        assert_eq!(
186            receiver
187                .recv_timeout(Duration::from_secs(1))
188                .expect("worker did not run on library-built BasicRuntime"),
189            0
190        );
191    }
192
193    #[test]
194    fn test_with_worker_threads_spawn_worker_runs() {
195        let shared_runtime =
196            BasicRuntime::with_worker_threads(2).expect("BasicRuntime::with_worker_threads");
197        let (worker, receiver) = make_test_worker();
198
199        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
200
201        assert_eq!(
202            receiver
203                .recv_timeout(Duration::from_secs(1))
204                .expect("worker did not run on multi-thread BasicRuntime"),
205            0
206        );
207    }
208
209    #[test]
210    fn test_spawn_worker_ignores_restart_on_fork() {
211        let rt = new_outer_runtime();
212        let shared_runtime = BasicRuntime::from_handle(rt);
213        let (worker, receiver) = make_test_worker();
214
215        let _handle = shared_runtime
216            .spawn_worker(worker, true)
217            .expect("restart_on_fork=true should be silently ignored on BasicRuntime");
218
219        assert_eq!(
220            receiver
221                .recv_timeout(Duration::from_secs(1))
222                .expect("worker did not run when restart_on_fork was passed"),
223            0
224        );
225    }
226
227    #[test]
228    fn test_from_handle_spawn_worker_runs() {
229        let rt = new_outer_runtime();
230        let shared_runtime = BasicRuntime::from_handle(rt);
231        let (worker, receiver) = make_test_worker();
232
233        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
234
235        assert_eq!(
236            receiver
237                .recv_timeout(Duration::from_secs(1))
238                .expect("worker did not run on BasicRuntime"),
239            0
240        );
241    }
242
243    #[test]
244    fn test_shutdown_async_stops_workers_only() {
245        let rt = new_outer_runtime();
246        let shared_runtime = BasicRuntime::from_handle(rt.clone());
247        let (worker, receiver) = make_test_worker();
248
249        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
250        receiver
251            .recv_timeout(Duration::from_secs(1))
252            .expect("worker did not run before shutdown");
253
254        rt.block_on(shared_runtime.shutdown_async());
255
256        // The shutdown sentinel from Worker::shutdown.
257        let mut last = receiver
258            .recv_timeout(Duration::from_secs(1))
259            .expect("shutdown did not send a value");
260        while let Ok(v) = receiver.try_recv() {
261            last = v;
262        }
263        assert_eq!(last, -1);
264        assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
265
266        // The outer runtime must still be usable after shutdown_async.
267        rt.block_on(async { sleep(Duration::from_millis(10)).await });
268    }
269
270    #[test]
271    fn test_keeps_runtime_alive_after_caller_drops() {
272        let rt = new_outer_runtime();
273        let shared_runtime = BasicRuntime::from_handle(rt.clone());
274        // Caller drops their clone; the Arc inside BasicRuntime
275        // must keep the runtime alive so spawn_worker still works.
276        drop(rt);
277        let (worker, receiver) = make_test_worker();
278
279        let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
280
281        assert_eq!(
282            receiver
283                .recv_timeout(Duration::from_secs(1))
284                .expect("worker did not run after caller dropped its runtime clone"),
285            0
286        );
287    }
288}