base_scheduler/clock.rs
1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::{Cancellation, OperationResult, TimeoutError};
5use std::{
6 future::{Future, poll_fn},
7 io,
8 pin::pin,
9 task::Poll,
10 time::{Duration, Instant},
11};
12
13/// Monotonic time and cooperative waits in one scheduler's clock domain.
14///
15/// Obtain current time through this trait rather than `Instant::now()` or `Instant::elapsed()` so simulated and paused clocks remain substitutable. Deadlines from independently advancing clock domains are not interchangeable.
16pub trait Clock: Send + Sync {
17 /// Observe this scheduler's monotonic, nondecreasing clock.
18 fn now(&self) -> Instant;
19
20 /// Wait until an absolute deadline, or until cancellation is observed.
21 ///
22 /// Futures are lazy. Pre-cancelled waits return cancellation without registering a timer, including for elapsed deadlines. Otherwise an elapsed deadline can complete immediately; no scheduler yield is promised. Once waiting, expiry wins if expiry and cancellation are observed together. Dropping the future releases its timer registration.
23 fn sleep_until(
24 &self,
25 deadline: Instant,
26 cancellation: &Cancellation,
27 ) -> impl Future<Output = OperationResult<()>> + Send;
28
29 /// Wait for a duration measured from the first poll, not construction. An unrepresentable deadline returns `io::ErrorKind::InvalidInput`.
30 fn sleep(
31 &self,
32 duration: Duration,
33 cancellation: &Cancellation,
34 ) -> impl Future<Output = OperationResult<()>> + Send {
35 async move {
36 cancellation.check()?;
37 let deadline = self.now().checked_add(duration).ok_or_else(|| {
38 io::Error::new(
39 io::ErrorKind::InvalidInput,
40 "sleep deadline is out of range",
41 )
42 })?;
43 self.sleep_until(deadline, cancellation).await
44 }
45 }
46
47 /// Await an operation for at most a relative duration, preserving its output inside `Ok`.
48 ///
49 /// The operation is polled first on every poll and wins if both it and the timer are ready, including with a zero duration. The timer starts when the operation first returns `Pending`; an immediately ready operation reads no clock and registers no timer. Constructing an unpolled timeout polls neither future.
50 ///
51 /// Expiry returns [`TimeoutError::Elapsed`]; timer failures, including an unrepresentable deadline, return [`TimeoutError::Clock`]. The losing future is dropped before returning. Expiry or timer failure drops the operation without requesting cooperative cancellation or awaiting backend retirement. Accepted I/O can continue after abandonment; callers remain responsible for protocol state and cleanup. This wrapper cannot preempt a long-running poll.
52 ///
53 /// ```
54 /// use base_scheduler::{Clock, TimeoutError};
55 /// use std::{future::Future, time::Duration};
56 ///
57 /// async fn receive<C: Clock>(
58 /// clock: &C,
59 /// operation: impl Future<Output = Result<String, &'static str>> + Send,
60 /// ) {
61 /// match clock.timeout(Duration::from_secs(5), operation).await {
62 /// Ok(Ok(item)) => println!("Received: {item}"),
63 /// Ok(Err(error)) => eprintln!("Receive failed: {error}"),
64 /// Err(TimeoutError::Elapsed) => eprintln!("Receive timed out"),
65 /// Err(TimeoutError::Clock(error)) => eprintln!("Timer failed: {error}"),
66 /// }
67 /// }
68 /// ```
69 fn timeout<FutureType>(
70 &self,
71 duration: Duration,
72 operation: FutureType,
73 ) -> impl Future<Output = Result<FutureType::Output, TimeoutError>> + Send
74 where
75 FutureType: Future + Send,
76 {
77 async move {
78 let mut operation = pin!(operation);
79 let never = Cancellation::never();
80 let mut timer = pin!(async { self.sleep(duration, &never).await });
81 poll_fn(move |context| {
82 if let Poll::Ready(output) = operation.as_mut().poll(context) {
83 return Poll::Ready(Ok(output));
84 }
85 timer.as_mut().poll(context).map(|result| {
86 Err(match result {
87 Ok(()) => TimeoutError::Elapsed,
88 Err(error) => TimeoutError::Clock(error),
89 })
90 })
91 })
92 .await
93 }
94 }
95}