Skip to main content

dialcache/
runtime.rs

1//! Detached task admission and timers.
2
3use std::time::Duration;
4
5use futures::future::BoxFuture;
6
7/// Runs detached work the cache does not await and supplies its timers.
8///
9/// Detached work is raw source or adapter work that outlives a caller's
10/// deadline, and shadow validation jobs. Timers deliver deadlines; a
11/// controlled runtime may deliver them on its own schedule.
12pub trait Runtime: Send + Sync + 'static {
13    /// Start `task` concurrently with the caller.
14    fn spawn(&self, task: BoxFuture<'static, ()>);
15    /// Start `task` after work that is already runnable has progressed.
16    ///
17    /// Production runtimes treat this like [`Runtime::spawn`]. A controlled
18    /// test runtime runs deferred work only once everything else is blocked.
19    fn defer(&self, task: BoxFuture<'static, ()>) {
20        self.spawn(task)
21    }
22    /// Admit synchronous CPU work without blocking the async executor.
23    ///
24    /// The default uses a process-wide pool of two threads and two queued jobs.
25    /// A full queue or thread-creation failure returns an error; cache plumbing
26    /// fails open. Once admitted, a job owns its resources until execution or
27    /// destruction, independently of the future waiting for its result.
28    /// Custom runtimes may override this with their own bounded CPU executor.
29    /// A controlled test runtime may queue the job for deterministic execution.
30    fn spawn_blocking(
31        &self,
32        task: Box<dyn FnOnce() + Send + 'static>,
33    ) -> Result<(), crate::BoxError> {
34        crate::blocking::submit(task)
35    }
36    /// A timer that completes once `duration` has passed.
37    fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()>;
38}
39
40/// Spawns onto one tokio runtime, captured as a handle at construction, and
41/// uses that runtime's timers.
42///
43/// The default instance is [`TokioRuntime::current`], taken while the cache is
44/// built; [`DialCacheBuilder::build`](crate::DialCacheBuilder::build) fails
45/// with a configuration error outside a tokio context instead of panicking on
46/// first use. The runtime must have its time driver enabled (tokio's
47/// `enable_time` or `enable_all`), or the first deadline panics inside the
48/// detached work and surfaces as [`Error::Panic`](crate::Error::Panic).
49#[cfg(feature = "tokio")]
50#[derive(Debug, Clone)]
51pub struct TokioRuntime {
52    handle: tokio::runtime::Handle,
53}
54
55#[cfg(feature = "tokio")]
56impl TokioRuntime {
57    /// The runtime of the current tokio context.
58    pub fn current() -> Result<Self, crate::error::ConfigError> {
59        tokio::runtime::Handle::try_current()
60            .map(Self::from_handle)
61            .map_err(|_| {
62                crate::error::ConfigError::invalid(
63                    "DialCache requires a tokio runtime: build the cache inside one, or \
64                     configure DialCacheBuilder::runtime with TokioRuntime::from_handle",
65                )
66            })
67    }
68
69    /// Use the runtime behind `handle`, whichever context later calls the cache.
70    pub fn from_handle(handle: tokio::runtime::Handle) -> Self {
71        TokioRuntime { handle }
72    }
73}
74
75#[cfg(feature = "tokio")]
76impl Runtime for TokioRuntime {
77    fn spawn(&self, task: BoxFuture<'static, ()>) {
78        self.handle.spawn(task);
79    }
80
81    fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()> {
82        // Enter the captured runtime so the timer binds to its time driver
83        // rather than to whichever runtime happens to call the cache.
84        let _enter = self.handle.enter();
85        Box::pin(tokio::time::sleep(duration))
86    }
87}