aimdb_core/executor.rs
1//! Runtime capability surface consumed by the database engine.
2//!
3//! [`RuntimeOps`] is the single trait a runtime adapter implements: identity,
4//! monotonic + wall-clock time, sleep, and logging, all on concrete types
5//! (`u64` nanoseconds, `core::time::Duration`, a boxed sleep future) so the
6//! runtime travels as a *value* — `Arc<dyn RuntimeOps>` — rather than a type
7//! parameter. `AimDb`/`RuntimeContext` hold the runtime this way, and adapters
8//! hand one to `AimDbBuilder::runtime`.
9//!
10//! The boxed `sleep` future costs one allocation per call, consistent with the
11//! engine's cost model: every service future is already boxed, and `alloc` is
12//! mandatory on all supported targets.
13
14/// Heap-pinned, `Send`, `'static` future — the unit of work AimDB drives.
15pub type BoxFuture =
16 core::pin::Pin<alloc::boxed::Box<dyn core::future::Future<Output = ()> + Send + 'static>>;
17
18/// Log severity for [`RuntimeOps::log`].
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LogLevel {
21 Debug,
22 Info,
23 Warn,
24 Error,
25}
26
27/// Object-safe runtime capabilities: identity, time, sleep, and logging.
28///
29/// Implemented by every runtime adapter (`TokioAdapter`, `EmbassyAdapter`,
30/// `WasmAdapter`) so a runtime can travel as `Arc<dyn RuntimeOps>` instead of
31/// a generic parameter.
32pub trait RuntimeOps: Send + Sync {
33 /// Short adapter identity, e.g. `"tokio"`.
34 fn name(&self) -> &'static str;
35
36 /// Monotonic clock reading in nanoseconds from an arbitrary epoch.
37 ///
38 /// Only differences between two readings are meaningful. Never decreases;
39 /// saturates at `u64::MAX` rather than wrapping.
40 fn now_nanos(&self) -> u64;
41
42 /// Wall-clock time as `(seconds, nanoseconds)` since the Unix epoch, if
43 /// the runtime has a real-time clock.
44 ///
45 /// Returns `None` on platforms without a wall clock (e.g. a bare MCU with
46 /// no RTC anchor), where only monotonic time is available.
47 fn unix_time(&self) -> Option<(u64, u32)>;
48
49 /// Completes after at least `d` has elapsed.
50 fn sleep(&self, d: core::time::Duration) -> BoxFuture;
51
52 /// Emits `msg` at `level` through the adapter's logging backend.
53 fn log(&self, level: LogLevel, msg: &str);
54}
55
56pub type ExecutorResult<T> = Result<T, ExecutorError>;
57
58/// Errors from the engine's task-coordination plumbing.
59#[derive(Debug, thiserror::Error)]
60pub enum ExecutorError {
61 /// A join pipeline's trigger queue closed because every sender was dropped.
62 #[error("Join queue closed")]
63 QueueClosed,
64}
65
66/// Shared behavioral contract for [`RuntimeOps`] implementations.
67///
68/// Each adapter crate calls [`assert_runtime_ops_contract`] from a test running
69/// under its own executor (`#[tokio::test]`, `block_on`, `#[wasm_bindgen_test]`),
70/// so the one contract is exercised against every runtime implementation.
71///
72/// [`assert_runtime_ops_contract`]: test_support::assert_runtime_ops_contract
73#[doc(hidden)]
74pub mod test_support {
75 use super::{LogLevel, RuntimeOps};
76
77 /// A no-op [`RuntimeOps`] for tests that need a runtime value (a
78 /// `RuntimeContext`, an engine clock) but no real runtime behind it: the
79 /// clock is pinned at `0`, sleeps complete immediately, logs are discarded.
80 ///
81 /// One shared stub instead of a hand-rolled copy per test module, so a
82 /// `RuntimeOps` change is fixed here and in the real adapters only.
83 #[derive(Clone, Copy, Debug, Default)]
84 pub struct NoopRuntimeOps;
85
86 impl RuntimeOps for NoopRuntimeOps {
87 fn name(&self) -> &'static str {
88 "noop"
89 }
90 fn now_nanos(&self) -> u64 {
91 0
92 }
93 fn unix_time(&self) -> Option<(u64, u32)> {
94 None
95 }
96 fn sleep(&self, _d: core::time::Duration) -> super::BoxFuture {
97 alloc::boxed::Box::pin(core::future::ready(()))
98 }
99 fn log(&self, _level: LogLevel, _msg: &str) {}
100 }
101
102 /// Asserts the behavioral contract every `RuntimeOps` implementation must hold.
103 ///
104 /// Uses `Duration::ZERO` for the sleep check: the Embassy host-test time
105 /// driver reports `now() == 0` with a no-op `schedule_wake`, so any non-zero
106 /// sleep would hang forever on the host. Adapters with a real clock should
107 /// additionally assert that a non-zero sleep advances `now_nanos` in their
108 /// own tests.
109 pub async fn assert_runtime_ops_contract(ops: &dyn RuntimeOps) {
110 assert!(!ops.name().is_empty(), "name() must be non-empty");
111
112 let t0 = ops.now_nanos();
113 let t1 = ops.now_nanos();
114 assert!(t1 >= t0, "now_nanos() must be monotonic ({t1} < {t0})");
115
116 ops.log(LogLevel::Debug, "runtime-ops contract: debug");
117 ops.log(LogLevel::Info, "runtime-ops contract: info");
118 ops.log(LogLevel::Warn, "runtime-ops contract: warn");
119 ops.log(LogLevel::Error, "runtime-ops contract: error");
120
121 ops.sleep(core::time::Duration::ZERO).await;
122
123 if let Some((secs, nanos)) = ops.unix_time() {
124 assert!(
125 nanos < 1_000_000_000,
126 "unix_time nanos out of range: {nanos}"
127 );
128 // Sanity: any real wall clock reads after 2020-09-13 (1.6e9).
129 assert!(
130 secs > 1_600_000_000,
131 "unix_time seconds implausible: {secs}"
132 );
133 }
134 }
135}