use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::PathBuf;
use crate::plan::{DeliverableStatus, FileMode, LockInfo, OwnedFile, PlanGraph};
use chrono::{DateTime, Utc};
use crate::task::CriticalPathResult;
pub(crate) struct PlanState {
pub(crate) graph: PlanGraph,
pub(crate) statuses: HashMap<String, DeliverableStatus>,
pub(crate) attempt_counts: HashMap<String, u32>,
pub(crate) failure_counts: HashMap<String, u32>,
pub(crate) lapse_counts: HashMap<String, u32>,
pub(crate) locks: HashMap<String, LockInfo>,
pub(crate) lockless: HashSet<String>,
pub(crate) file_claims: HashMap<PathBuf, FileClaim>,
pub(crate) cached_result: CriticalPathResult,
pub(crate) leased_hours: HashMap<String, f32>,
pub(crate) reported_actuals: HashMap<String, crate::ev_store::ReportedActuals>,
pub(crate) actuals_updated_at: Option<DateTime<Utc>>,
}
pub(crate) struct ActualsDeltas {
pub(crate) leased_hours: HashMap<String, f32>,
pub(crate) reported: HashMap<String, crate::ev_store::ReportedActuals>,
pub(crate) at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FileClaim {
Exclusive(String),
Append(BTreeSet<String>),
}
pub(crate) fn modes_conflict(requested: FileMode, held: FileMode) -> bool {
!(requested == FileMode::Append && held == FileMode::Append)
}
impl FileClaim {
pub(crate) fn conflicts_with(&self, requested: FileMode) -> bool {
let held = match self {
FileClaim::Exclusive(_) => FileMode::Exclusive,
FileClaim::Append(_) => FileMode::Append,
};
modes_conflict(requested, held)
}
}
pub(crate) fn add_file_claims(
claims: &mut HashMap<PathBuf, FileClaim>,
deliverable_id: &str,
files: &[OwnedFile],
) {
for f in files {
match f.mode() {
FileMode::Exclusive => {
debug_assert!(
!matches!(claims.get(f.path()), Some(FileClaim::Append(_))),
"exclusive claim by {deliverable_id} on a path held by append claims"
);
claims.insert(
f.path().to_path_buf(),
FileClaim::Exclusive(deliverable_id.to_string()),
);
}
FileMode::Append => match claims.entry(f.path().to_path_buf()) {
std::collections::hash_map::Entry::Occupied(mut e) => {
debug_assert!(
matches!(e.get(), FileClaim::Append(_)),
"append claim by {deliverable_id} on a path held exclusively"
);
if let FileClaim::Append(set) = e.get_mut() {
set.insert(deliverable_id.to_string());
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(FileClaim::Append(BTreeSet::from([
deliverable_id.to_string()
])));
}
},
}
}
}
pub(crate) fn release_file_claims(
claims: &mut HashMap<PathBuf, FileClaim>,
deliverable_id: &str,
files: &[OwnedFile],
) {
for f in files {
let drop_path = match claims.get_mut(f.path()) {
Some(FileClaim::Exclusive(owner)) => owner == deliverable_id,
Some(FileClaim::Append(set)) => {
set.remove(deliverable_id);
set.is_empty()
}
None => false,
};
if drop_path {
claims.remove(f.path());
}
}
}
pub(crate) fn hours_between(start: DateTime<Utc>, end: DateTime<Utc>) -> f32 {
let micros = (end - start).num_microseconds().unwrap_or(i64::MAX);
(micros.max(0) as f64 / 3_600_000_000.0) as f32
}
pub(crate) fn rederive_status(
d: &crate::plan::Deliverable,
statuses: &HashMap<String, DeliverableStatus>,
) -> DeliverableStatus {
if crate::graph::prerequisite_ids(d)
.all(|p| statuses.get(p) == Some(&DeliverableStatus::Complete))
{
DeliverableStatus::Ready
} else {
DeliverableStatus::Pending
}
}
impl PlanState {
pub(crate) fn new(
graph: PlanGraph,
statuses: HashMap<String, DeliverableStatus>,
cached_result: CriticalPathResult,
) -> Self {
Self {
graph,
statuses,
attempt_counts: HashMap::new(),
failure_counts: HashMap::new(),
lapse_counts: HashMap::new(),
locks: HashMap::new(),
lockless: HashSet::new(),
file_claims: HashMap::new(),
cached_result,
leased_hours: HashMap::new(),
reported_actuals: HashMap::new(),
actuals_updated_at: None,
}
}
pub(crate) fn take_actuals_deltas(&mut self) -> ActualsDeltas {
ActualsDeltas {
leased_hours: std::mem::take(&mut self.leased_hours),
reported: std::mem::take(&mut self.reported_actuals),
at: self.actuals_updated_at.take(),
}
}
fn touch_actuals(&mut self, at: DateTime<Utc>) {
self.actuals_updated_at = Some(self.actuals_updated_at.map_or(at, |t| t.max(at)));
}
pub(crate) fn report_actuals(
&mut self,
deliverable_id: &str,
report: crate::ev_store::ReportedActuals,
at: DateTime<Utc>,
) {
if report == crate::ev_store::ReportedActuals::default() {
return;
}
self.reported_actuals
.insert(deliverable_id.to_string(), report);
self.touch_actuals(at);
}
pub(crate) fn record_lease_end(&mut self, lock: &LockInfo, end: DateTime<Utc>) {
*self
.leased_hours
.entry(lock.deliverable_id.clone())
.or_insert(0.0) += hours_between(lock.acquired_at, end);
self.touch_actuals(end);
}
pub(crate) fn end_lease(
&mut self,
deliverable_id: &str,
end: DateTime<Utc>,
) -> Option<LockInfo> {
let lock = self.locks.remove(deliverable_id)?;
if let Some(d) = self
.graph
.deliverables
.iter()
.find(|d| d.id == deliverable_id)
{
release_file_claims(&mut self.file_claims, deliverable_id, &d.owned_files);
}
self.record_lease_end(&lock, end);
Some(lock)
}
pub(crate) fn is_lockless_in_progress(&self, deliverable_id: &str) -> bool {
self.lockless.contains(deliverable_id)
&& self.statuses.get(deliverable_id) == Some(&DeliverableStatus::InProgress)
&& !self.locks.contains_key(deliverable_id)
}
pub(crate) fn attempt_count(&self, deliverable_id: &str) -> u32 {
self.attempt_counts
.get(deliverable_id)
.copied()
.unwrap_or(0)
}
pub(crate) fn failure_count(&self, deliverable_id: &str) -> u32 {
self.failure_counts
.get(deliverable_id)
.copied()
.unwrap_or(0)
}
pub(crate) fn lapse_count(&self, deliverable_id: &str) -> u32 {
self.lapse_counts.get(deliverable_id).copied().unwrap_or(0)
}
pub(crate) fn reap_expired(&mut self, now: DateTime<Utc>) -> Vec<LockInfo> {
let expired_ids: Vec<String> = self
.locks
.iter()
.filter(|(_, info)| info.expires_at < now)
.map(|(id, _)| id.clone())
.collect();
let mut reaped = Vec::with_capacity(expired_ids.len());
for id in expired_ids {
let end = self.locks.get(&id).map_or(now, |l| l.expires_at.min(now));
if let Some(info) = self.end_lease(&id, end) {
*self.lapse_counts.entry(id.clone()).or_insert(0) += 1;
let status = self
.graph
.deliverables
.iter()
.find(|d| d.id == id)
.map_or(DeliverableStatus::Ready, |d| {
rederive_status(d, &self.statuses)
});
self.statuses.insert(id, status);
reaped.push(info);
}
}
reaped
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn releasing_one_append_holder_keeps_the_other_claim() {
let files = vec![OwnedFile::Claim {
path: PathBuf::from("R.md"),
mode: Some(FileMode::Append),
}];
let mut claims = HashMap::new();
add_file_claims(&mut claims, "a", &files);
add_file_claims(&mut claims, "b", &files);
release_file_claims(&mut claims, "a", &files);
assert_eq!(
claims.get(&PathBuf::from("R.md")),
Some(&FileClaim::Append(BTreeSet::from(["b".to_string()])))
);
}
#[test]
fn file_claim_conflicts_unless_both_sides_append() {
let ex = FileClaim::Exclusive("a".into());
let ap = FileClaim::Append(BTreeSet::from(["a".to_string()]));
assert!(ex.conflicts_with(FileMode::Exclusive));
assert!(ex.conflicts_with(FileMode::Append));
assert!(ap.conflicts_with(FileMode::Exclusive));
assert!(!ap.conflicts_with(FileMode::Append));
}
#[test]
fn reaped_lease_with_incomplete_prerequisites_becomes_pending() {
use crate::plan::{CallerId, Deliverable, PlanId, Prerequisite};
let dl = |id: &str, pre: &[&str]| Deliverable {
id: id.to_string(),
owned_files: vec![],
prerequisites: pre
.iter()
.map(|p| Prerequisite::Id((*p).to_string()))
.collect(),
estimated_effort_hours: Some(1.0),
duration_hours: None,
estimate: None,
metadata: serde_json::json!({}),
milestone: false,
earning_rule: None,
};
let graph = PlanGraph {
deliverables: vec![dl("a", &[]), dl("b", &["a"])],
max_chained_dispatch: None,
};
let cpm = crate::schedule::compute_cpm(&graph).expect("valid");
let statuses = HashMap::from([
("a".to_string(), DeliverableStatus::Ready),
("b".to_string(), DeliverableStatus::InProgress),
]);
let mut state = PlanState::new(graph, statuses, cpm);
let now = Utc::now();
state.locks.insert(
"b".into(),
LockInfo {
plan_id: PlanId("p".into()),
deliverable_id: "b".into(),
caller_id: CallerId("w".into()),
acquired_at: now,
expires_at: now - chrono::Duration::hours(1),
},
);
state.reap_expired(now);
assert_eq!(state.statuses["b"], DeliverableStatus::Pending);
}
}