Skip to main content

mkit_server/
rt.rs

1//! Async and runtime model (PRD §5.2).
2//!
3//! Traits in this crate use native `async fn` / return-position
4//! `impl Future + MaybeSend`. [`MaybeSend`] means `Send` on native targets
5//! and is a blanket marker on wasm32, where Workers futures are `!Send`.
6//! Where a `dyn` future is needed (hook lists), [`BoxFuture`] carries the
7//! same conditional `Send`.
8
9use core::future::Future;
10use core::pin::Pin;
11use core::sync::atomic::{AtomicBool, AtomicI64, Ordering};
12use core::time::Duration;
13use std::sync::Arc;
14
15/// `Send` on native targets; implemented for every type on wasm32.
16#[cfg(not(target_arch = "wasm32"))]
17pub trait MaybeSend: Send {}
18#[cfg(not(target_arch = "wasm32"))]
19impl<T: Send + ?Sized> MaybeSend for T {}
20
21/// `Send` on native targets; implemented for every type on wasm32.
22#[cfg(target_arch = "wasm32")]
23pub trait MaybeSend {}
24#[cfg(target_arch = "wasm32")]
25impl<T: ?Sized> MaybeSend for T {}
26
27/// `Sync` on native targets; implemented for every type on wasm32.
28#[cfg(not(target_arch = "wasm32"))]
29pub trait MaybeSync: Sync {}
30#[cfg(not(target_arch = "wasm32"))]
31impl<T: Sync + ?Sized> MaybeSync for T {}
32
33/// `Sync` on native targets; implemented for every type on wasm32.
34#[cfg(target_arch = "wasm32")]
35pub trait MaybeSync {}
36#[cfg(target_arch = "wasm32")]
37impl<T: ?Sized> MaybeSync for T {}
38
39/// A boxed future that is `Send` on native targets only.
40#[cfg(not(target_arch = "wasm32"))]
41pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
42/// A boxed future that is `Send` on native targets only.
43#[cfg(target_arch = "wasm32")]
44pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
45
46/// A boxed stream that is `Send` on native targets only.
47#[cfg(not(target_arch = "wasm32"))]
48pub type BoxStream<'a, T> = Pin<Box<dyn futures_core::Stream<Item = T> + Send + 'a>>;
49/// A boxed stream that is `Send` on native targets only.
50#[cfg(target_arch = "wasm32")]
51pub type BoxStream<'a, T> = Pin<Box<dyn futures_core::Stream<Item = T> + 'a>>;
52
53/// Injected time source, in Unix epoch milliseconds.
54///
55/// Nothing in `mkit-server` reads `SystemTime` or `Instant` directly:
56/// `Instant::now()` panics on wasm32 (see
57/// `apps/mkit-worker-common/src/adapter.rs`), and tests need to control
58/// time. Adapters pass their runtime's clock in.
59pub trait Clock: MaybeSend + MaybeSync {
60    /// The current time in Unix epoch milliseconds.
61    fn now_ms(&self) -> i64;
62}
63
64/// The host wall clock. Native targets only; Workers supply their own.
65#[cfg(not(target_arch = "wasm32"))]
66#[derive(Debug, Default, Clone, Copy)]
67pub struct SystemClock;
68
69#[cfg(not(target_arch = "wasm32"))]
70impl Clock for SystemClock {
71    fn now_ms(&self) -> i64 {
72        use std::time::{SystemTime, UNIX_EPOCH};
73        // Saturate rather than panic: a clock this far off is a host fault
74        // that the validity-window checks will reject anyway.
75        match SystemTime::now().duration_since(UNIX_EPOCH) {
76            Ok(since) => i64::try_from(since.as_millis()).unwrap_or(i64::MAX),
77            Err(before) => i64::try_from(before.duration().as_millis()).map_or(i64::MIN, |ms| -ms),
78        }
79    }
80}
81
82/// A clock that only moves when told to. Usable on every target, and shared
83/// across threads through interior mutability.
84#[derive(Debug, Default)]
85pub struct ManualClock {
86    now_ms: AtomicI64,
87}
88
89impl ManualClock {
90    /// A clock reading `start_ms`.
91    #[must_use]
92    pub const fn new(start_ms: i64) -> Self {
93        Self {
94            now_ms: AtomicI64::new(start_ms),
95        }
96    }
97
98    /// Set the reading to `now_ms`.
99    pub fn set(&self, now_ms: i64) {
100        self.now_ms.store(now_ms, Ordering::SeqCst);
101    }
102
103    /// Move the reading forward (or back, for a negative `delta_ms`).
104    pub fn advance(&self, delta_ms: i64) {
105        self.now_ms.fetch_add(delta_ms, Ordering::SeqCst);
106    }
107}
108
109impl Clock for ManualClock {
110    fn now_ms(&self) -> i64 {
111        self.now_ms.load(Ordering::SeqCst)
112    }
113}
114
115/// Injected task spawner: tokio on native, `wasm_bindgen_futures` or
116/// `ctx.wait_until` on Workers.
117pub trait Spawner: MaybeSend + MaybeSync {
118    /// Run `fut` to completion in the background.
119    fn spawn(&self, fut: BoxFuture<'static, ()>);
120}
121
122/// Injected timer, the counterpart of [`Clock`] for deadlines: `Instant` and
123/// `tokio::time` are not available on every target, so an adapter passes in
124/// its runtime's sleep (`tokio::time::sleep` on native, `worker::Delay` on
125/// Workers) and core code bounds an external call with [`with_timeout`].
126pub trait Sleep: MaybeSend + MaybeSync {
127    /// A future that completes once `duration` has elapsed.
128    fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()>;
129}
130
131impl<T: Sleep + ?Sized> Sleep for Arc<T> {
132    fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()> {
133        (**self).sleep(duration)
134    }
135}
136
137/// [`with_timeout`] gave up before the future finished.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct Elapsed;
140
141/// Run `fut`, giving up with [`Elapsed`] once `sleep` says `after` has passed.
142/// A future that is ready wins over an elapsed timer, and a timed-out future
143/// is dropped, so the caller must not assume its side effects did not land.
144///
145/// # Errors
146/// [`Elapsed`] when the timer completes first.
147pub async fn with_timeout<S, F>(sleep: &S, after: Duration, fut: F) -> Result<F::Output, Elapsed>
148where
149    S: Sleep + ?Sized,
150    F: Future,
151{
152    let fut = core::pin::pin!(fut);
153    match futures::future::select(fut, sleep.sleep(after)).await {
154        futures::future::Either::Left((output, _)) => Ok(output),
155        futures::future::Either::Right(((), _)) => Err(Elapsed),
156    }
157}
158
159/// A [`Sleep`] that only completes when told to, for tests. Shared across
160/// clones; [`ManualSleep::elapsed`] starts already fired.
161#[derive(Debug, Clone, Default)]
162pub struct ManualSleep {
163    fired: Arc<AtomicBool>,
164    wake: Arc<tokio::sync::Notify>,
165    requested: Arc<std::sync::Mutex<Vec<Duration>>>,
166}
167
168impl ManualSleep {
169    /// A sleeper whose sleeps stay pending until [`Self::fire`].
170    #[must_use]
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// A sleeper whose sleeps complete immediately.
176    #[must_use]
177    pub fn elapsed() -> Self {
178        let sleeper = Self::default();
179        sleeper.fire();
180        sleeper
181    }
182
183    /// The duration of every sleep requested so far, in order.
184    #[must_use]
185    pub fn requested(&self) -> Vec<Duration> {
186        self.requested
187            .lock()
188            .map_or_else(|poisoned| poisoned.into_inner().clone(), |v| v.clone())
189    }
190
191    /// Complete every current and future sleep.
192    pub fn fire(&self) {
193        self.fired.store(true, Ordering::SeqCst);
194        self.wake.notify_waiters();
195    }
196}
197
198impl Sleep for ManualSleep {
199    fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()> {
200        match self.requested.lock() {
201            Ok(mut requested) => requested.push(duration),
202            Err(poisoned) => poisoned.into_inner().push(duration),
203        }
204        let (fired, wake) = (self.fired.clone(), self.wake.clone());
205        Box::pin(async move {
206            loop {
207                let notified = wake.notified();
208                if fired.load(Ordering::SeqCst) {
209                    return;
210                }
211                notified.await;
212            }
213        })
214    }
215}
216
217/// Make a [`MaybeSend`] future satisfy the `+ Send` bound that generated
218/// connectrpc service traits require. On native targets the future is
219/// already `Send` and is returned unchanged.
220#[cfg(not(target_arch = "wasm32"))]
221#[must_use]
222pub fn send_wrap<F: Future + Send>(f: F) -> F {
223    f
224}
225
226/// Make a [`MaybeSend`] future satisfy the `+ Send` bound that generated
227/// connectrpc service traits require. The Workers adapter wraps the whole
228/// handler future once (PRD §5.2).
229///
230/// `SendWrapper` checks the thread on every poll and drop: a poll or drop on
231/// a thread other than the one that created it panics, and is never
232/// undefined behavior. Workers run wasm32 single-threaded, so it cannot
233/// fire there; a wasm32 build with the `atomics` target feature can be
234/// multi-threaded, and there the caller must keep the future on its
235/// creating thread.
236#[cfg(target_arch = "wasm32")]
237#[must_use]
238pub fn send_wrap<F: Future>(f: F) -> send_wrapper::SendWrapper<F> {
239    send_wrapper::SendWrapper::new(f)
240}
241
242/// [`send_wrap`] for a stream, e.g. a response stream handed to Connect's
243/// `Response::stream_ok`. On native targets the stream is already `Send` and
244/// is returned unchanged.
245#[cfg(all(feature = "connect", not(target_arch = "wasm32")))]
246pub(crate) fn send_wrap_stream<S: futures_core::Stream + Send>(s: S) -> S {
247    s
248}
249
250/// [`send_wrap`] for a stream, e.g. a response stream handed to Connect's
251/// `Response::stream_ok`; the same thread rule applies.
252#[cfg(all(feature = "connect", target_arch = "wasm32"))]
253pub(crate) fn send_wrap_stream<S: futures_core::Stream>(s: S) -> send_wrapper::SendWrapper<S> {
254    send_wrapper::SendWrapper::new(s)
255}
256
257// Native only: these assert the native (`Send`) half of the model.
258#[cfg(all(test, not(target_arch = "wasm32")))]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn manual_clock_advances() {
264        let clock = ManualClock::new(1_000);
265        assert_eq!(clock.now_ms(), 1_000);
266        clock.advance(250);
267        assert_eq!(clock.now_ms(), 1_250);
268        clock.set(7);
269        assert_eq!(clock.now_ms(), 7);
270        assert_eq!(ManualClock::default().now_ms(), 0);
271    }
272
273    #[test]
274    fn system_clock_is_after_2020() {
275        // 2020-01-01T00:00:00Z; a sanity bound, not a precision check.
276        assert!(SystemClock.now_ms() > 1_577_836_800_000);
277    }
278
279    #[test]
280    fn send_wrap_is_identity_on_native() {
281        let fut = send_wrap(async { 41 + 1 });
282        assert_eq!(futures_executor::block_on(fut), 42);
283    }
284
285    fn assert_send<T: Send>(_: &T) {}
286
287    /// A trait shaped like the storage/policy traits later work packages add.
288    trait Service {
289        fn call(&self) -> impl Future<Output = u8> + MaybeSend;
290    }
291
292    struct Seven;
293    impl Service for Seven {
294        async fn call(&self) -> u8 {
295            7
296        }
297    }
298
299    /// Compile-time check: on native, `MaybeSend: Send` makes a generic
300    /// caller see the returned future as `Send` through supertrait
301    /// elaboration, without naming the concrete type.
302    fn require_send_from_generic<S: Service>(service: &S) -> u8 {
303        let fut = service.call();
304        assert_send(&fut);
305        futures_executor::block_on(fut)
306    }
307
308    #[test]
309    fn maybe_send_future_is_send_to_generic_callers_on_native() {
310        assert_eq!(require_send_from_generic(&Seven), 7);
311        let boxed: BoxFuture<'static, u8> = Box::pin(Seven.call());
312        assert_send(&boxed);
313    }
314
315    #[test]
316    fn with_timeout_returns_a_ready_future_and_times_out_a_pending_one() {
317        let never = ManualSleep::new();
318        let ready = with_timeout(&never, Duration::from_secs(5), async { 9 });
319        assert_eq!(futures_executor::block_on(ready), Ok(9));
320        let fired = ManualSleep::elapsed();
321        let stuck = with_timeout(
322            &fired,
323            Duration::from_secs(5),
324            core::future::pending::<u8>(),
325        );
326        assert_eq!(futures_executor::block_on(stuck), Err(Elapsed));
327        // A ready future still wins over an already-fired timer.
328        let both = with_timeout(&fired, Duration::from_secs(5), async { 3 });
329        assert_eq!(futures_executor::block_on(both), Ok(3));
330    }
331
332    #[test]
333    fn manual_sleep_wakes_a_pending_sleep_when_fired() {
334        let sleeper = ManualSleep::new();
335        let timer = sleeper.sleep(Duration::from_secs(1));
336        let woken = std::sync::Arc::new(core::sync::atomic::AtomicBool::new(false));
337        let flag = woken.clone();
338        let handle = std::thread::spawn(move || {
339            futures_executor::block_on(timer);
340            flag.store(true, Ordering::SeqCst);
341        });
342        std::thread::sleep(Duration::from_millis(50));
343        assert!(!woken.load(Ordering::SeqCst));
344        sleeper.fire();
345        handle.join().unwrap();
346        assert!(woken.load(Ordering::SeqCst));
347    }
348
349    #[test]
350    fn spawner_accepts_boxed_futures() {
351        struct Inline;
352        impl Spawner for Inline {
353            fn spawn(&self, fut: BoxFuture<'static, ()>) {
354                futures_executor::block_on(fut);
355            }
356        }
357        let ran = std::sync::Arc::new(core::sync::atomic::AtomicBool::new(false));
358        let flag = ran.clone();
359        Inline.spawn(Box::pin(async move { flag.store(true, Ordering::SeqCst) }));
360        assert!(ran.load(Ordering::SeqCst));
361    }
362}