use rayon::ThreadPool;
use serde::{Deserialize, Serialize};
use crate::error::{OcrError, Result};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConcurrencyConfig {
pub max_threads: Option<usize>,
}
pub(crate) 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(crate) fn build_thread_pool(budget: usize) -> Result<ThreadPool> {
rayon::ThreadPoolBuilder::new()
.num_threads(budget)
.build()
.map_err(|source| OcrError::Config {
message: format!("failed to initialize the OCR worker pool with {budget} threads"),
source: Some(Box::new(source)),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn budget_prefers_user_value() {
let cfg = ConcurrencyConfig { max_threads: Some(3) };
assert_eq!(resolve_thread_budget(Some(&cfg)), 3);
}
#[test]
fn budget_auto_is_sane() {
let budget = resolve_thread_budget(None);
assert!((1..=8).contains(&budget));
}
#[test]
fn budget_clamps_to_one() {
let cfg = ConcurrencyConfig { max_threads: Some(0) };
assert_eq!(resolve_thread_budget(Some(&cfg)), 1);
}
#[test]
fn private_pool_uses_resolved_budget() {
let pool = build_thread_pool(2).expect("the private pool should build");
assert_eq!(pool.current_num_threads(), 2);
}
}