use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::OnceLock;
use tokio::runtime::{Builder, Runtime};
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
static IO_RT_SEQ: AtomicUsize = AtomicUsize::new(0);
fn io_runtime_thread_name() -> String {
format!(
"degenbot-io-rt-{}",
IO_RT_SEQ.fetch_add(1, Ordering::Relaxed)
)
}
fn build_runtime() -> Result<Runtime, std::io::Error> {
let workers = crate::cpu_budget::ambient_io_worker_count();
crate::worker_census::register(crate::worker_census::WorkerCensusEntry {
resource: "io_runtime_workers",
kind: "tokio multi-thread runtime (ambient I/O — pump, dispatch, delivery, pyo3-async)",
count: workers,
thread_name: "degenbot-io-rt-{n}",
sizing: "cpu_budget::ambient_io_worker_count — cgroup budget minus the solve bins, floored at 1; override `runtime.io_workers` (env DEGENBOT_IO_WORKERS)",
binding: "shared",
});
Builder::new_multi_thread()
.worker_threads(workers)
.thread_name_fn(io_runtime_thread_name)
.enable_all()
.build()
}
pub fn get_runtime() -> &'static Runtime {
RUNTIME.get_or_init(|| {
#[expect(clippy::panic)]
build_runtime().unwrap_or_else(|e| panic!("Failed to create Tokio runtime: {e}"))
})
}
#[cfg(test)]
#[expect(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_build_runtime_sizes_from_cgroup_budget_policy() {
let expected = crate::cpu_budget::ambient_io_worker_count_from(
::degenbot_config::holder::config().runtime.io_workers,
crate::cpu_budget::solve_worker_count(),
crate::cpu_budget::effective_cpu_budget(),
);
let rt = build_runtime().unwrap();
assert_eq!(
rt.metrics().num_workers(),
expected,
"ambient runtime workers must follow the CPU-budget policy"
);
}
#[test]
fn ambient_runtime_workers_carry_the_census_thread_name() {
let rt = build_runtime().unwrap();
let name = rt.block_on(async {
tokio::spawn(async move { std::thread::current().name().map(str::to_owned) })
.await
.expect("worker spawn")
});
let name = name.expect("worker thread name");
assert!(
name.starts_with("degenbot-io-rt-"),
"ambient worker must be census-named, got {name}"
);
}
#[test]
fn ambient_runtime_registers_a_census_row_matching_its_worker_count() {
let rt = build_runtime().unwrap();
let row = crate::worker_census::snapshot()
.into_iter()
.find(|e| e.resource == "io_runtime_workers")
.expect("ambient runtime must self-register in the census");
assert_eq!(row.count, rt.metrics().num_workers());
assert_eq!(row.thread_name, "degenbot-io-rt-{n}");
assert_eq!(row.sizing, row.sizing); }
#[test]
fn test_runtime_singleton() {
let rt1 = get_runtime();
let rt2 = get_runtime();
assert!(std::ptr::eq(rt1, rt2));
}
#[test]
fn test_runtime_can_spawn_tasks() {
let runtime = get_runtime();
let result = runtime.block_on(async {
let handle = tokio::spawn(async { 42 });
handle
.await
.expect("spawned task should complete successfully")
});
assert_eq!(result, 42);
}
}