use std::sync::{mpsc::sync_channel, OnceLock};
use std::time::{Duration, Instant};
use tokio::runtime::{Builder, Runtime};
use tropel_sdk::{Result, TropelError};
const SPIN_WINDOW: Duration = Duration::from_micros(40);
thread_local! {
static SKIP_SPIN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
static IO_RT: OnceLock<Runtime> = OnceLock::new();
fn default_io_workers() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
}
fn workers_from_override(override_str: Option<&str>) -> usize {
override_str
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or_else(default_io_workers)
.clamp(1, 512)
}
fn io_worker_threads() -> usize {
workers_from_override(std::env::var("TROPEL_IO_WORKERS").ok().as_deref())
}
const IO_TASK_DROPPED: &str = "io task dropped";
fn io_rt() -> &'static Runtime {
IO_RT.get_or_init(|| {
Builder::new_multi_thread()
.enable_all()
.worker_threads(io_worker_threads())
.thread_name("tropel-io")
.build()
.expect("build tropel-io runtime")
})
}
pub fn execute_blocking<F, T>(fut: F) -> Result<T>
where
F: std::future::Future<Output = Result<T>> + Send + 'static,
T: Send + 'static,
{
let (tx, rx) = sync_channel::<Result<T>>(1);
io_rt().spawn(async move {
let _ = tx.send(fut.await); });
let spin_deadline = Instant::now() + SPIN_WINDOW;
let mut spun = false;
if !SKIP_SPIN.with(|s| s.get()) {
loop {
match rx.try_recv() {
Ok(r) => {
SKIP_SPIN.with(|s| s.set(false)); return r;
}
Err(std::sync::mpsc::TryRecvError::Empty) => {
if Instant::now() < spin_deadline {
spun = true;
std::hint::spin_loop();
continue;
}
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
return Err(TropelError::Http(IO_TASK_DROPPED.into()));
}
}
break;
}
if spun {
SKIP_SPIN.with(|s| s.set(true));
}
}
let park_start = Instant::now();
let result = rx
.recv_timeout(std::time::Duration::from_secs(65))
.map_err(|e| match e {
std::sync::mpsc::RecvTimeoutError::Timeout => {
TropelError::Http("blocking request timed out (65s)".into())
}
std::sync::mpsc::RecvTimeoutError::Disconnected => {
TropelError::Http(IO_TASK_DROPPED.into())
}
})?;
if park_start.elapsed() < SPIN_WINDOW {
SKIP_SPIN.with(|s| s.set(false));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn workers_from_override_defaults_to_cores() {
let expected = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
assert_eq!(workers_from_override(None), expected);
assert_eq!(workers_from_override(Some("bogus")), expected);
}
#[test]
fn workers_from_override_clamps_bounds() {
assert_eq!(workers_from_override(Some("999")), 512);
assert_eq!(workers_from_override(Some("0")), 1);
assert_eq!(workers_from_override(Some(" 8 ")), 8);
}
#[test]
fn execute_blocking_resolves_future() {
let result = execute_blocking(async { Ok::<i32, TropelError>(42) }).unwrap();
assert_eq!(result, 42);
}
#[test]
fn execute_blocking_propagates_error() {
let result: tropel_sdk::Result<i32> =
execute_blocking(async { Err::<i32, _>(TropelError::Http("boom".into())) });
let err = result.unwrap_err();
assert_eq!(format!("{}", err), "HTTP error: boom");
}
#[test]
fn execute_blocking_tight_loop_no_starvation() {
for i in 0..2_000 {
let v = execute_blocking(async move { Ok::<i32, TropelError>(i) }).unwrap();
assert_eq!(v, i, "result mismatch at iteration {i}");
}
}
#[test]
fn execute_blocking_works_from_inside_current_thread_runtime() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let out =
rt.block_on(async { execute_blocking(async { Ok::<i32, TropelError>(7) }).unwrap() });
assert_eq!(out, 7);
}
}