use super::pausable_worker::{tokio_spawn_fn, PausableWorker};
use super::{
BlockingRuntime, BoxedWorker, SharedRuntime, SharedRuntimeError, WorkerEntry, WorkerHandle,
};
use crate::worker::Worker;
use futures::stream::{FuturesUnordered, StreamExt};
use libdd_common::MutexExt;
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use tracing::{debug, error, warn};
#[derive(Debug)]
pub struct BasicRuntime {
runtime: Arc<tokio::runtime::Runtime>,
workers: Arc<Mutex<Vec<WorkerEntry>>>,
next_worker_id: AtomicU64,
}
impl BasicRuntime {
pub fn with_worker_threads(worker_threads: usize) -> Result<Self, SharedRuntimeError> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(worker_threads)
.enable_all()
.build()?;
Ok(Self::from_handle(Arc::new(runtime)))
}
pub fn from_handle(runtime: Arc<tokio::runtime::Runtime>) -> Self {
Self {
runtime,
workers: Arc::new(Mutex::new(Vec::new())),
next_worker_id: AtomicU64::new(1),
}
}
fn push_worker(
&self,
workers_guard: &mut std::sync::MutexGuard<Vec<WorkerEntry>>,
pausable_worker: PausableWorker<BoxedWorker>,
) -> WorkerHandle {
let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed);
workers_guard.push(WorkerEntry {
id: worker_id,
restart_on_fork: false,
worker: pausable_worker,
});
WorkerHandle {
worker_id,
workers: self.workers.clone(),
}
}
}
impl SharedRuntime for BasicRuntime {
fn new() -> Result<Self, SharedRuntimeError> {
Self::with_worker_threads(1)
}
fn spawn_worker<T: Worker + Sync + 'static>(
&self,
worker: T,
restart_on_fork: bool,
) -> Result<WorkerHandle, SharedRuntimeError> {
if restart_on_fork {
warn!(
"restart_on_fork is ignored on BasicRuntime: regular mode is not fork-safe; \
use ForkSafeRuntime if you need fork hooks"
);
}
let boxed_worker: BoxedWorker = Box::new(worker);
debug!(?boxed_worker, "Spawning worker on BasicRuntime");
let mut pausable_worker = PausableWorker::new(boxed_worker);
let mut workers_guard = self.workers.lock_or_panic();
pausable_worker.start(tokio_spawn_fn(self.runtime.handle()))?;
Ok(self.push_worker(&mut workers_guard, pausable_worker))
}
async fn shutdown_async(&self) {
debug!("Shutting down all workers on BasicRuntime");
let workers = {
let mut workers_lock = self.workers.lock_or_panic();
std::mem::take(&mut *workers_lock)
};
let futures: FuturesUnordered<_> = workers
.into_iter()
.map(|mut worker_entry| async move {
if let Err(e) = worker_entry.worker.pause().await {
error!("Worker failed to shutdown: {:?}", e);
return;
}
worker_entry.worker.shutdown().await;
})
.collect();
futures.collect::<()>().await;
}
}
impl BlockingRuntime for BasicRuntime {
fn block_on<F: std::future::Future>(&self, f: F) -> Result<F::Output, io::Error> {
Ok(self.runtime.block_on(f))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared_runtime::BlockOnTimeoutError;
use async_trait::async_trait;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug)]
struct TestWorker {
state: i32,
sender: Sender<i32>,
}
fn make_test_worker() -> (TestWorker, Receiver<i32>) {
let (sender, receiver) = channel::<i32>();
(TestWorker { state: 0, sender }, receiver)
}
#[async_trait]
impl Worker for TestWorker {
async fn run(&mut self) {
let _ = self.sender.send(self.state);
self.state += 1;
}
async fn trigger(&mut self) {
sleep(Duration::from_millis(100)).await;
}
async fn shutdown(&mut self) {
self.state = -1;
let _ = self.sender.send(self.state);
}
}
fn new_outer_runtime() -> Arc<tokio::runtime::Runtime> {
Arc::new(
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("failed to build outer runtime for BasicRuntime test"),
)
}
#[test]
fn test_new_lib_built_runtime_spawn_worker_runs() {
let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
let (worker, receiver) = make_test_worker();
let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
assert_eq!(
receiver
.recv_timeout(Duration::from_secs(1))
.expect("worker did not run on library-built BasicRuntime"),
0
);
}
#[test]
fn test_with_worker_threads_spawn_worker_runs() {
let shared_runtime =
BasicRuntime::with_worker_threads(2).expect("BasicRuntime::with_worker_threads");
let (worker, receiver) = make_test_worker();
let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
assert_eq!(
receiver
.recv_timeout(Duration::from_secs(1))
.expect("worker did not run on multi-thread BasicRuntime"),
0
);
}
#[test]
fn test_spawn_worker_ignores_restart_on_fork() {
let rt = new_outer_runtime();
let shared_runtime = BasicRuntime::from_handle(rt);
let (worker, receiver) = make_test_worker();
let _handle = shared_runtime
.spawn_worker(worker, true)
.expect("restart_on_fork=true should be silently ignored on BasicRuntime");
assert_eq!(
receiver
.recv_timeout(Duration::from_secs(1))
.expect("worker did not run when restart_on_fork was passed"),
0
);
}
#[test]
fn test_from_handle_spawn_worker_runs() {
let rt = new_outer_runtime();
let shared_runtime = BasicRuntime::from_handle(rt);
let (worker, receiver) = make_test_worker();
let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
assert_eq!(
receiver
.recv_timeout(Duration::from_secs(1))
.expect("worker did not run on BasicRuntime"),
0
);
}
#[test]
fn test_shutdown_async_stops_workers_only() {
let rt = new_outer_runtime();
let shared_runtime = BasicRuntime::from_handle(rt.clone());
let (worker, receiver) = make_test_worker();
let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
receiver
.recv_timeout(Duration::from_secs(1))
.expect("worker did not run before shutdown");
rt.block_on(shared_runtime.shutdown_async());
let mut last = receiver
.recv_timeout(Duration::from_secs(1))
.expect("shutdown did not send a value");
while let Ok(v) = receiver.try_recv() {
last = v;
}
assert_eq!(last, -1);
assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
rt.block_on(async { sleep(Duration::from_millis(10)).await });
}
#[test]
fn test_block_on_with_timeout_completes() {
let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
let value = shared_runtime
.block_on_with_timeout(async { 42 }, Duration::from_secs(1))
.expect("block_on_with_timeout should complete before the deadline");
assert_eq!(value, 42);
}
#[test]
fn test_block_on_with_timeout_times_out() {
let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
let err = shared_runtime
.block_on_with_timeout(
async { sleep(Duration::from_secs(60)).await },
Duration::from_millis(10),
)
.expect_err("block_on_with_timeout should time out before the sleep completes");
assert!(matches!(err, BlockOnTimeoutError::TimedOut(_)));
}
#[test]
fn test_block_on_with_timeout_errors_when_timers_disabled() {
let rt = Arc::new(
tokio::runtime::Builder::new_current_thread()
.build()
.expect("failed to build a timerless runtime for this test"),
);
let shared_runtime = BasicRuntime::from_handle(rt);
let err = shared_runtime
.block_on_with_timeout(async { 42 }, Duration::from_secs(1))
.expect_err("block_on_with_timeout should error when timers are disabled");
assert!(matches!(err, BlockOnTimeoutError::Io(_)));
}
#[test]
fn test_block_on_with_timeout_errors_when_timers_disabled_multi_thread() {
let rt = Arc::new(
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.build()
.expect("failed to build a timerless multi-thread runtime for this test"),
);
let shared_runtime = BasicRuntime::from_handle(rt);
let err = shared_runtime
.block_on_with_timeout(async { 42 }, Duration::from_secs(1))
.expect_err("block_on_with_timeout should error when timers are disabled");
assert!(matches!(err, BlockOnTimeoutError::Io(_)));
}
#[test]
fn test_keeps_runtime_alive_after_caller_drops() {
let rt = new_outer_runtime();
let shared_runtime = BasicRuntime::from_handle(rt.clone());
drop(rt);
let (worker, receiver) = make_test_worker();
let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
assert_eq!(
receiver
.recv_timeout(Duration::from_secs(1))
.expect("worker did not run after caller dropped its runtime clone"),
0
);
}
}