use std::sync::Once;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ConcurrencyConfig {
pub max_threads: Option<usize>,
}
static POOL_INIT: Once = Once::new();
pub fn resolve_thread_budget(config: Option<&ConcurrencyConfig>) -> usize {
if let Some(n) = config.and_then(|c| c.max_threads) {
return n.max(1);
}
num_cpus::get().min(8)
}
pub fn init_thread_pools(budget: usize) {
POOL_INIT.call_once(|| {
rayon::ThreadPoolBuilder::new().num_threads(budget).build_global().ok();
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_thread_budget_none() {
let budget = resolve_thread_budget(None);
assert!(budget >= 1);
assert!(budget <= 8);
}
#[test]
fn test_resolve_thread_budget_with_config() {
let config = ConcurrencyConfig { max_threads: Some(4) };
assert_eq!(resolve_thread_budget(Some(&config)), 4);
}
#[test]
fn test_resolve_thread_budget_clamps_to_one() {
let config = ConcurrencyConfig { max_threads: Some(0) };
assert_eq!(resolve_thread_budget(Some(&config)), 1);
}
#[test]
fn test_resolve_thread_budget_no_max() {
let config = ConcurrencyConfig { max_threads: None };
let budget = resolve_thread_budget(Some(&config));
assert!(budget >= 1);
assert!(budget <= 8);
}
#[test]
fn test_init_thread_pools_idempotent() {
init_thread_pools(2);
init_thread_pools(4);
}
#[test]
fn test_default() {
let config = ConcurrencyConfig::default();
assert!(config.max_threads.is_none());
}
#[test]
fn test_serde_roundtrip() {
let json = r#"{"max_threads": 2}"#;
let config: ConcurrencyConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_threads, Some(2));
let serialized = serde_json::to_string(&config).unwrap();
let roundtripped: ConcurrencyConfig = serde_json::from_str(&serialized).unwrap();
assert_eq!(roundtripped.max_threads, Some(2));
}
#[test]
fn test_serde_empty() {
let json = r#"{}"#;
let config: ConcurrencyConfig = serde_json::from_str(json).unwrap();
assert!(config.max_threads.is_none());
}
}