libdd_shared_runtime/shared_runtime/
mod.rs1pub(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#[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 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 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 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 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 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 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 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 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#[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 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#[derive(Debug)]
337pub enum SharedRuntimeError {
338 RuntimeUnavailable,
340 LockFailed(String),
342 WorkerError(PausableWorkerError),
344 RuntimeCreation(io::Error),
346 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#[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 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 #[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 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 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 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 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 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 while receiver.try_recv().is_ok() {}
604
605 assert!(shared_runtime.after_fork_parent().is_ok());
606
607 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 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 while receiver.try_recv().is_ok() {}
635
636 assert!(shared_runtime.after_fork_child().is_ok());
637
638 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 receiver
657 .recv_timeout(Duration::from_secs(1))
658 .expect("worker did not run");
659
660 shared_runtime.shutdown(None).unwrap();
661
662 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 receiver
681 .recv_timeout(Duration::from_secs(1))
682 .expect("worker did not run");
683
684 shared_runtime.before_fork();
685 while receiver.try_recv().is_ok() {}
687
688 assert!(shared_runtime.after_fork_child().is_ok());
689
690 assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
692
693 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}