choreo_content/runtime.rs
1//! Sidecar `tokio` runtime for the Coordination Platform's subxt client.
2//!
3//! The daemon and this crate's blocking `execute_*` entry points are
4//! synchronous, thread-based code. Only `subxt` is async, so the crate owns a
5//! single process-wide runtime created once at startup; the blocking
6//! `execute_*` functions run their subxt futures on it via
7//! [`Runtime::block_on`]. IPFS (`ureq`) and the indexer (`tungstenite`,
8//! synchronous mode) do NOT use this runtime.
9//!
10//! [`init`] must be called exactly once before any tx/state tool executes
11//! (the daemon does so from `main()`). [`get`] returns `None` before init or
12//! when the runtime failed to build, and callers map that to a
13//! [`crate::ContentError::RuntimeNotInitialized`] rather than panicking.
14
15use std::sync::OnceLock;
16
17static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
18
19/// Create the multi-threaded tokio runtime (with IO + time drivers) and store
20/// it in the process-wide sidecar. Idempotent: subsequent calls are no-ops,
21/// including calls that race a concurrent initializer.
22///
23/// Fails only if the OS refuses to spawn the worker threads, surfaced as an
24/// error instead of panicking so the daemon can abort startup cleanly.
25///
26/// # Errors
27///
28/// Returns the underlying `std::io::Error` boxed if `tokio::runtime::Builder`
29/// cannot build the multi-threaded runtime (e.g. worker-thread spawn
30/// failure). Never fails once the runtime is already initialized.
31pub fn init() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
32 if RUNTIME.get().is_some() {
33 return Ok(());
34 }
35 let rt = tokio::runtime::Builder::new_multi_thread()
36 .enable_all()
37 .build()?;
38 // A racing thread may have initialized the runtime between the check above
39 // and this `set`; that is a success, not an error — the sidecar exists and
40 // our built runtime is simply discarded.
41 if RUNTIME.set(rt).is_ok() {
42 tracing::info!("coordinator tokio runtime initialized");
43 }
44 Ok(())
45}
46
47/// Access the sidecar runtime, or `None` if [`init`] has not been called (or
48/// failed). Callers must not panic on `None` — they surface a
49/// [`crate::ContentError::RuntimeNotInitialized`] error instead.
50pub fn get() -> Option<&'static tokio::runtime::Runtime> {
51 RUNTIME.get()
52}
53
54/// Run `fut` to completion on the sidecar tokio runtime.
55///
56/// Returns [`crate::ContentError::RuntimeNotInitialized`] if [`init`] was never
57/// called (or failed), so callers surface a clear error instead of panicking on
58/// a missing runtime. Logs the wall-clock duration so every tool call leaves an
59/// observability trail.
60pub(crate) fn block_on<F>(fut: F) -> Result<F::Output, crate::ContentError>
61where
62 F: std::future::Future,
63{
64 let rt = get().ok_or(crate::ContentError::RuntimeNotInitialized)?;
65 let start = std::time::Instant::now();
66 let out = rt.block_on(fut);
67 tracing::debug!(
68 elapsed_ms = start.elapsed().as_millis(),
69 "coordinator tool call completed"
70 );
71 Ok(out)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn init_then_get_returns_some() {
80 // Idempotent — safe to call even if another test initialized it.
81 init().expect("runtime init should succeed");
82 assert!(
83 get().is_some(),
84 "get() must return the runtime after init()"
85 );
86 }
87
88 #[test]
89 fn block_on_runs_on_sidecar() {
90 init().expect("runtime init should succeed");
91 let out = block_on(async { 42u8 }).expect("block_on must run on an initialized runtime");
92 assert_eq!(out, 42);
93 }
94}