Skip to main content

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 for managing [`PausableWorker`]s across fork boundaries.
5//!
6//! This module provides a SharedRuntime that manages a tokio runtime and allows
7//! spawning PausableWorkers on it. It also provides hooks for safely handling
8//! fork operations by pausing workers before fork and restarting them appropriately
9//! in parent and child processes.
10
11pub(crate) mod pausable_worker;
12
13use crate::worker::Worker;
14use futures::stream::{FuturesUnordered, StreamExt};
15use libdd_common::MutexExt;
16use pausable_worker::{PausableWorker, PausableWorkerError};
17use std::sync::atomic::AtomicU64;
18use std::sync::{Arc, Mutex};
19use std::{fmt, io};
20use tracing::{debug, error};
21
22/// Native-only runtime management, fork safety, and tokio integration.
23///
24/// Gated once here so individual items inside don't need `#[cfg]`.
25#[cfg(not(target_arch = "wasm32"))]
26mod native {
27    use super::*;
28    use pausable_worker::tokio_spawn_fn;
29    use std::sync::atomic::Ordering;
30    use tokio::runtime::{Builder, Runtime};
31
32    fn build_runtime() -> Result<Runtime, io::Error> {
33        Builder::new_multi_thread()
34            .worker_threads(1)
35            .enable_all()
36            .build()
37    }
38
39    impl SharedRuntime {
40        pub(in super::super) fn new_native() -> Result<Self, SharedRuntimeError> {
41            Ok(Self {
42                runtime: Arc::new(Mutex::new(Some(Arc::new(build_runtime()?)))),
43                workers: Arc::new(Mutex::new(Vec::new())),
44                next_worker_id: AtomicU64::new(1),
45            })
46        }
47
48        /// Returns a clone of the tokio runtime handle managed by this SharedRuntime.
49        ///
50        /// # Errors
51        /// Returns [`SharedRuntimeError::RuntimeUnavailable`] if the runtime has been shut down.
52        pub fn runtime_handle(&self) -> Result<tokio::runtime::Handle, SharedRuntimeError> {
53            Ok(self
54                .runtime
55                .lock_or_panic()
56                .as_ref()
57                .ok_or(SharedRuntimeError::RuntimeUnavailable)?
58                .handle()
59                .clone())
60        }
61
62        /// Spawn a PausableWorker on this runtime.
63        ///
64        /// The worker will be tracked by this SharedRuntime and will be paused/resumed
65        /// during fork operations (native only).
66        /// If `restart_on_fork` is true, the worker will be reset and restarted when calling
67        /// `after_fork_child` else the worker is dropped *without* calling `Worker::shutdown`.
68        ///
69        /// # Errors
70        /// Returns an error if the worker cannot be started.
71        pub fn spawn_worker<T: Worker + Sync + 'static>(
72            &self,
73            worker: T,
74            restart_on_fork: bool,
75        ) -> Result<WorkerHandle, SharedRuntimeError> {
76            let boxed_worker: BoxedWorker = Box::new(worker);
77            debug!(?boxed_worker, "Spawning worker on SharedRuntime");
78            let mut pausable_worker = PausableWorker::new(boxed_worker);
79
80            // Lock runtime first, then workers, following the documented mutex
81            // lock order (matches before_fork). Both guards are held across
82            // start+push so that before_fork cannot interleave between them:
83            // otherwise before_fork could take the runtime, drop it, and miss
84            // our (not-yet-pushed) worker, leaving us with a worker running on
85            // a torn-down runtime that before_fork never paused. If the
86            // runtime has been taken (fork window already passed), we skip
87            // starting; after_fork_parent/child will start the worker on the
88            // new runtime.
89            let runtime_guard = self.runtime.lock_or_panic();
90            let mut workers_guard = self.workers.lock_or_panic();
91
92            if let Some(rt) = runtime_guard.as_ref() {
93                if let Err(e) = pausable_worker.start(tokio_spawn_fn(rt.handle())) {
94                    return Err(e.into());
95                }
96            }
97
98            let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed);
99
100            workers_guard.push(WorkerEntry {
101                id: worker_id,
102                restart_on_fork,
103                worker: pausable_worker,
104            });
105
106            Ok(WorkerHandle {
107                worker_id,
108                workers: self.workers.clone(),
109            })
110        }
111
112        /// Hook to be called before forking.
113        ///
114        /// This method pauses all workers and prepares the runtime for forking.
115        /// It ensures that no background tasks are running when the fork occurs,
116        /// preventing potential deadlocks in the child process.
117        ///
118        /// Worker errors are logged but do not cause the function to fail.
119        /// If the worker fails to pause it is dropped without calling shutdown.
120        pub fn before_fork(&self) {
121            debug!("before_fork: pausing all workers");
122            if let Some(runtime) = self.runtime.lock_or_panic().take() {
123                let mut workers_lock = self.workers.lock_or_panic();
124                runtime.block_on(async {
125                    let futures: FuturesUnordered<_> = workers_lock
126                        .iter_mut()
127                        .map(|worker_entry| async {
128                            if let Err(e) = worker_entry.worker.pause().await {
129                                error!("Worker failed to pause before fork: {:?}", e);
130                            }
131                        })
132                        .collect();
133
134                    futures.collect::<()>().await;
135                });
136            }
137        }
138
139        fn restart_runtime(&self) -> Result<(), SharedRuntimeError> {
140            let mut runtime_lock = self.runtime.lock_or_panic();
141            if runtime_lock.is_none() {
142                *runtime_lock = Some(Arc::new(build_runtime()?));
143            }
144            Ok(())
145        }
146
147        /// Hook to be called in the parent process after forking.
148        ///
149        /// This method restarts workers and resumes normal operation in the parent process.
150        /// The runtime may need to be recreated if it was shut down in before_fork.
151        ///
152        /// # Errors
153        /// Returns an error if workers cannot be restarted or the runtime cannot be recreated.
154        pub fn after_fork_parent(&self) -> Result<(), SharedRuntimeError> {
155            debug!("after_fork_parent: restarting runtime and workers");
156            self.restart_runtime()?;
157
158            let runtime_lock = self.runtime.lock_or_panic();
159            let handle = runtime_lock
160                .as_ref()
161                .ok_or(SharedRuntimeError::RuntimeUnavailable)?
162                .handle()
163                .clone();
164            drop(runtime_lock);
165
166            let mut workers_lock = self.workers.lock_or_panic();
167
168            for worker_entry in workers_lock.iter_mut() {
169                worker_entry.worker.start(tokio_spawn_fn(&handle))?;
170            }
171
172            Ok(())
173        }
174
175        /// Hook to be called in the child process after forking.
176        ///
177        /// This method reinitializes the runtime and workers in the child process.
178        /// A new runtime must be created since tokio runtimes cannot be safely forked.
179        /// Workers are reset and restarted to resume operations in the child.
180        ///
181        /// # Errors
182        /// Returns an error if the runtime cannot be reinitialized or workers cannot be started.
183        pub fn after_fork_child(&self) -> Result<(), SharedRuntimeError> {
184            debug!("after_fork_child: reinitializing runtime and workers");
185            self.restart_runtime()?;
186
187            let runtime_lock = self.runtime.lock_or_panic();
188            let handle = runtime_lock
189                .as_ref()
190                .ok_or(SharedRuntimeError::RuntimeUnavailable)?
191                .handle()
192                .clone();
193            drop(runtime_lock);
194
195            let mut workers_lock = self.workers.lock_or_panic();
196
197            workers_lock.retain(|entry| entry.restart_on_fork);
198
199            for worker_entry in workers_lock.iter_mut() {
200                worker_entry.worker.reset();
201                worker_entry.worker.start(tokio_spawn_fn(&handle))?;
202            }
203
204            Ok(())
205        }
206
207        /// Run a future to completion on the shared runtime, blocking the current thread.
208        ///
209        /// If the runtime is not available (e.g. after calling before_fork), a temporary
210        /// single-threaded runtime is used.
211        ///
212        /// Not available on wasm32 -- use async paths instead.
213        ///
214        /// # Errors
215        /// Returns an error if it fails to create a fallback runtime.
216        pub fn block_on<F: std::future::Future>(&self, f: F) -> Result<F::Output, io::Error> {
217            let runtime = match self.runtime.lock_or_panic().as_ref() {
218                None => Arc::new(Builder::new_current_thread().enable_all().build()?),
219                Some(runtime) => runtime.clone(),
220            };
221            Ok(runtime.block_on(f))
222        }
223
224        /// Shutdown the runtime and all workers synchronously with optional timeout.
225        ///
226        /// Not available on wasm32 -- use [`shutdown_async`](Self::shutdown_async) instead.
227        ///
228        /// Worker errors are logged but do not cause the function to fail.
229        ///
230        /// # Errors
231        /// Returns an error only if shutdown times out.
232        pub fn shutdown(
233            &self,
234            timeout: Option<std::time::Duration>,
235        ) -> Result<(), SharedRuntimeError> {
236            debug!(?timeout, "Shutting down SharedRuntime");
237            match self.runtime.lock_or_panic().take() {
238                Some(runtime) => {
239                    if let Some(timeout) = timeout {
240                        match runtime.block_on(async {
241                            tokio::time::timeout(timeout, self.shutdown_async()).await
242                        }) {
243                            Ok(()) => Ok(()),
244                            Err(_) => Err(SharedRuntimeError::ShutdownTimedOut(timeout)),
245                        }
246                    } else {
247                        runtime.block_on(self.shutdown_async());
248                        Ok(())
249                    }
250                }
251                None => Ok(()),
252            }
253        }
254    }
255}
256
257type BoxedWorker = Box<dyn Worker + Sync>;
258
259#[derive(Debug)]
260struct WorkerEntry {
261    id: u64,
262    restart_on_fork: bool,
263    worker: PausableWorker<BoxedWorker>,
264}
265
266/// Handle to a worker registered on a [`SharedRuntime`].
267///
268/// This handle can be used to stop the worker.
269///
270/// # Warning
271/// If every clone of this handle is dropped without calling [`WorkerHandle::stop`], the worker
272/// remains registered on the [`SharedRuntime`] and can only be torn down by shutting the
273/// runtime down. Workers are expected to detect that their input channel has been closed and
274/// park themselves to avoid spinning, but they will not be freed until the runtime stops.
275#[must_use = "dropping a WorkerHandle without calling stop() leaks the worker until the SharedRuntime is shut down"]
276#[derive(Clone, Debug)]
277pub struct WorkerHandle {
278    worker_id: u64,
279    workers: Arc<Mutex<Vec<WorkerEntry>>>,
280}
281
282#[derive(Debug)]
283pub enum WorkerHandleError {
284    AlreadyStopped,
285    WorkerError(PausableWorkerError),
286}
287
288impl fmt::Display for WorkerHandleError {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        match self {
291            Self::AlreadyStopped => {
292                write!(f, "Worker has already been stopped")
293            }
294            Self::WorkerError(err) => write!(f, "Worker error: {}", err),
295        }
296    }
297}
298
299impl std::error::Error for WorkerHandleError {}
300
301impl From<PausableWorkerError> for WorkerHandleError {
302    fn from(err: PausableWorkerError) -> Self {
303        Self::WorkerError(err)
304    }
305}
306
307impl WorkerHandle {
308    /// Stop the worker and execute the shutdown logic.
309    ///
310    /// # Errors
311    /// Returns an error if the worker has already been stopped.
312    ///
313    /// # Cancel safety
314    /// This function is *NOT* cancel safe and shouldn't be called in [Worker::trigger].
315    /// If cancelled, the stopped worker can end up in an invalid state if a fork occurs while
316    /// stopping.
317    pub async fn stop(self) -> Result<(), WorkerHandleError> {
318        let mut worker = {
319            let mut workers_lock = self.workers.lock_or_panic();
320            let Some(position) = workers_lock
321                .iter()
322                .position(|entry| entry.id == self.worker_id)
323            else {
324                return Err(WorkerHandleError::AlreadyStopped);
325            };
326            let WorkerEntry { worker, .. } = workers_lock.swap_remove(position);
327            worker
328        };
329        worker.pause().await?;
330        worker.shutdown().await;
331        Ok(())
332    }
333}
334
335/// Errors that can occur when using SharedRuntime.
336#[derive(Debug)]
337pub enum SharedRuntimeError {
338    /// The runtime is not available or in an invalid state.
339    RuntimeUnavailable,
340    /// Failed to acquire a lock on internal state.
341    LockFailed(String),
342    /// A worker operation failed.
343    WorkerError(PausableWorkerError),
344    /// Failed to create the tokio runtime.
345    RuntimeCreation(io::Error),
346    /// Shutdown timed out.
347    ShutdownTimedOut(std::time::Duration),
348}
349
350impl fmt::Display for SharedRuntimeError {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        match self {
353            Self::RuntimeUnavailable => {
354                write!(f, "Runtime is not available or in an invalid state")
355            }
356            Self::LockFailed(msg) => write!(f, "Failed to acquire lock: {}", msg),
357            Self::WorkerError(err) => write!(f, "Worker error: {}", err),
358            Self::RuntimeCreation(err) => {
359                write!(f, "Failed to create runtime: {}", err)
360            }
361            Self::ShutdownTimedOut(duration) => {
362                write!(f, "Shutdown timed out after {:?}", duration)
363            }
364        }
365    }
366}
367
368impl std::error::Error for SharedRuntimeError {}
369
370impl From<PausableWorkerError> for SharedRuntimeError {
371    fn from(err: PausableWorkerError) -> Self {
372        SharedRuntimeError::WorkerError(err)
373    }
374}
375
376impl From<io::Error> for SharedRuntimeError {
377    fn from(err: io::Error) -> Self {
378        SharedRuntimeError::RuntimeCreation(err)
379    }
380}
381
382/// A shared runtime that manages PausableWorkers and provides fork safety hooks.
383///
384/// The SharedRuntime owns a tokio runtime (on native) and tracks PausableWorkers
385/// spawned on it. It provides methods to safely pause workers before forking and
386/// restart them after fork in both parent and child processes.
387///
388/// On wasm32, no tokio runtime is created. Workers are spawned via `spawn_local`
389/// on the JS event loop.
390///
391/// # Mutex lock order
392/// When locking both [Self::runtime] and [Self::workers], the mutex must be locked in the order of
393/// the fields in the struct. When possible avoid holding both locks simultaneously.
394#[derive(Debug)]
395pub struct SharedRuntime {
396    #[cfg(not(target_arch = "wasm32"))]
397    runtime: Arc<Mutex<Option<Arc<tokio::runtime::Runtime>>>>,
398    workers: Arc<Mutex<Vec<WorkerEntry>>>,
399    next_worker_id: AtomicU64,
400}
401
402impl SharedRuntime {
403    /// Create a new SharedRuntime.
404    ///
405    /// On native, this creates a tokio multi-thread runtime. On wasm32, no runtime
406    /// is created (workers are spawned on the JS event loop via `spawn_local`).
407    ///
408    /// # Errors
409    /// Returns an error if the tokio runtime cannot be created (native only).
410    pub fn new() -> Result<Self, SharedRuntimeError> {
411        debug!("Creating new SharedRuntime");
412
413        #[cfg(not(target_arch = "wasm32"))]
414        {
415            Self::new_native()
416        }
417        #[cfg(target_arch = "wasm32")]
418        {
419            Ok(Self {
420                workers: Arc::new(Mutex::new(Vec::new())),
421                next_worker_id: AtomicU64::new(1),
422            })
423        }
424    }
425
426    /// Spawn a PausableWorker on the JS event loop (wasm variant).
427    #[cfg(target_arch = "wasm32")]
428    pub fn spawn_worker<T: Worker + Sync + 'static>(
429        &self,
430        worker: T,
431        restart_on_fork: bool,
432    ) -> Result<WorkerHandle, SharedRuntimeError> {
433        use std::sync::atomic::Ordering;
434
435        let boxed_worker: BoxedWorker = Box::new(worker);
436        debug!(?boxed_worker, "Spawning worker on SharedRuntime");
437        let mut pausable_worker = PausableWorker::new(boxed_worker);
438
439        let mut workers_guard = self.workers.lock_or_panic();
440
441        if let Err(e) = pausable_worker.start(|future| {
442            use futures_util::FutureExt;
443            let (remote, handle) = future.remote_handle();
444            wasm_bindgen_futures::spawn_local(remote);
445            Box::pin(async { Ok(handle.await) })
446        }) {
447            return Err(e.into());
448        }
449
450        let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed);
451
452        workers_guard.push(WorkerEntry {
453            id: worker_id,
454            restart_on_fork,
455            worker: pausable_worker,
456        });
457
458        Ok(WorkerHandle {
459            worker_id,
460            workers: self.workers.clone(),
461        })
462    }
463
464    /// Shutdown all workers asynchronously.
465    ///
466    /// This should be called during application shutdown to cleanly stop all
467    /// background workers and the runtime.
468    ///
469    /// Worker errors are logged but do not cause the function to fail.
470    pub async fn shutdown_async(&self) {
471        debug!("Shutting down all workers asynchronously");
472        let workers = {
473            let mut workers_lock = self.workers.lock_or_panic();
474            std::mem::take(&mut *workers_lock)
475        };
476
477        let futures: FuturesUnordered<_> = workers
478            .into_iter()
479            .map(|mut worker_entry| async move {
480                if let Err(e) = worker_entry.worker.pause().await {
481                    error!("Worker failed to shutdown: {:?}", e);
482                    return;
483                }
484                worker_entry.worker.shutdown().await;
485            })
486            .collect();
487
488        futures.collect::<()>().await;
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use async_trait::async_trait;
496    use std::sync::mpsc::{channel, Receiver, Sender};
497    use std::time::Duration;
498    use tokio::time::sleep;
499
500    #[derive(Debug)]
501    struct TestWorker {
502        state: i32,
503        sender: Sender<i32>,
504    }
505
506    fn make_test_worker() -> (TestWorker, Receiver<i32>) {
507        let (sender, receiver) = channel::<i32>();
508        (TestWorker { state: 0, sender }, receiver)
509    }
510
511    #[async_trait]
512    impl Worker for TestWorker {
513        async fn run(&mut self) {
514            let _ = self.sender.send(self.state);
515            self.state += 1;
516        }
517
518        async fn trigger(&mut self) {
519            sleep(Duration::from_millis(100)).await;
520        }
521
522        fn reset(&mut self) {
523            self.state = 0;
524        }
525
526        async fn shutdown(&mut self) {
527            self.state = -1;
528            let _ = self.sender.send(self.state);
529        }
530    }
531
532    #[test]
533    fn test_shared_runtime_creation() {
534        let shared_runtime = SharedRuntime::new();
535        assert!(shared_runtime.is_ok());
536    }
537
538    #[test]
539    fn test_spawn_worker() {
540        let shared_runtime = SharedRuntime::new().unwrap();
541        let (worker, receiver) = make_test_worker();
542
543        let result = shared_runtime.spawn_worker(worker, true);
544        assert!(result.is_ok());
545        assert_eq!(shared_runtime.workers.lock_or_panic().len(), 1);
546
547        // Verify the worker is actually running by receiving its first output
548        assert_eq!(
549            receiver
550                .recv_timeout(Duration::from_secs(1))
551                .expect("worker did not run"),
552            0
553        );
554    }
555
556    #[test]
557    fn test_worker_handle_stop() {
558        let rt = tokio::runtime::Runtime::new().unwrap();
559        let shared_runtime = SharedRuntime::new().unwrap();
560        let (worker, receiver) = make_test_worker();
561
562        let handle = shared_runtime.spawn_worker(worker, true).unwrap();
563        assert_eq!(shared_runtime.workers.lock_or_panic().len(), 1);
564
565        // Wait for at least one run before stopping
566        receiver
567            .recv_timeout(Duration::from_secs(1))
568            .expect("worker did not run");
569
570        rt.block_on(async {
571            assert!(handle.stop().await.is_ok());
572        });
573
574        assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
575
576        // Drain all messages after stop — the last one must be the shutdown sentinel
577        let mut last = receiver
578            .recv_timeout(Duration::from_secs(1))
579            .expect("shutdown did not send a value");
580        while let Ok(v) = receiver.try_recv() {
581            last = v;
582        }
583        assert_eq!(last, -1);
584    }
585
586    #[test]
587    fn test_before_and_after_fork_parent() {
588        let shared_runtime = SharedRuntime::new().unwrap();
589        let (worker, receiver) = make_test_worker();
590
591        let _ = shared_runtime.spawn_worker(worker, true).unwrap();
592
593        // Let the worker run until state > 0 so that preservation is observable
594        let mut state_before_fork = 0;
595        while state_before_fork == 0 {
596            state_before_fork = receiver
597                .recv_timeout(Duration::from_secs(1))
598                .expect("worker did not advance state before fork");
599        }
600
601        shared_runtime.before_fork();
602        // Drain pre-fork buffered messages now that the worker is paused
603        while receiver.try_recv().is_ok() {}
604
605        assert!(shared_runtime.after_fork_parent().is_ok());
606
607        // State must be preserved (not reset) after fork in the parent
608        let after_fork_value = receiver
609            .recv_timeout(Duration::from_secs(1))
610            .expect("worker did not resume after fork");
611        assert!(
612            after_fork_value > state_before_fork,
613            "after_fork_parent should preserve state: got {after_fork_value}, expected > {state_before_fork}"
614        );
615    }
616
617    #[test]
618    fn test_after_fork_child() {
619        let shared_runtime = SharedRuntime::new().unwrap();
620        let (worker, receiver) = make_test_worker();
621
622        let _ = shared_runtime.spawn_worker(worker, true).unwrap();
623
624        // Let the worker run until state > 0 so that the reset is observable
625        let mut state_before_fork = 0;
626        while state_before_fork == 0 {
627            state_before_fork = receiver
628                .recv_timeout(Duration::from_secs(1))
629                .expect("worker did not advance state before fork");
630        }
631
632        shared_runtime.before_fork();
633        // Drain pre-fork buffered messages now that the worker is paused
634        while receiver.try_recv().is_ok() {}
635
636        assert!(shared_runtime.after_fork_child().is_ok());
637
638        // State must be reset to 0 in the child
639        let after_fork_value = receiver
640            .recv_timeout(Duration::from_secs(1))
641            .expect("worker did not resume after fork child");
642        assert_eq!(
643            after_fork_value, 0,
644            "after_fork_child should reset state to 0, got {after_fork_value}"
645        );
646    }
647
648    #[test]
649    fn test_shutdown() {
650        let shared_runtime = SharedRuntime::new().unwrap();
651        let (worker, receiver) = make_test_worker();
652
653        let _ = shared_runtime.spawn_worker(worker, true).unwrap();
654
655        // Wait for at least one run before shutting down
656        receiver
657            .recv_timeout(Duration::from_secs(1))
658            .expect("worker did not run");
659
660        shared_runtime.shutdown(None).unwrap();
661
662        // Drain all messages after shutdown — the last one must be the shutdown sentinel
663        let mut last = receiver
664            .recv_timeout(Duration::from_secs(1))
665            .expect("shutdown did not send a value");
666        while let Ok(v) = receiver.try_recv() {
667            last = v;
668        }
669        assert_eq!(last, -1);
670    }
671
672    #[test]
673    fn test_after_fork_child_drops_worker_not_restart_on_fork() {
674        let shared_runtime = SharedRuntime::new().unwrap();
675        let (worker, receiver) = make_test_worker();
676
677        let _ = shared_runtime.spawn_worker(worker, false).unwrap();
678
679        // Wait for the worker to run at least once
680        receiver
681            .recv_timeout(Duration::from_secs(1))
682            .expect("worker did not run");
683
684        shared_runtime.before_fork();
685        // Drain buffered messages now that the worker is paused
686        while receiver.try_recv().is_ok() {}
687
688        assert!(shared_runtime.after_fork_child().is_ok());
689
690        // Worker must be removed from the list
691        assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
692
693        // Worker must not produce any more messages (not restarted, not shut down)
694        assert!(
695            receiver.recv_timeout(Duration::from_millis(200)).is_err(),
696            "worker should not run or shut down after fork in child when restart_on_fork is false"
697        );
698    }
699}