use std::collections::BTreeMap;
use std::path::Path;
use std::time::SystemTime;
use argonauts_core::events::task::TaskState;
use argonauts_core::events::{
DivideEvent, FallbackEvent, ReviewEvent, RunEvent, RunEventKind, TaskEvent,
};
use argonauts_core::layout::run_files;
use argonauts_core::names::ModelRef;
use argonauts_core::steps::judgment::ReviewVerdict;
use argonauts_core::{RunId, TaskId};
use time::OffsetDateTime;
#[derive(Debug, Default)]
pub(super) struct Records {
pub reviews: Vec<Review>,
pub tasks: Vec<Task>,
pub fallbacks: Vec<Fallback>,
}
#[derive(Debug, Clone)]
pub(super) struct Review {
pub run: RunId,
pub task_id: Option<TaskId>,
pub task: Option<String>,
pub reviewer: ModelRef,
pub verdict: ReviewVerdict,
pub summary: String,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub(super) struct Task {
pub run: RunId,
pub task_id: TaskId,
pub title: String,
pub state: Option<TaskState>,
pub worker: Option<ModelRef>,
}
#[derive(Debug, Clone)]
pub(super) struct Fallback {
pub run: RunId,
pub task_id: TaskId,
pub task: String,
pub stopped: ModelRef,
pub reason: String,
pub to: Option<ModelRef>,
}
const KINDS: [&[u8]; 4] = [b"task", b"review", b"fallback", b"divide"];
pub(super) fn read(runs_dir: &Path, since: OffsetDateTime) -> Records {
let mut records = Records::default();
let Ok(entries) = std::fs::read_dir(runs_dir) else {
return records;
};
let mut journals: Vec<_> = entries
.filter_map(Result::ok)
.map(|entry| entry.path().join(run_files::EVENTS))
.filter(|path| written_since(path, since))
.collect();
journals.sort();
for journal in journals {
if let Ok(bytes) = std::fs::read(&journal) {
read_run(&bytes, since, &mut records);
}
}
records
}
fn written_since(path: &Path, since: OffsetDateTime) -> bool {
let Ok(modified) = std::fs::metadata(path).and_then(|meta| meta.modified()) else {
return false;
};
let since = SystemTime::from(since);
modified >= since
}
fn read_run(bytes: &[u8], since: OffsetDateTime, records: &mut Records) {
let mut tasks: BTreeMap<TaskId, Task> = BTreeMap::new();
let mut created: BTreeMap<TaskId, OffsetDateTime> = BTreeMap::new();
let mut fallbacks: Vec<Fallback> = Vec::new();
for line in bytes.split(|byte| *byte == b'\n') {
if !kind_of(line).is_some_and(|kind| KINDS.contains(&kind)) {
continue;
}
let Ok(event) = serde_json::from_slice::<RunEvent>(line) else {
continue;
};
let task_id = event.task_id.clone();
match event.kind {
RunEventKind::Task(TaskEvent::Created(spec)) => {
created.insert(spec.id.clone(), event.ts);
tasks.insert(
spec.id.clone(),
Task {
run: event.run_id.clone(),
task_id: spec.id.clone(),
title: spec.title.clone(),
state: None,
worker: spec.worker.clone(),
},
);
}
RunEventKind::Task(TaskEvent::State { to, .. }) => {
if let Some(task) = task_id.as_ref().and_then(|task_id| tasks.get_mut(task_id)) {
task.state = Some(to);
}
}
RunEventKind::Task(TaskEvent::Try { worker, .. }) => {
if let Some(task) = task_id.as_ref().and_then(|task_id| tasks.get_mut(task_id)) {
task.worker = Some(worker);
}
}
RunEventKind::Divide(DivideEvent::Worker {
task: task_id,
worker,
..
}) => {
if let Some(task) = tasks.get_mut(&task_id) {
task.worker = Some(worker);
}
}
RunEventKind::Review(ReviewEvent::Reviewed {
reviewer, review, ..
}) if event.ts >= since => {
let task = task_id
.as_ref()
.and_then(|task_id| tasks.get(task_id))
.map(|task| task.title.clone());
let comments = review
.comments
.iter()
.map(|comment| {
let place = match comment.line {
Some(line) => format!("{}:{line}", comment.path),
None => comment.path.clone(),
};
let severity = format!("{:?}", comment.severity).to_lowercase();
format!("[{severity}] {place} {}", comment.text.trim())
})
.collect();
records.reviews.push(Review {
run: event.run_id.clone(),
task_id,
task,
reviewer,
verdict: review.verdict,
summary: review.summary.trim().to_owned(),
comments,
});
}
RunEventKind::Fallback(FallbackEvent::Needed(plan)) if event.ts >= since => {
fallbacks.push(Fallback {
run: event.run_id.clone(),
task_id: plan.task.clone(),
task: plan.title.clone(),
stopped: plan.stopped.clone(),
reason: plan.reason.clone(),
to: None,
});
}
RunEventKind::Fallback(FallbackEvent::FellBack {
task: task_id, to, ..
}) => {
let waiting = fallbacks
.iter_mut()
.rev()
.find(|fallback| fallback.task_id == task_id && fallback.to.is_none());
if let Some(fallback) = waiting {
fallback.to = Some(to);
}
}
_ => {}
}
}
records.tasks.extend(
tasks
.into_values()
.filter(|task| created.get(&task.task_id).is_some_and(|at| *at >= since)),
);
records.fallbacks.extend(fallbacks);
}
fn kind_of(line: &[u8]) -> Option<&[u8]> {
const TAG: &[u8] = br#""kind":{"type":""#;
let start = line.windows(TAG.len()).position(|window| window == TAG)? + TAG.len();
let len = line[start..].iter().position(|byte| *byte == b'"')?;
Some(&line[start..start + len])
}