use std::sync::Arc;
use batpak::store::{JobHandle, JobStatus, JoinError, Spawn, SpawnError};
struct SupervisedJob {
name: String,
handle: Box<dyn JobHandle>,
}
pub struct Supervisor {
spawn: Arc<dyn Spawn>,
running: Vec<SupervisedJob>,
}
impl Supervisor {
#[must_use]
pub fn new(spawn: Arc<dyn Spawn>) -> Self {
Self {
spawn,
running: Vec::new(),
}
}
pub fn spawn(
&mut self,
name: impl Into<String>,
stack_size: Option<usize>,
body: Box<dyn FnOnce() + Send + 'static>,
) -> Result<(), SpawnError> {
let name = name.into();
let handle = self.spawn.spawn(name.clone(), stack_size, body)?;
self.running.push(SupervisedJob { name, handle });
Ok(())
}
#[must_use]
pub fn job_count(&self) -> usize {
self.running.len()
}
#[must_use]
pub fn all_finished(&self) -> bool {
self.running.iter().all(|job| job.handle.is_finished())
}
#[must_use]
pub fn statuses(&self) -> Vec<(String, JobStatus)> {
self.running
.iter()
.map(|job| (job.name.clone(), job.handle.status()))
.collect()
}
#[must_use]
pub fn join_all(&mut self) -> Vec<(String, Result<(), JoinError>)> {
let jobs = std::mem::take(&mut self.running);
jobs.into_iter()
.map(|job| (job.name, job.handle.join()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::Supervisor;
use batpak::store::{JobStatus, ThreadSpawn};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
#[test]
fn spawns_runs_and_joins_a_body() {
let mut supervisor = Supervisor::new(Arc::new(ThreadSpawn));
let ran = Arc::new(AtomicBool::new(false));
let ran_in_body = Arc::clone(&ran);
supervisor
.spawn(
"unit",
None,
Box::new(move || ran_in_body.store(true, Ordering::Release)),
)
.expect("ThreadSpawn starts the body");
assert_eq!(supervisor.job_count(), 1);
let outcomes = supervisor.join_all();
assert_eq!(outcomes.len(), 1);
assert_eq!(outcomes[0].0, "unit");
assert!(outcomes[0].1.is_ok(), "a clean body joins Ok");
assert!(
ran.load(Ordering::Acquire),
"PROPERTY: the supervisor runs the supplied body to completion",
);
assert_eq!(supervisor.job_count(), 0, "join_all empties the supervisor");
}
#[test]
fn join_surfaces_a_panicking_body_without_losing_siblings() {
let mut supervisor = Supervisor::new(Arc::new(ThreadSpawn));
let cleaned = Arc::new(AtomicU32::new(0));
for index in 0..3 {
let cleaned_in_body = Arc::clone(&cleaned);
supervisor
.spawn(
format!("job-{index}"),
None,
Box::new(move || {
if index == 1 {
std::hint::black_box(Option::<()>::None)
.expect("intentional supervised-job panic proof");
}
cleaned_in_body.fetch_add(1, Ordering::AcqRel);
}),
)
.expect("spawn");
}
let outcomes = supervisor.join_all();
assert_eq!(outcomes.len(), 3, "every spawned job is joined");
assert!(outcomes[0].1.is_ok());
assert!(
outcomes[1].1.is_err(),
"PROPERTY: a panicking body surfaces as a join error",
);
assert!(outcomes[2].1.is_ok());
assert_eq!(
cleaned.load(Ordering::Acquire),
2,
"the two clean siblings still ran",
);
}
#[test]
fn status_tracks_a_gated_body_until_released() {
let mut supervisor = Supervisor::new(Arc::new(ThreadSpawn));
let gate = Arc::new(AtomicBool::new(false));
let gate_in_body = Arc::clone(&gate);
supervisor
.spawn(
"gated",
None,
Box::new(move || {
while !gate_in_body.load(Ordering::Acquire) {
std::hint::spin_loop();
}
}),
)
.expect("spawn");
assert_eq!(
supervisor.statuses(),
vec![("gated".to_owned(), JobStatus::Running)],
"a gated body reports Running before release",
);
gate.store(true, Ordering::Release);
let outcomes = supervisor.join_all();
assert!(outcomes[0].1.is_ok(), "released body joins Ok");
}
}