use std::thread;
use std::time::{Duration, Instant};
use crate::{AppError, AppResult, ErrorCode};
const POLL_INTERVAL: Duration = Duration::from_millis(5);
pub(crate) fn join_within(
handle: Option<thread::JoinHandle<AppResult<()>>>,
grace: Duration,
) -> AppResult<()> {
let Some(handle) = handle else {
return Ok(());
};
let deadline = Instant::now() + grace;
while !handle.is_finished() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(());
}
thread::sleep(remaining.min(POLL_INTERVAL));
}
match handle.join() {
Ok(result) => result,
Err(_panic) => Err(AppError::new(
ErrorCode::Internal,
"process worker thread panicked",
)),
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use super::*;
#[test]
fn join_within_handles_absent_finished_failed_and_panicked_workers() {
join_within(None, Duration::from_millis(10)).unwrap();
let ok = thread::spawn(|| Ok(()));
join_within(Some(ok), Duration::from_secs(1)).unwrap();
let failed = thread::spawn(|| Err(AppError::new(ErrorCode::Internal, "worker failed")));
assert_eq!(
join_within(Some(failed), Duration::from_secs(1))
.unwrap_err()
.code(),
ErrorCode::Internal
);
let panicked = thread::spawn(|| -> AppResult<()> { panic!("worker panic") });
assert_eq!(
join_within(Some(panicked), Duration::from_secs(1))
.unwrap_err()
.code(),
ErrorCode::Internal
);
}
#[test]
fn join_within_detaches_a_worker_that_outlives_the_grace_period() {
let (unblock_tx, unblock_rx) = mpsc::channel::<()>();
let slow = thread::spawn(move || {
let _ = unblock_rx.recv();
Ok(())
});
join_within(Some(slow), Duration::from_millis(20)).unwrap();
let _ = unblock_tx.send(());
}
}