Skip to main content

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}