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,
Unattributable {
untagged_entries: usize,
},
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 mut untagged_entries = 0_usize;
let mut ours: Vec<&str> = Vec::new();
for line in content.lines() {
let Some(body) = timestamped_body(line) else {
continue;
};
match split_pid_frame(body) {
Some((entry_pid, entry)) => {
if entry_pid == pid {
ours.push(entry);
}
}
None => untagged_entries += 1,
}
}
let Some(start) = ours.iter().rposition(|entry| entry.starts_with("ARMED ")) else {
if untagged_entries > 0 {
return Ok(NoteFate::Unattributable { untagged_entries });
}
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 entry in &ours[start + 1..] {
if let Some(json) = entry.strip_prefix("OUTCOME ") {
match serde_json::from_str::<OutcomeRecord>(json) {
Ok(record) if record.pid == pid => outcome = Some(record),
Ok(other) => {
outcome_unreadable = Some(format!(
"an OUTCOME line framed for pid {pid} carries a record for pid {}",
other.pid
));
}
Err(parse_error) => {
outcome_unreadable =
Some(format!("an OUTCOME line does not parse: {parse_error}"));
}
}
} else if let Some(reason) = entry.strip_prefix("DISARMED ") {
disarmed_reason = Some(reason.to_owned());
break;
}
}
Ok(match disarmed_reason {
Some(reason) => NoteFate::Disarmed {
outcome,
outcome_unreadable,
reason,
},
None => NoteFate::ArmedNotDisarmed {
outcome,
outcome_unreadable,
},
})
}
fn split_pid_frame(body: &str) -> Option<(u32, &str)> {
let tagged = body.strip_prefix("pid=")?;
let (digits, entry) = tagged.split_once(' ')?;
Some((digits.parse().ok()?, entry))
}
fn timestamped_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;