use core::future::Future;
use std::time::Duration;
use tokio::runtime::{EnterGuard, Handle, Runtime};
#[cfg(feature = "dial9")]
use crate::telemetry::tracing;
#[derive(Clone, Debug)]
pub struct OwnedRuntimeHandle {
tokio: Handle,
#[cfg(feature = "dial9")]
dial9: ::dial9_tokio_telemetry::telemetry::TelemetryHandle,
}
impl OwnedRuntimeHandle {
#[must_use]
pub fn current() -> Self {
Self {
tokio: Handle::current(),
#[cfg(feature = "dial9")]
dial9: ::dial9_tokio_telemetry::telemetry::TelemetryHandle::current(),
}
}
pub(crate) fn tokio_handle(&self) -> &Handle {
&self.tokio
}
pub fn enter(&self) -> tokio::runtime::EnterGuard<'_> {
self.tokio.enter()
}
pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
#[cfg(feature = "dial9")]
{
let _enter = self.tokio.enter();
self.dial9.spawn(future)
}
#[cfg(not(feature = "dial9"))]
{
self.tokio.spawn(future)
}
}
#[expect(
clippy::panic,
reason = "task cancellation is unrecoverable at this infallible blocking boundary"
)]
pub(crate) fn block_on_task<F>(&self, future: F) -> F::Output
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let runtime = self.clone();
self.tokio.block_on(async move {
let task = runtime.spawn(future);
match task.await {
Ok(output) => output,
Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()),
Err(err) => panic!("blocking runtime task was cancelled: {err}"),
}
})
}
}
impl From<Handle> for OwnedRuntimeHandle {
fn from(tokio: Handle) -> Self {
Self {
tokio,
#[cfg(feature = "dial9")]
dial9: ::dial9_tokio_telemetry::telemetry::TelemetryHandle::disabled(),
}
}
}
#[derive(Debug)]
pub struct OwnedRuntime {
inner: RuntimeInner,
}
#[derive(Debug)]
enum RuntimeInner {
Tokio(Runtime),
#[cfg(feature = "dial9")]
Dial9(::dial9_tokio_telemetry::TracedRuntime),
}
impl OwnedRuntime {
#[must_use]
pub fn from_tokio(runtime: Runtime) -> Self {
Self {
inner: RuntimeInner::Tokio(runtime),
}
}
#[cfg(feature = "dial9")]
#[cfg_attr(docsrs, doc(cfg(feature = "dial9")))]
#[must_use]
pub fn from_dial9(runtime: ::dial9_tokio_telemetry::TracedRuntime) -> Self {
Self {
inner: RuntimeInner::Dial9(runtime),
}
}
#[must_use]
pub fn handle(&self) -> OwnedRuntimeHandle {
match &self.inner {
RuntimeInner::Tokio(runtime) => runtime.handle().clone().into(),
#[cfg(feature = "dial9")]
RuntimeInner::Dial9(runtime) => OwnedRuntimeHandle {
tokio: runtime.runtime().handle().clone(),
dial9: runtime.guard().handle(),
},
}
}
pub(crate) fn tokio_runtime(&self) -> &Runtime {
match &self.inner {
RuntimeInner::Tokio(runtime) => runtime,
#[cfg(feature = "dial9")]
RuntimeInner::Dial9(runtime) => runtime.runtime(),
}
}
pub fn enter(&self) -> EnterGuard<'_> {
self.tokio_runtime().enter()
}
pub fn block_on<F>(&self, future: F) -> F::Output
where
F: Future,
{
self.tokio_runtime().block_on(future)
}
pub fn block_on_task<F>(&self, future: F) -> F::Output
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
match &self.inner {
RuntimeInner::Tokio(runtime) => runtime.block_on(future),
#[cfg(feature = "dial9")]
RuntimeInner::Dial9(runtime) => runtime.block_on(future),
}
}
pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
self.handle().spawn(future)
}
pub(crate) fn shutdown(self, grace: Duration) {
self.shutdown_bounded(grace);
}
pub fn shutdown_bounded(self, grace: Duration) {
match self.inner {
RuntimeInner::Tokio(runtime) => runtime.shutdown_timeout(grace),
#[cfg(feature = "dial9")]
RuntimeInner::Dial9(runtime) => {
let (done_tx, done_rx) = std::sync::mpsc::sync_channel(1);
let mut runtime = std::mem::ManuallyDrop::new(runtime);
let spawned = std::thread::Builder::new()
.name("rama-runtime-dispose".to_owned())
.spawn(move || {
drop(unsafe { std::mem::ManuallyDrop::take(&mut runtime) });
_ = done_tx.send(());
});
match spawned {
Ok(thread) => match done_rx.recv_timeout(grace) {
Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
if thread.join().is_err() {
tracing::error!("dial9 runtime dispose thread panicked");
}
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
tracing::warn!(
?grace,
"dial9 runtime shutdown timed out; detaching dispose thread"
);
}
},
Err(err) => {
tracing::error!(
%err,
"failed to spawn dial9 runtime dispose thread; leaking runtime"
);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancelled_blocking_task_has_a_readable_panic() {
let runtime = OwnedRuntime::from_tokio(
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap(),
);
let handle = runtime.handle();
drop(runtime);
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handle.block_on_task(async {});
}))
.unwrap_err();
let message = panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&str>().copied())
.unwrap();
assert!(message.contains("blocking runtime task was cancelled"));
}
#[cfg(feature = "dial9")]
#[test]
fn external_spawn_keeps_its_dial9_session() {
use std::sync::mpsc;
let temp_dir = tempfile::tempdir().unwrap();
let config = ::dial9_tokio_telemetry::Dial9Config::builder()
.enabled(true)
.base_path(temp_dir.path().join("owned-runtime.bin"))
.max_file_size(1024 * 1024)
.max_total_size(4 * 1024 * 1024)
.build()
.unwrap();
let runtime = OwnedRuntime::from_dial9(
::dial9_tokio_telemetry::TracedRuntime::try_new(config).unwrap(),
);
let handle = runtime.handle();
let (tx, rx) = mpsc::sync_channel(1);
std::thread::spawn(move || {
_ = handle.spawn(async move {
tx.send(
::dial9_tokio_telemetry::telemetry::TelemetryHandle::current().is_enabled(),
)
.unwrap();
});
})
.join()
.unwrap();
assert!(rx.recv_timeout(Duration::from_secs(1)).unwrap());
drop(runtime);
}
#[cfg(feature = "dial9")]
#[test]
fn dial9_shutdown_honors_the_grace_period() {
use std::sync::mpsc;
let config = ::dial9_tokio_telemetry::Dial9Config::builder()
.enabled(false)
.build()
.unwrap();
let runtime = OwnedRuntime::from_dial9(
::dial9_tokio_telemetry::TracedRuntime::try_new(config).unwrap(),
);
let (started_tx, started_rx) = mpsc::sync_channel(1);
let (release_tx, release_rx) = mpsc::sync_channel(1);
_ = runtime.spawn(async move {
started_tx.send(()).unwrap();
_ = release_rx.recv();
});
started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
let started = std::time::Instant::now();
runtime.shutdown_bounded(Duration::from_millis(10));
assert!(started.elapsed() < Duration::from_secs(1));
release_tx.send(()).unwrap();
}
}