pub(crate) mod pausable_worker;
#[cfg(not(target_arch = "wasm32"))]
mod basic;
#[cfg(not(target_arch = "wasm32"))]
mod fork_safe;
#[cfg(target_arch = "wasm32")]
mod local;
#[cfg(not(target_arch = "wasm32"))]
pub use basic::BasicRuntime;
#[cfg(not(target_arch = "wasm32"))]
pub use fork_safe::ForkSafeRuntime;
#[cfg(target_arch = "wasm32")]
pub use local::LocalRuntime;
use crate::worker::Worker;
use libdd_capabilities::MaybeSend;
use libdd_common::MutexExt;
use pausable_worker::{PausableWorker, PausableWorkerError};
use std::sync::{Arc, Mutex};
use std::{fmt, io};
pub(crate) type BoxedWorker = Box<dyn Worker + Sync>;
#[derive(Debug)]
pub(crate) struct WorkerEntry {
pub(crate) id: u64,
pub(crate) restart_on_fork: bool,
pub(crate) worker: PausableWorker<BoxedWorker>,
}
pub trait SharedRuntime {
fn new() -> Result<Self, SharedRuntimeError>
where
Self: Sized;
fn spawn_worker<T: Worker + Sync + 'static>(
&self,
worker: T,
restart_on_fork: bool,
) -> Result<WorkerHandle, SharedRuntimeError>;
fn shutdown_async(&self) -> impl std::future::Future<Output = ()> + MaybeSend + '_
where
Self: Sync;
}
#[derive(Debug)]
pub enum BlockOnTimeoutError {
Io(io::Error),
TimedOut(std::time::Duration),
}
impl fmt::Display for BlockOnTimeoutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => write!(f, "Executor error: {}", err),
Self::TimedOut(duration) => write!(f, "Timed out after {:?}", duration),
}
}
}
impl std::error::Error for BlockOnTimeoutError {}
impl From<io::Error> for BlockOnTimeoutError {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}
#[cfg(not(target_arch = "wasm32"))]
fn is_timers_disabled_panic(payload: &(dyn std::any::Any + Send)) -> bool {
if let Some(inner) = payload.downcast_ref::<Box<dyn std::any::Any + Send>>() {
return is_timers_disabled_panic(inner.as_ref());
}
let message = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str));
matches!(message, Some(message) if message.contains("timers are disabled"))
}
#[cfg(not(target_arch = "wasm32"))]
pub trait BlockingRuntime: SharedRuntime {
fn block_on<F: std::future::Future>(&self, f: F) -> Result<F::Output, io::Error>;
fn block_on_with_timeout<F: std::future::Future>(
&self,
f: F,
timeout: std::time::Duration,
) -> Result<F::Output, BlockOnTimeoutError> {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.block_on(async move { tokio::time::timeout(timeout, f).await })
}));
let outcome = match result {
Ok(outcome) => outcome,
Err(payload) if is_timers_disabled_panic(&payload) => {
return Err(BlockOnTimeoutError::Io(io::Error::other(
"block_on_with_timeout requires a runtime with timers enabled",
)));
}
Err(payload) => std::panic::resume_unwind(payload),
};
outcome?.map_err(|_| BlockOnTimeoutError::TimedOut(timeout))
}
}
#[must_use = "dropping a WorkerHandle without calling stop() leaks the worker until the SharedRuntime is shut down"]
#[derive(Clone, Debug)]
pub struct WorkerHandle {
pub(crate) worker_id: u64,
pub(crate) workers: Arc<Mutex<Vec<WorkerEntry>>>,
}
#[derive(Debug)]
pub enum WorkerHandleError {
AlreadyStopped,
WorkerError(PausableWorkerError),
}
impl fmt::Display for WorkerHandleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyStopped => {
write!(f, "Worker has already been stopped")
}
Self::WorkerError(err) => write!(f, "Worker error: {}", err),
}
}
}
impl std::error::Error for WorkerHandleError {}
impl From<PausableWorkerError> for WorkerHandleError {
fn from(err: PausableWorkerError) -> Self {
Self::WorkerError(err)
}
}
impl WorkerHandle {
pub fn set_fork_restart(&self, restart_on_fork: bool) -> Result<(), WorkerHandleError> {
let mut workers_lock = self.workers.lock_or_panic();
let Some(entry) = workers_lock
.iter_mut()
.find(|entry| entry.id == self.worker_id)
else {
return Err(WorkerHandleError::AlreadyStopped);
};
entry.restart_on_fork = restart_on_fork;
Ok(())
}
pub async fn stop(self) -> Result<(), WorkerHandleError> {
let mut worker = {
let mut workers_lock = self.workers.lock_or_panic();
let Some(position) = workers_lock
.iter()
.position(|entry| entry.id == self.worker_id)
else {
return Err(WorkerHandleError::AlreadyStopped);
};
let WorkerEntry { worker, .. } = workers_lock.swap_remove(position);
worker
};
worker.pause().await?;
worker.shutdown().await;
Ok(())
}
}
#[derive(Debug)]
pub enum SharedRuntimeError {
RuntimeUnavailable,
LockFailed(String),
WorkerError(PausableWorkerError),
RuntimeCreation(io::Error),
ShutdownTimedOut(std::time::Duration),
}
impl fmt::Display for SharedRuntimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RuntimeUnavailable => {
write!(f, "Runtime is not available or in an invalid state")
}
Self::LockFailed(msg) => write!(f, "Failed to acquire lock: {}", msg),
Self::WorkerError(err) => write!(f, "Worker error: {}", err),
Self::RuntimeCreation(err) => {
write!(f, "Failed to create runtime: {}", err)
}
Self::ShutdownTimedOut(duration) => {
write!(f, "Shutdown timed out after {:?}", duration)
}
}
}
}
impl std::error::Error for SharedRuntimeError {}
impl From<PausableWorkerError> for SharedRuntimeError {
fn from(err: PausableWorkerError) -> Self {
SharedRuntimeError::WorkerError(err)
}
}
impl From<io::Error> for SharedRuntimeError {
fn from(err: io::Error) -> Self {
SharedRuntimeError::RuntimeCreation(err)
}
}