use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::ServerError;
use crate::shutdown::ShutdownOutcome;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ParkedWorkerRecord {
pub worker: String,
pub queue: Option<String>,
pub tasks: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OutcomeRecord {
pub pid: u32,
pub outcome: ShutdownOutcome,
pub drain_timeout_seconds: u64,
pub delivered_drain_requests: usize,
pub parked: Vec<ParkedWorkerRecord>,
#[serde(default)]
pub parked_declared_commands: Vec<String>,
pub managed_workers_stopped: Vec<String>,
pub managed_workers_unstopped: Vec<String>,
}
impl OutcomeRecord {
#[must_use]
pub fn from_report(pid: u32, report: &crate::shutdown::ShutdownReport) -> Self {
Self {
pid,
outcome: report.outcome,
drain_timeout_seconds: report.drain_timeout.as_secs(),
delivered_drain_requests: report.delivered_drain_requests,
parked: report
.parked
.iter()
.map(|lost| ParkedWorkerRecord {
worker: lost.worker_id.value().to_string(),
queue: lost.task_queue.clone(),
tasks: lost
.tasks
.iter()
.map(|task| {
format!("{}/{}#{}", task.workflow_id, task.activity_id, task.attempt)
})
.collect(),
})
.collect(),
parked_declared_commands: report
.parked_declared
.iter()
.map(|key| format!("{}/{}#{}", key.workflow_id, key.activity_id, key.attempt))
.collect(),
managed_workers_stopped: report.managed_workers_stopped.clone(),
managed_workers_unstopped: report.managed_workers_unstopped.clone(),
}
}
}
const OUTCOME_KIND: &str = "OUTCOME";
pub fn render_entry(record: &OutcomeRecord) -> Result<String, ServerError> {
let json = serde_json::to_string(record).map_err(|serialize_error| ServerError::DeathNote {
message: format!("cannot serialize the shutdown outcome record: {serialize_error}"),
})?;
Ok(format!("{OUTCOME_KIND} {json}"))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum NoteFate {
NoNote,
NoBracketForPid,
ArmedNotDisarmed {
outcome: Option<OutcomeRecord>,
outcome_unreadable: Option<String>,
},
Disarmed {
outcome: Option<OutcomeRecord>,
outcome_unreadable: Option<String>,
reason: String,
},
}
pub fn read_fate(home: &Path, pid: u32) -> Result<NoteFate, ServerError> {
let path = crate::death_note::note_path(home);
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(io_error) if io_error.kind() == std::io::ErrorKind::NotFound => {
return Ok(NoteFate::NoNote);
}
Err(io_error) => {
return Err(ServerError::DeathNote {
message: format!(
"cannot read the death note `{}`: {io_error}",
path.display()
),
});
}
};
let armed_marker = format!("ARMED pid={pid} ");
let mut bracket_start: Option<usize> = None;
let lines: Vec<&str> = content.lines().collect();
for (index, line) in lines.iter().enumerate() {
if entry_body(line).is_some_and(|body| body.starts_with(&armed_marker)) {
bracket_start = Some(index);
}
}
let Some(start) = bracket_start else {
return Ok(NoteFate::NoBracketForPid);
};
let mut outcome: Option<OutcomeRecord> = None;
let mut outcome_unreadable: Option<String> = None;
let mut disarmed_reason: Option<String> = None;
for line in &lines[start + 1..] {
let Some(body) = entry_body(line) else {
continue;
};
if let Some(json) = body.strip_prefix("OUTCOME ") {
match serde_json::from_str::<OutcomeRecord>(json) {
Ok(record) if record.pid == pid => outcome = Some(record),
Ok(_other_pid) => {}
Err(parse_error) => {
outcome_unreadable =
Some(format!("an OUTCOME line does not parse: {parse_error}"));
}
}
} else if let Some(reason) = body.strip_prefix("DISARMED ") {
disarmed_reason = Some(reason.to_owned());
break;
} else if body.starts_with("ARMED pid=") {
break;
}
}
Ok(match disarmed_reason {
Some(reason) => NoteFate::Disarmed {
outcome,
outcome_unreadable,
reason,
},
None => NoteFate::ArmedNotDisarmed {
outcome,
outcome_unreadable,
},
})
}
fn entry_body(line: &str) -> Option<&str> {
let (timestamp, body) = line.split_once(' ')?;
if timestamp.len() >= 20
&& timestamp
.chars()
.take(4)
.all(|character| character.is_ascii_digit())
{
Some(body)
} else {
None
}
}
#[cfg(test)]
#[path = "outcome_tests.rs"]
mod tests;