use linkrs_core::{StorageError, StorageResult};
use parking_lot::{Condvar, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct BackgroundTaskHandle {
running: Arc<AtomicBool>,
finished: Arc<Mutex<bool>>,
condvar: Arc<Condvar>,
}
impl BackgroundTaskHandle {
pub fn stop(&self) {
self.running.store(false, Ordering::Release);
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Acquire)
}
pub fn join(&self) {
let mut finished = self.finished.lock();
while !*finished {
self.condvar.wait(&mut finished);
}
}
}
#[derive(Clone)]
pub struct StorageThreadPool {
inner: Arc<rayon::ThreadPool>,
}
impl std::fmt::Debug for StorageThreadPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StorageThreadPool")
.field("num_threads", &self.inner.current_num_threads())
.finish()
}
}
impl StorageThreadPool {
pub fn new() -> StorageResult<Self> {
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2)
.clamp(2, 16);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.thread_name(|idx| format!("linkrs-storage-{idx}"))
.build()
.map_err(|e| {
StorageError::storage_error(format!("failed to build thread pool: {e}"))
})?;
Ok(Self {
inner: Arc::new(pool),
})
}
pub fn spawn<F>(&self, task: F)
where
F: FnOnce() + Send + 'static,
{
self.inner.spawn(task);
}
pub fn install<R, F>(&self, task: F) -> R
where
R: Send,
F: FnOnce() -> R + Send,
{
self.inner.install(task)
}
pub fn spawn_periodic<F>(
&self,
running: Arc<AtomicBool>,
interval: Duration,
min_interval: Duration,
task: F,
) -> BackgroundTaskHandle
where
F: FnMut() + Send + 'static,
{
running.store(true, Ordering::Release);
let finished = Arc::new(Mutex::new(false));
let condvar = Arc::new(Condvar::new());
let mut task = task;
let run = running.clone();
let fin = finished.clone();
let cv = condvar.clone();
self.inner.spawn(move || {
while run.load(Ordering::Acquire) {
let start = std::time::Instant::now();
task();
if !run.load(Ordering::Acquire) {
break;
}
let sleep = interval.saturating_sub(start.elapsed()).max(min_interval);
std::thread::sleep(sleep);
}
*fin.lock() = true;
cv.notify_all();
});
BackgroundTaskHandle {
running,
finished,
condvar,
}
}
}
impl Default for StorageThreadPool {
fn default() -> Self {
Self::new().expect("storage thread pool construction cannot fail with defaults")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn test_spawn_one_shot() {
let pool = StorageThreadPool::new().unwrap();
let flag = Arc::new(AtomicBool::new(false));
let f = flag.clone();
pool.spawn(move || {
f.store(true, Ordering::Release);
});
for _ in 0..100 {
if flag.load(Ordering::Acquire) {
return;
}
std::thread::sleep(Duration::from_millis(5));
}
panic!("one-shot task did not run");
}
#[test]
fn test_spawn_periodic_stop_and_join() {
let pool = StorageThreadPool::new().unwrap();
let running = Arc::new(AtomicBool::new(false));
let passes = Arc::new(AtomicUsize::new(0));
let p = passes.clone();
let handle = pool.spawn_periodic(
running.clone(),
Duration::from_millis(10),
Duration::from_millis(1),
move || {
p.fetch_add(1, Ordering::Relaxed);
},
);
std::thread::sleep(Duration::from_millis(50));
assert!(passes.load(Ordering::Relaxed) > 0);
handle.stop();
handle.join();
let count_after_stop = passes.load(Ordering::Relaxed);
std::thread::sleep(Duration::from_millis(50));
assert_eq!(
passes.load(Ordering::Relaxed),
count_after_stop,
"loop must stop after stop()"
);
}
#[test]
fn test_pool_shared_across_clones() {
let pool = StorageThreadPool::new().unwrap();
let pool2 = pool.clone();
let pool3 = pool.clone();
assert_eq!(
pool.inner.current_num_threads(),
pool2.inner.current_num_threads()
);
assert_eq!(
pool2.inner.current_num_threads(),
pool3.inner.current_num_threads()
);
}
}