use super::RunStatus;
use crate::base::{Error, ErrorKind, WorkflowIx};
use crate::observe::{Selection, Trace, WorkflowExit, WorkflowRecord};
use crate::program::Program;
use crate::state::Stats;
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
#[derive(Debug)]
pub(crate) struct Record {
pub(crate) ix: WorkflowIx,
pub(crate) selection: Selection,
pub(crate) sweeps: u32,
pub(crate) exit: Option<WorkflowExit>,
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ArenaStats {
pub views: u32,
pub checkpoints: u32,
pub live_max: usize,
pub peak_arena: usize,
}
pub struct Run {
program: Program,
status: RunStatus,
records: Vec<Record>,
stats: Stats,
trace: Option<Box<Trace>>,
}
impl Run {
pub(crate) fn new(
program: Program,
status: RunStatus,
records: Vec<Record>,
stats: Stats,
trace: Option<Box<Trace>>,
) -> Self {
Self {
program,
status,
records,
stats,
trace,
}
}
pub fn trace(&self) -> Option<&Trace> {
self.trace.as_deref()
}
pub fn status(&self) -> &RunStatus {
&self.status
}
pub fn workflows(&self) -> impl Iterator<Item = WorkflowRecord<'_>> {
self.records.iter().map(|r| WorkflowRecord {
id: &self.program.workflows()[r.ix.index() as usize].id,
index: r.ix,
selection: &r.selection,
sweeps: r.sweeps,
exit: r.exit,
})
}
pub fn matched(&self) -> usize {
self.records
.iter()
.filter(|r| matches!(r.selection, Selection::Ran))
.count()
}
pub fn retryable(&self) -> bool {
match &self.status {
RunStatus::Stopped(e) => e.is_retryable(),
RunStatus::TimedOut => true,
_ => false,
}
}
pub fn partial(&self) -> bool {
matches!(
self.status,
RunStatus::Stopped(_) | RunStatus::TimedOut | RunStatus::Cancelled
)
}
pub fn take_trace(&mut self) -> Option<Trace> {
self.trace.take().map(|trace| *trace)
}
pub fn into_result(self) -> crate::Result<Self> {
match self.status {
RunStatus::Stopped(e) => Err(e),
RunStatus::TimedOut => Err(Error::new(ErrorKind::TimedOut, "the run timed out")),
RunStatus::Cancelled => Err(Error::new(ErrorKind::Cancelled, "the run was cancelled")),
RunStatus::Completed | RunStatus::CompletedWithErrors => Ok(self),
}
}
#[doc(hidden)]
pub fn arena_stats(&self) -> ArenaStats {
ArenaStats {
views: self.stats.views,
checkpoints: self.stats.checkpoints,
live_max: self.stats.live_max,
peak_arena: self.stats.peak_arena,
}
}
}
impl Serialize for Run {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
struct Records<'r>(&'r Run);
impl Serialize for Records<'_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.0.workflows())
}
}
let mut o = s.serialize_struct("Run", 6)?;
o.serialize_field("status", &self.status)?;
match &self.status {
RunStatus::Stopped(e) => o.serialize_field("error", e)?,
_ => o.skip_field("error")?,
}
o.serialize_field("retryable", &self.retryable())?;
o.serialize_field("partial", &self.partial())?;
o.serialize_field("matched", &self.matched())?;
o.serialize_field("workflows", &Records(self))?;
o.end()
}
}
#[cfg(feature = "ts")]
crate::base::ts::ts_decl!(
Run,
"Run",
[
" A run as JSON: how it ended and what each workflow did. Neither the trace",
" nor the arena counters are part of it.",
],
|cfg| {
use crate::base::ts::{Prop, object};
object(&[
Prop::new("status", RunStatus::name(cfg), &[" How the run ended."]),
Prop::optional(
"error",
Error::name(cfg),
&[" The error that stopped the run; present only when `status` is `stopped`."],
),
Prop::new(
"retryable",
"boolean",
&[
" May running the message again succeed? True for a timeout and for a",
" stop whose error says so.",
],
),
Prop::new(
"partial",
"boolean",
&[" Did the run end before every workflow was considered?"],
),
Prop::new(
"matched",
"number",
&[" How many workflows were selected and ran."],
),
Prop::new(
"workflows",
Vec::<WorkflowRecord<'static>>::name(cfg),
&[
" Every workflow considered, in program order, up to and including the",
" one that stopped the run.",
],
),
])
}
);
impl std::fmt::Debug for Run {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Run")
.field("status", &self.status)
.field("records", &self.records)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Registry;
use crate::def::{ParseOptions, parse};
use crate::program::CompileOptions;
use serde_json::json;
fn program() -> Program {
let compiler = Registry::default().compiler(CompileOptions::default());
let ws = ["a", "b", "c", "d", "e"].map(|id| {
let json = json!({"id": id, "tasks": [
{"id": "t", "function": {"name": "log", "input": {"message": "hi"}}}
]});
let def = parse(&json, ParseOptions::default()).def.unwrap();
compiler.compile_one(&def).unwrap().workflow
});
compiler.link(ws)
}
fn record(i: u32, selection: Selection, exit: Option<WorkflowExit>) -> Record {
Record {
ix: WorkflowIx::new(i),
selection,
sweeps: u32::from(exit.is_some()),
exit,
}
}
fn records() -> Vec<Record> {
vec![
record(0, Selection::Ran, Some(WorkflowExit::Completed)),
record(1, Selection::SkippedByCondition, None),
record(
2,
Selection::ConditionFailed(Error::new(ErrorKind::Eval, "x")),
None,
),
record(3, Selection::SkippedByRollout, None),
record(4, Selection::Ran, Some(WorkflowExit::Stopped)),
]
}
fn run(status: RunStatus) -> Run {
Run::new(program(), status, records(), Stats::default(), None)
}
#[test]
fn a_run_reports_its_workflows_and_their_exits() {
let r = run(RunStatus::Completed);
assert_eq!(r.matched(), 2);
assert!(!r.retryable() && !r.partial());
let seen: Vec<_> = r.workflows().map(|w| (w.id, w.sweeps, w.exit)).collect();
assert_eq!(seen[0], ("a", 1, Some(WorkflowExit::Completed)));
assert_eq!(seen[1], ("b", 0, None));
assert_eq!(seen[4], ("e", 1, Some(WorkflowExit::Stopped)));
assert!(matches!(
r.workflows().nth(2).unwrap().selection,
Selection::ConditionFailed(_)
));
assert!(r.into_result().is_ok());
assert!(run(RunStatus::CompletedWithErrors).into_result().is_ok());
assert!(!run(RunStatus::CompletedWithErrors).partial());
let r = run(RunStatus::TimedOut);
assert!(r.retryable() && r.partial());
let e = r.into_result().unwrap_err();
assert_eq!(e.code(), "TIMEOUT_ERROR");
let r = run(RunStatus::Cancelled);
assert!(!r.retryable() && r.partial());
assert_eq!(r.into_result().unwrap_err().code(), "CANCELLED");
let r = run(RunStatus::Stopped(
Error::service("X", "m").with_retryable(true),
));
assert!(r.retryable() && r.partial());
assert_eq!(r.into_result().unwrap_err().code(), "X");
let r = run(RunStatus::Stopped(Error::service("X", "m")));
assert!(!r.retryable() && r.partial());
}
#[test]
fn arena_stats_come_from_the_state_stats() {
let stats = Stats {
views: 2,
checkpoints: 3,
live_max: 40,
peak_arena: 50,
..Stats::default()
};
let r = Run::new(program(), RunStatus::Completed, Vec::new(), stats, None);
let a = r.arena_stats();
assert_eq!(
(a.views, a.checkpoints, a.live_max, a.peak_arena),
(2, 3, 40, 50)
);
}
}