use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;
use core::future::Future;
use futures::channel::oneshot;
use rayon::ThreadPool;
use crate::{BackgroundHandle, BackgroundPool};
use proxima_core::ProximaError;
pub struct RayonBackgroundPool {
pool: Arc<ThreadPool>,
}
impl RayonBackgroundPool {
pub fn new() -> Result<Self, ProximaError> {
let pool = rayon::ThreadPoolBuilder::new()
.thread_name(|index| format!("proxima-rayon-bg-{index}"))
.build()
.map_err(|error| ProximaError::Config(format!("build rayon pool: {error}")))?;
Ok(Self {
pool: Arc::new(pool),
})
}
pub fn with_threads(threads: usize) -> Result<Self, ProximaError> {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.thread_name(|index| format!("proxima-rayon-bg-{index}"))
.build()
.map_err(|error| ProximaError::Config(format!("build rayon pool: {error}")))?;
Ok(Self {
pool: Arc::new(pool),
})
}
pub fn spawn<F, T>(
&self,
work: F,
) -> impl Future<Output = Result<T, ProximaError>> + Send + 'static
where
F: FnOnce() -> Result<T, ProximaError> + Send + 'static,
T: Send + 'static,
{
let (tx, rx) = oneshot::channel();
self.pool.spawn(move || {
let result = work();
let _ = tx.send(result);
});
async move {
rx.await.unwrap_or_else(|_| {
Err(ProximaError::Body(
"rayon background task dropped sender".into(),
))
})
}
}
}
impl BackgroundPool for RayonBackgroundPool {
fn spawn(
&self,
work: Box<dyn FnOnce() -> Result<Box<dyn core::any::Any + Send>, ProximaError> + Send>,
) -> BackgroundHandle<Box<dyn core::any::Any + Send>> {
let (tx, rx) = oneshot::channel();
self.pool.spawn(move || {
let result = work();
let _ = tx.send(result);
});
Box::pin(async move {
rx.await.unwrap_or_else(|_| {
Err(ProximaError::Body(
"rayon background task dropped sender".into(),
))
})
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[proxima::test]
async fn typed_spawn_hands_back_the_concrete_type() {
let pool = RayonBackgroundPool::new().expect("build pool");
let counter = Arc::new(AtomicUsize::new(0));
let counter_for_work = Arc::clone(&counter);
let value = pool
.spawn(move || {
counter_for_work.fetch_add(1, Ordering::SeqCst);
Ok(42_u32)
})
.await
.expect("background result");
assert_eq!(value, 42);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[proxima::test]
async fn typed_spawn_runs_many_jobs_across_the_pool() {
let pool = RayonBackgroundPool::with_threads(4).expect("build pool");
let handles: Vec<_> = (0..8_u32)
.map(|index| pool.spawn(move || Ok(index * 2)))
.collect();
let mut results: Vec<u32> = Vec::new();
for handle in handles {
results.push(handle.await.expect("result"));
}
results.sort_unstable();
let expected: Vec<u32> = (0..8).map(|index| index * 2).collect();
assert_eq!(results, expected);
}
#[proxima::test]
async fn erased_pool_round_trips_a_payload_through_the_trait() {
let pool: Arc<dyn BackgroundPool> =
Arc::new(RayonBackgroundPool::with_threads(2).expect("build pool"));
let handle = pool.spawn(Box::new(|| {
Ok(Box::new(alloc::string::String::from("hello")) as Box<dyn Any + Send>)
}));
let boxed = handle.await.expect("background result");
let value = boxed.downcast::<alloc::string::String>().expect("downcast");
assert_eq!(*value, "hello");
}
#[proxima::test]
async fn erased_pool_surfaces_the_jobs_own_error() {
let pool: Arc<dyn BackgroundPool> =
Arc::new(RayonBackgroundPool::new().expect("build pool"));
let handle = pool.spawn(Box::new(|| Err(ProximaError::Body("job said no".into()))));
let error = handle.await.expect_err("job failed");
assert!(
error.to_string().contains("job said no"),
"job error, not a transport error; got {error}"
);
}
}