sail-rs 0.7.1

Official Rust SDK for Sail: create and drive Sailboxes (sandboxed cloud VMs) with lifecycle, streaming exec, file transfer, and ingress.
Documentation
//! Process-wide tokio runtime, initialized lazily on first use.
//!
//! NEVER initialize at import/module-load time: spawning runtime threads
//! before an application forks (common in Python preload patterns) leaves
//! the child with a dead runtime. Lazy init means a child that never
//! touched the SDK pre-fork gets a fresh runtime on first use.

use std::future::Future;
use std::sync::OnceLock;

use tokio::runtime::Runtime;

static RUNTIME: OnceLock<Runtime> = OnceLock::new();

/// Return the process-wide multi-threaded tokio runtime, building it on first
/// call. Safe to call after a fork in a child that never used the SDK pre-fork.
pub fn runtime() -> &'static Runtime {
    RUNTIME.get_or_init(|| {
        // Read the environment here, at first use, not at module load: the
        // lazy-init contract above applies to everything the runtime build
        // depends on.
        let threads = worker_thread_count(
            std::env::var("SAIL_RUNTIME_THREADS").ok().as_deref(),
            std::thread::available_parallelism().map_or(2, usize::from),
        );
        tokio::runtime::Builder::new_multi_thread()
            .worker_threads(threads)
            .thread_name("sail-core")
            .enable_all()
            .build()
            .expect("failed to build sail-core tokio runtime")
    })
}

/// Pick how many worker threads the shared runtime gets.
///
/// The SDK is mostly waiting on the network, but request work (TLS, JSON,
/// stream copying) runs on these threads, so a fixed two-thread pool became a
/// ceiling for applications driving dozens of Sailboxes concurrently. Scale
/// with the machine instead: one thread per CPU, at least 2 so a blocked
/// request cannot starve the pool, at most 8 because a thin RPC client gains
/// nothing from more while every process that imports the SDK would pay for
/// the idle threads.
///
/// `SAIL_RUNTIME_THREADS` overrides the default (accepted range 1..=256).
/// Anything unparseable or out of range falls back to the default rather than
/// erroring: this runs inside runtime construction, where a panic would poison
/// every SDK call in the process.
fn worker_thread_count(env_value: Option<&str>, available: usize) -> usize {
    if let Some(raw) = env_value {
        if let Ok(n) = raw.trim().parse::<usize>() {
            if (1..=256).contains(&n) {
                return n;
            }
        }
    }
    available.clamp(2, 8)
}

/// Drive an SDK future to completion on the shared internal runtime, for
/// synchronous callers outside any async context.
///
/// Must not be called from within a tokio runtime; an async host should await
/// the `async` API directly instead.
///
/// # Panics
///
/// Panics when called from within a tokio runtime (tokio's "Cannot start a
/// runtime from within a runtime" guard; a `debug_assert` names this call
/// first in debug builds), and if the shared runtime fails to build on first
/// use.
// The sync facade for the PyO3 bridge (with the GIL released) and the CLI. The
// debug assert documents the never-inside-a-runtime invariant of those entry
// points.
pub fn block_on<F: Future>(future: F) -> F::Output {
    debug_assert!(
        tokio::runtime::Handle::try_current().is_err(),
        "sail::block_on called from within a tokio runtime; use the async API instead"
    );
    runtime().block_on(future)
}

#[cfg(test)]
mod tests {
    use super::worker_thread_count;

    #[test]
    fn defaults_scale_with_the_machine_between_2_and_8() {
        assert_eq!(worker_thread_count(None, 1), 2);
        assert_eq!(worker_thread_count(None, 2), 2);
        assert_eq!(worker_thread_count(None, 4), 4);
        assert_eq!(worker_thread_count(None, 8), 8);
        assert_eq!(worker_thread_count(None, 64), 8);
    }

    #[test]
    fn env_override_wins_within_its_accepted_range() {
        assert_eq!(worker_thread_count(Some("1"), 64), 1);
        assert_eq!(worker_thread_count(Some("16"), 4), 16);
        assert_eq!(worker_thread_count(Some(" 32 "), 4), 32);
        assert_eq!(worker_thread_count(Some("256"), 4), 256);
    }

    #[test]
    fn bad_env_values_fall_back_to_the_default() {
        assert_eq!(worker_thread_count(Some(""), 4), 4);
        assert_eq!(worker_thread_count(Some("0"), 4), 4);
        assert_eq!(worker_thread_count(Some("257"), 4), 4);
        assert_eq!(worker_thread_count(Some("-2"), 4), 4);
        assert_eq!(worker_thread_count(Some("two"), 4), 4);
        assert_eq!(worker_thread_count(Some("2.5"), 4), 4);
    }
}