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}