use std::{any::Any, collections::{HashMap, HashSet}, fmt::{self, Debug}, hash::Hash, io::{self, Write}, path::PathBuf};
use executor::ExecutorBuilder;
use log::trace;
use thiserror::Error;
use crossbeam_channel::SendError;
use petgraph::prelude::*;
use crate::executor::{Executor, ExecutorResult};
pub mod executor;
pub trait JobUnit: Debug + Any + Sized + Copy + Hash + Eq + Send {
fn deps(&self) -> Vec<Self>;
fn cmd(&self) -> Option<String>;
fn out_file(&self) -> PathBuf;
fn log_file(&self) -> Option<PathBuf>;
fn name(&self) -> &'static str;
fn nice(&self) -> u16 {
0
}
}
struct ExpandedDescr<'a, J: JobUnit>(&'a J);
impl<'a, J: JobUnit> Debug for ExpandedDescr<'a, J> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = self.0;
let mut dbg_struct = f.debug_struct(inner.name());
dbg_struct
.field("dependencies", &inner.deps())
.field("output file",&inner.out_file());
match &inner.cmd() {
Some(cmd) => dbg_struct.field("command",cmd),
x@None => dbg_struct.field("command",&x),
};
match &inner.log_file() {
Some(log) => dbg_struct.field("log",log),
x@None => dbg_struct.field("log",&x),
};
dbg_struct.finish()
}
}
#[derive(Debug)]
pub struct ToRun<J: JobUnit> {
job_idx: NodeIndex,
job_unit: J,
}
impl<J: JobUnit> ToRun<J> {
pub fn job_idx(&self) -> NodeIndex {
self.job_idx
}
pub fn job_unit(&self) -> &J {
&self.job_unit
}
}
pub struct Scheduler<ExB: ExecutorBuilder> {
ex_builder: ExB,
}
#[derive(Debug, Error)]
pub enum Error<J: JobUnit> {
#[error("error sending job to worker")]
ErrorSendingJob(#[from] SendError<J>),
#[error("system I/O error")]
IoError(#[from] io::Error),
#[error("Error in subcommand: {msg}")]
SubCommandError { msg: String },
#[error("Command-less job unit file is absent: {file}")]
MissingCommandLessOutputFile { file: PathBuf },
}
impl<ExB: ExecutorBuilder> Scheduler<ExB> {
pub fn new(ex_builder: ExB) -> Self {
Scheduler { ex_builder }
}
pub fn run<J: JobUnit>(self, final_job_units: Vec<J>) -> Result<(), Error<J>> {
let mut graph = petgraph::stable_graph::StableDiGraph::new();
let mut stack = final_job_units;
let mut already_processed = HashSet::new();
let mut indexes = HashMap::new();
while let Some(job_unit) = stack.pop() {
let newly_seen = already_processed.insert(job_unit);
if !newly_seen {
continue;
}
if job_unit.out_file().exists() {
continue;
}
let &mut ju_index = indexes
.entry(job_unit)
.or_insert_with_key(|j| graph.add_node(Some(*j)));
let deps = job_unit.deps();
for dep in deps {
if dep.out_file().exists() {
continue;
}
if !already_processed.contains(&dep) {
stack.push(dep);
}
let &mut dep_index = indexes
.entry(dep)
.or_insert_with_key(|j| graph.add_node(Some(*j)));
debug_assert!(!graph.contains_edge(ju_index, dep_index));
graph.add_edge(ju_index, dep_index, ());
}
}
let (executor, to_run_channel_sndr, done_channel_sndr, done_channel_rcvr) =
self.ex_builder.init();
let no_deps_nodes: Vec<_> = graph
.node_indices()
.filter(|&idx| graph.neighbors_directed(idx, Outgoing).next().is_none())
.collect();
let start_idx = graph.add_node(None);
for job_idx in no_deps_nodes {
graph.add_edge(job_idx, start_idx, ());
}
let start_count: usize = graph.node_count();
let mut done_count: usize = 0;
let mut failed_count: usize = 0;
done_channel_sndr
.send(ExecutorResult {
job_idx: start_idx,
result: Ok(()),
})
.unwrap();
let done_iter = done_channel_rcvr.into_iter();
trace!("Entering done_iter main loop.");
for done in done_iter {
trace!("Iterating done_iter main loop.");
let j_idx = done.job_idx;
if let Err(msg) = done.result {
trace!("Job errored.");
let job_unit = graph.node_weight(j_idx).unwrap().unwrap();
{
let stderr = io::stderr();
let mut lock = stderr.lock();
writeln!(
lock,
"There was an error running the following job: {:#?} interpreted as {:#?}\nMessage was: {}",
job_unit, ExpandedDescr(&job_unit), msg
)
.unwrap();
let mut stack = vec![j_idx];
while let Some(idx) = stack.pop() {
stack.extend(graph.neighbors_directed(idx, Incoming));
writeln!(
lock,
"Because a dependency failed, deleting job: {:?}\n",
graph.node_weight(idx).unwrap()
)
.unwrap();
graph.remove_node(idx);
failed_count += 1;
}
}
continue;
}
trace!("Job was succesful.");
let was_required_by: Vec<_> = graph.neighbors_directed(j_idx, Incoming).collect();
graph.remove_node(j_idx);
done_count += 1;
assert_eq!(done_count + failed_count + graph.node_count(), start_count);
trace!("Writting progress.");
writeln!(
io::stdout().lock(),
"{} done ({:5.1}%), {} failed ({:5.1}%), {} remaining ({:5.1}%)\n",
done_count,
100. * (done_count as f64) / (start_count as f64),
failed_count,
100. * (failed_count as f64) / (start_count as f64),
graph.node_count(),
100. * (graph.node_count() as f64) / (start_count as f64)
)
.unwrap();
if graph.node_count() == 0 {
break;
}
trace!("Submitting dependent jobs.");
for job_idx in was_required_by {
if graph.neighbors_directed(job_idx, Outgoing).count() == 0 {
let j_u = graph.node_weight(job_idx).unwrap().unwrap();
let out = j_u.out_file();
if out.exists() {
writeln!(
io::stdout().lock(),
"Output already present, skipping job: {:?}\n",
j_u
)
.unwrap();
done_channel_sndr
.send(ExecutorResult {
result: Ok(()),
job_idx,
})
.unwrap();
} else {
if j_u.cmd().is_none() {
return Err(Error::MissingCommandLessOutputFile {
file: j_u.out_file(),
});
}
to_run_channel_sndr
.send(ToRun {
job_idx,
job_unit: j_u,
})
.unwrap();
}
}
}
}
drop(to_run_channel_sndr);
executor.join();
Ok(())
}
}