1use super::RunStatus;
4use crate::base::{Error, ErrorKind, WorkflowIx};
5use crate::observe::{Selection, Trace, WorkflowExit, WorkflowRecord};
6use crate::program::Program;
7use crate::state::Stats;
8use serde::ser::SerializeStruct;
9use serde::{Serialize, Serializer};
10
11#[derive(Debug)]
13pub(crate) struct Record {
14 pub(crate) ix: WorkflowIx,
15 pub(crate) selection: Selection,
16 pub(crate) sweeps: u32,
17 pub(crate) exit: Option<WorkflowExit>,
18}
19
20#[doc(hidden)]
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct ArenaStats {
25 pub views: u32,
27 pub checkpoints: u32,
29 pub live_max: usize,
31 pub peak_arena: usize,
33}
34
35pub struct Run {
38 program: Program,
39 status: RunStatus,
40 records: Vec<Record>,
41 stats: Stats,
42 trace: Option<Box<Trace>>,
45}
46
47impl Run {
48 pub(crate) fn new(
49 program: Program,
50 status: RunStatus,
51 records: Vec<Record>,
52 stats: Stats,
53 trace: Option<Box<Trace>>,
54 ) -> Self {
55 Self {
56 program,
57 status,
58 records,
59 stats,
60 trace,
61 }
62 }
63
64 pub fn trace(&self) -> Option<&Trace> {
70 self.trace.as_deref()
71 }
72
73 pub fn status(&self) -> &RunStatus {
75 &self.status
76 }
77
78 pub fn workflows(&self) -> impl Iterator<Item = WorkflowRecord<'_>> {
81 self.records.iter().map(|r| WorkflowRecord {
82 id: &self.program.workflows()[r.ix.index() as usize].id,
83 index: r.ix,
84 selection: &r.selection,
85 sweeps: r.sweeps,
86 exit: r.exit,
87 })
88 }
89
90 pub fn matched(&self) -> usize {
92 self.records
93 .iter()
94 .filter(|r| matches!(r.selection, Selection::Ran))
95 .count()
96 }
97
98 pub fn retryable(&self) -> bool {
101 match &self.status {
102 RunStatus::Stopped(e) => e.is_retryable(),
103 RunStatus::TimedOut => true,
104 _ => false,
105 }
106 }
107
108 pub fn partial(&self) -> bool {
110 matches!(
111 self.status,
112 RunStatus::Stopped(_) | RunStatus::TimedOut | RunStatus::Cancelled
113 )
114 }
115
116 pub fn take_trace(&mut self) -> Option<Trace> {
121 self.trace.take().map(|trace| *trace)
122 }
123
124 pub fn into_result(self) -> crate::Result<Self> {
128 match self.status {
129 RunStatus::Stopped(e) => Err(e),
130 RunStatus::TimedOut => Err(Error::new(ErrorKind::TimedOut, "the run timed out")),
131 RunStatus::Cancelled => Err(Error::new(ErrorKind::Cancelled, "the run was cancelled")),
132 RunStatus::Completed | RunStatus::CompletedWithErrors => Ok(self),
133 }
134 }
135
136 #[doc(hidden)]
138 pub fn arena_stats(&self) -> ArenaStats {
139 ArenaStats {
140 views: self.stats.views,
141 checkpoints: self.stats.checkpoints,
142 live_max: self.stats.live_max,
143 peak_arena: self.stats.peak_arena,
144 }
145 }
146}
147
148impl Serialize for Run {
154 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
155 struct Records<'r>(&'r Run);
157 impl Serialize for Records<'_> {
158 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
159 s.collect_seq(self.0.workflows())
160 }
161 }
162
163 let mut o = s.serialize_struct("Run", 6)?;
164 o.serialize_field("status", &self.status)?;
165 match &self.status {
166 RunStatus::Stopped(e) => o.serialize_field("error", e)?,
167 _ => o.skip_field("error")?,
168 }
169 o.serialize_field("retryable", &self.retryable())?;
170 o.serialize_field("partial", &self.partial())?;
171 o.serialize_field("matched", &self.matched())?;
172 o.serialize_field("workflows", &Records(self))?;
173 o.end()
174 }
175}
176
177#[cfg(feature = "ts")]
178crate::base::ts::ts_decl!(
179 Run,
180 "Run",
181 [
182 " A run as JSON: how it ended and what each workflow did. Neither the trace",
183 " nor the arena counters are part of it.",
184 ],
185 |cfg| {
186 use crate::base::ts::{Prop, object};
187 object(&[
188 Prop::new("status", RunStatus::name(cfg), &[" How the run ended."]),
189 Prop::optional(
190 "error",
191 Error::name(cfg),
192 &[" The error that stopped the run; present only when `status` is `stopped`."],
193 ),
194 Prop::new(
195 "retryable",
196 "boolean",
197 &[
198 " May running the message again succeed? True for a timeout and for a",
199 " stop whose error says so.",
200 ],
201 ),
202 Prop::new(
203 "partial",
204 "boolean",
205 &[" Did the run end before every workflow was considered?"],
206 ),
207 Prop::new(
208 "matched",
209 "number",
210 &[" How many workflows were selected and ran."],
211 ),
212 Prop::new(
213 "workflows",
214 Vec::<WorkflowRecord<'static>>::name(cfg),
215 &[
216 " Every workflow considered, in program order, up to and including the",
217 " one that stopped the run.",
218 ],
219 ),
220 ])
221 }
222);
223
224impl std::fmt::Debug for Run {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 f.debug_struct("Run")
227 .field("status", &self.status)
228 .field("records", &self.records)
229 .finish_non_exhaustive()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::Registry;
237 use crate::def::{ParseOptions, parse};
238 use crate::program::CompileOptions;
239 use serde_json::json;
240
241 fn program() -> Program {
242 let compiler = Registry::default().compiler(CompileOptions::default());
243 let ws = ["a", "b", "c", "d", "e"].map(|id| {
244 let json = json!({"id": id, "tasks": [
245 {"id": "t", "function": {"name": "log", "input": {"message": "hi"}}}
246 ]});
247 let def = parse(&json, ParseOptions::default()).def.unwrap();
248 compiler.compile_one(&def).unwrap().workflow
249 });
250 compiler.link(ws)
251 }
252
253 fn record(i: u32, selection: Selection, exit: Option<WorkflowExit>) -> Record {
254 Record {
255 ix: WorkflowIx::new(i),
256 selection,
257 sweeps: u32::from(exit.is_some()),
258 exit,
259 }
260 }
261
262 fn records() -> Vec<Record> {
263 vec![
264 record(0, Selection::Ran, Some(WorkflowExit::Completed)),
265 record(1, Selection::SkippedByCondition, None),
266 record(
267 2,
268 Selection::ConditionFailed(Error::new(ErrorKind::Eval, "x")),
269 None,
270 ),
271 record(3, Selection::SkippedByRollout, None),
272 record(4, Selection::Ran, Some(WorkflowExit::Stopped)),
273 ]
274 }
275
276 fn run(status: RunStatus) -> Run {
277 Run::new(program(), status, records(), Stats::default(), None)
278 }
279
280 #[test]
281 fn a_run_reports_its_workflows_and_their_exits() {
282 let r = run(RunStatus::Completed);
283 assert_eq!(r.matched(), 2);
284 assert!(!r.retryable() && !r.partial());
285 let seen: Vec<_> = r.workflows().map(|w| (w.id, w.sweeps, w.exit)).collect();
286 assert_eq!(seen[0], ("a", 1, Some(WorkflowExit::Completed)));
287 assert_eq!(seen[1], ("b", 0, None));
288 assert_eq!(seen[4], ("e", 1, Some(WorkflowExit::Stopped)));
289 assert!(matches!(
290 r.workflows().nth(2).unwrap().selection,
291 Selection::ConditionFailed(_)
292 ));
293 assert!(r.into_result().is_ok());
294 assert!(run(RunStatus::CompletedWithErrors).into_result().is_ok());
295 assert!(!run(RunStatus::CompletedWithErrors).partial());
296
297 let r = run(RunStatus::TimedOut);
298 assert!(r.retryable() && r.partial());
299 let e = r.into_result().unwrap_err();
300 assert_eq!(e.code(), "TIMEOUT_ERROR");
301
302 let r = run(RunStatus::Cancelled);
303 assert!(!r.retryable() && r.partial());
304 assert_eq!(r.into_result().unwrap_err().code(), "CANCELLED");
305
306 let r = run(RunStatus::Stopped(
307 Error::service("X", "m").with_retryable(true),
308 ));
309 assert!(r.retryable() && r.partial());
310 assert_eq!(r.into_result().unwrap_err().code(), "X");
311 let r = run(RunStatus::Stopped(Error::service("X", "m")));
312 assert!(!r.retryable() && r.partial());
313 }
314
315 #[test]
316 fn arena_stats_come_from_the_state_stats() {
317 let stats = Stats {
318 views: 2,
319 checkpoints: 3,
320 live_max: 40,
321 peak_arena: 50,
322 ..Stats::default()
323 };
324 let r = Run::new(program(), RunStatus::Completed, Vec::new(), stats, None);
325 let a = r.arena_stats();
326 assert_eq!(
327 (a.views, a.checkpoints, a.live_max, a.peak_arena),
328 (2, 3, 40, 50)
329 );
330 }
331}