Skip to main content

orbit_runtime/
decide.rs

1//! The deterministic workflow decider.
2//!
3//! `decide(state, definition, now)` inspects an execution and returns the
4//! events that should happen next (schedule ready steps, fan out `for` steps,
5//! fire timers, apply retry policy, start compensation, finish the
6//! workflow...). It performs no I/O. The engine applies the events, persists
7//! them, and calls `decide` again until it returns nothing.
8//!
9//! Unit keys: `step` for a step, `step[i]` for element `i` of a `for` step,
10//! and `compensate:<unit>` for the compensation of a unit.
11
12use crate::model::*;
13use orbit_ir::{
14    eval, handler_key, parse_handler_key, Action, Invocation, Scope, StepIr, WorkflowIr,
15};
16use serde_json::Value;
17
18/// Expression scope over an execution, optionally with a loop variable bound
19/// and with one step's output overridden (an element's own output inside its
20/// compensation).
21pub struct StateScope<'a> {
22    pub state: &'a ExecutionState,
23    pub local: Option<(&'a str, &'a Value)>,
24    pub own_output: Option<(&'a str, &'a Value)>,
25    /// `failure` inside `on` handlers: `{ reason, code, message }`.
26    pub failure: Option<Value>,
27}
28
29impl<'a> StateScope<'a> {
30    pub fn new(state: &'a ExecutionState) -> Self {
31        StateScope {
32            state,
33            local: None,
34            own_output: None,
35            failure: None,
36        }
37    }
38}
39
40impl Scope for StateScope<'_> {
41    fn input(&self) -> &Value {
42        &self.state.input
43    }
44
45    fn step_output(&self, step: &str) -> Option<&Value> {
46        if let Some((s, v)) = self.own_output {
47            if s == step {
48                return Some(v);
49            }
50        }
51        // Inside a handler, a name refers to a step of the same handler block
52        // first (handler step names never clash with workflow step names).
53        let handler_unit = match (&self.failure, &self.state.handlers) {
54            (Some(_), Some(phase)) => self.state.units.get(&handler_key(&phase.kind, step)),
55            _ => None,
56        };
57        let found = handler_unit
58            .or_else(|| self.state.units.get(step))
59            .and_then(|u| match u.status() {
60                UnitStatus::Succeeded | UnitStatus::Skipped => u.output.as_ref(),
61                _ => None,
62            });
63        // Handlers see steps that never ran (or did not succeed) as `null`;
64        // the compiler types them as optional there.
65        static NULL: Value = Value::Null;
66        match (found, &self.failure) {
67            (None, Some(_)) => Some(&NULL),
68            (found, _) => found,
69        }
70    }
71
72    fn execution_field(&self, field: &str) -> Option<Value> {
73        Some(Value::String(match field {
74            "id" => self.state.id.clone(),
75            "workflow" => self.state.workflow.clone(),
76            "version" => self.state.version.clone(),
77            _ => return None,
78        }))
79    }
80
81    fn local(&self, name: &str) -> Option<&Value> {
82        if name == "failure" {
83            if let Some(f) = &self.failure {
84                return Some(f);
85            }
86        }
87        self.local.filter(|(n, _)| *n == name).map(|(_, v)| v)
88    }
89}
90
91/// The `failure` value handlers see, from the abort reason.
92pub fn failure_value(state: &ExecutionState) -> Value {
93    let (reason, code, message) = match &state.abort {
94        Some(AbortReason::Failed { error }) => {
95            ("failed", error.code.clone(), error.message.clone())
96        }
97        Some(AbortReason::Cancelled { reason }) => {
98            ("cancelled", codes::CANCELLED.to_string(), reason.clone())
99        }
100        Some(AbortReason::TimedOut) => (
101            "timed_out",
102            codes::WORKFLOW_TIMEOUT.to_string(),
103            "workflow timed out".to_string(),
104        ),
105        None => ("failed", "UNKNOWN".to_string(), String::new()),
106    };
107    serde_json::json!({ "reason": reason, "code": code, "message": message })
108}
109
110/// The handler step behind a handler unit key.
111fn handler_step<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a StepIr> {
112    let (kind, id) = parse_handler_key(key)?;
113    def.handler_step(kind, id)
114}
115
116/// The invocation behind a unit key.
117pub fn invocation<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a Invocation> {
118    if let Some(step) = handler_step(def, key) {
119        return step.invocation();
120    }
121    let k = parse_key(key);
122    let step = def.step(k.step)?;
123    if k.compensation {
124        step.compensation.as_ref()
125    } else {
126        step.invocation()
127    }
128}
129
130fn unit(state: &ExecutionState, key: &str) -> UnitState {
131    state.units.get(key).cloned().unwrap_or_default()
132}
133
134/// The `for` element bound to a unit, if any: (variable, value).
135fn element<'a>(
136    state: &'a ExecutionState,
137    def: &'a WorkflowIr,
138    key: &str,
139) -> Option<(&'a str, &'a Value)> {
140    let k = parse_key(key);
141    let index = k.index?;
142    let fe = def.step(k.step)?.for_each.as_ref()?;
143    let items = state.units.get(k.step)?.input.as_ref()?.as_array()?;
144    Some((fe.variable.as_str(), items.get(index)?))
145}
146
147/// Evaluate the payload and idempotency key for a unit's first attempt.
148fn schedule(state: &ExecutionState, def: &WorkflowIr, key: &str, inv: &Invocation) -> Event {
149    let k = parse_key(key);
150    let mut scope = StateScope::new(state);
151    scope.local = element(state, def, key);
152    if parse_handler_key(key).is_some() {
153        scope.failure = Some(failure_value(state));
154    }
155    // Inside the compensation of an element, `<step>` is that element's output.
156    let element_unit = if k.compensation && k.index.is_some() {
157        key.strip_prefix("compensate:")
158    } else {
159        None
160    };
161    let own = element_unit
162        .and_then(|e| state.units.get(e))
163        .and_then(|u| u.output.as_ref());
164    if let Some(v) = own {
165        scope.own_output = Some((k.step, v));
166    }
167    let fail = |code: &str, msg: String| Event::StepFailed {
168        step: key.into(),
169        error: TaskError::new(code, msg, false),
170    };
171    let mut input = match eval(&inv.input, &scope) {
172        Ok(v) => v,
173        Err(e) => return fail(codes::EXPRESSION_ERROR, format!("evaluating input: {e}")),
174    };
175    if let Some(contract) = def.contracts.get(&inv.task) {
176        contract.input.apply_defaults(&mut input);
177        if let Err(e) = contract.input.validate(&input, false) {
178            return fail(
179                codes::INPUT_CONTRACT_VIOLATION,
180                format!("input for `{}` is invalid at {e}", inv.task),
181            );
182        }
183    }
184    let idempotency_key = match &inv.idempotency_key {
185        Some(expr) => match eval(expr, &scope) {
186            Ok(Value::String(s)) => s,
187            Ok(Value::Null) => format!("{}/{key}", state.id),
188            Ok(other) => other.to_string(),
189            Err(e) => {
190                return fail(
191                    codes::EXPRESSION_ERROR,
192                    format!("evaluating idempotency_key: {e}"),
193                )
194            }
195        },
196        None => format!("{}/{key}", state.id),
197    };
198    Event::StepScheduled {
199        step: key.into(),
200        attempt: 1,
201        input,
202        idempotency_key,
203        redelivery: false,
204    }
205}
206
207/// Retry-or-fail decision after a failed attempt.
208fn after_failure(
209    state: &ExecutionState,
210    key: &str,
211    u: &UnitState,
212    inv: Option<&Invocation>,
213    now: u64,
214) -> Event {
215    let error = u
216        .last_error
217        .clone()
218        .unwrap_or_else(|| TaskError::new("UNKNOWN", "attempt failed", true));
219    let aborting = state.abort.is_some() && !is_cleanup_key(key);
220    if let Some(inv) = inv {
221        let policy = &inv.retry;
222        let retryable = error.retryable && !policy.non_retryable.contains(&error.code);
223        if retryable && !aborting && u.attempt < policy.max_attempts {
224            let next = u.attempt + 1;
225            let delay = policy.delay_before(next);
226            return Event::RetryScheduled {
227                step: key.into(),
228                next_attempt: next,
229                delay_ms: delay,
230                fire_at_ms: now + delay,
231            };
232        }
233        if retryable && u.attempt >= policy.max_attempts && policy.max_attempts > 1 {
234            let mut e = error.clone();
235            e.message = format!("{} (gave up after {} attempts)", e.message, u.attempt);
236            return Event::StepFailed {
237                step: key.into(),
238                error: e,
239            };
240        }
241    }
242    Event::StepFailed {
243        step: key.into(),
244        error,
245    }
246}
247
248fn finish(state: &ExecutionState, outcome: CompensationOutcome) -> Event {
249    match &state.abort {
250        Some(AbortReason::Cancelled { .. }) => Event::WorkflowCancelled {
251            compensation: outcome,
252        },
253        Some(AbortReason::TimedOut) => Event::WorkflowTimedOut {
254            compensation: outcome,
255        },
256        Some(AbortReason::Failed { error }) => Event::WorkflowFailed {
257            error: error.clone(),
258            compensation: outcome,
259        },
260        None => Event::WorkflowFailed {
261            error: TaskError::new("UNKNOWN", "workflow aborted", false),
262            compensation: outcome,
263        },
264    }
265}
266
267/// Compute the next events. Returns an empty vector at a fixpoint.
268pub fn decide(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
269    if state.status.is_terminal() {
270        return vec![];
271    }
272    let mut out = vec![];
273
274    // 1. Workflow deadline.
275    if state.status == WorkflowStatus::Running && state.abort.is_none() {
276        if let Some(t) = def.timeout_ms {
277            if now >= state.created_at_ms + t {
278                return vec![Event::WorkflowTimeoutReached {}];
279            }
280        }
281    }
282
283    // 2. Per-unit transitions: deadlines, timers, retries, redelivery.
284    // Signals delivered in this pass (so two waiting units never take the same one).
285    let mut taken: Vec<u64> = vec![];
286    for (key, u) in &state.units {
287        let inv = invocation(def, key);
288        match u.status() {
289            UnitStatus::Dispatched => {
290                if u.deadline_ms.is_some_and(|d| now >= d) {
291                    out.push(Event::StepAttemptFailed {
292                        step: key.clone(),
293                        attempt: u.attempt,
294                        error: TaskError::new(
295                            codes::TIMEOUT,
296                            format!("attempt {} exceeded its timeout", u.attempt),
297                            true,
298                        ),
299                    });
300                }
301            }
302            UnitStatus::Scheduled => {
303                let qt = inv.and_then(|i| i.queue_timeout_ms);
304                if let (Some(qt), Some(at)) = (qt, u.scheduled_at_ms) {
305                    if !u.redelivery && now >= at + qt {
306                        out.push(Event::StepAttemptFailed {
307                            step: key.clone(),
308                            attempt: u.attempt,
309                            error: TaskError::new(
310                                codes::QUEUE_TIMEOUT,
311                                "no worker picked up the task before queue_timeout",
312                                false,
313                            ),
314                        });
315                    }
316                }
317            }
318            UnitStatus::Waiting => {
319                if let Some(signal) = awaited_signal(def, key) {
320                    // `await`: take the oldest undelivered signal of this name.
321                    let next = state.signals.iter().find(|r| {
322                        r.name == signal && r.delivered_to.is_none() && !taken.contains(&r.seq)
323                    });
324                    if let Some(r) = next {
325                        taken.push(r.seq);
326                        out.push(Event::SignalDelivered {
327                            step: key.clone(),
328                            signal_seq: r.seq,
329                        });
330                    } else if u.wake_at_ms.is_some_and(|w| now >= w) {
331                        out.push(Event::StepFailed {
332                            step: key.clone(),
333                            error: TaskError::new(
334                                codes::SIGNAL_TIMEOUT,
335                                format!("no `{signal}` signal arrived before the step's timeout"),
336                                false,
337                            ),
338                        });
339                    }
340                } else if u.wake_at_ms.is_some_and(|w| now >= w) {
341                    out.push(Event::TimerFired { step: key.clone() });
342                }
343            }
344            UnitStatus::RetryWait => {
345                if u.wake_at_ms.is_some_and(|w| now >= w) {
346                    if state.abort.is_some() && !is_cleanup_key(key) {
347                        out.push(Event::StepFailed {
348                            step: key.clone(),
349                            error: u.last_error.clone().unwrap_or_else(|| {
350                                TaskError::new(codes::CANCELLED, "aborted", false)
351                            }),
352                        });
353                    } else {
354                        out.push(Event::StepScheduled {
355                            step: key.clone(),
356                            attempt: u.attempt,
357                            input: u.input.clone().unwrap_or(Value::Null),
358                            idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
359                            redelivery: false,
360                        });
361                    }
362                }
363            }
364            UnitStatus::AttemptFailed => out.push(after_failure(state, key, u, inv, now)),
365            UnitStatus::Uncertain => out.push(Event::StepScheduled {
366                step: key.clone(),
367                attempt: u.attempt,
368                input: u.input.clone().unwrap_or(Value::Null),
369                idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
370                redelivery: true,
371            }),
372            _ => {}
373        }
374    }
375    if !out.is_empty() {
376        return out;
377    }
378
379    if let Some(phase) = &state.handlers {
380        return decide_handlers(state, def, phase, now);
381    }
382    match state.status {
383        WorkflowStatus::Running => decide_running(state, def, now),
384        WorkflowStatus::Compensating => decide_compensating(state, def),
385        _ => vec![],
386    }
387}
388
389/// The signal an `await` unit waits for.
390fn awaited_signal<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a str> {
391    let k = parse_key(key);
392    if k.compensation {
393        return None;
394    }
395    match &def.step(k.step)?.action {
396        Action::Await { signal, .. } => Some(signal),
397        _ => None,
398    }
399}
400
401/// Evaluate a `compute` step (or one element of it) in the runtime.
402fn compute(state: &ExecutionState, def: &WorkflowIr, key: &str, expr: &orbit_ir::Expr) -> Event {
403    let mut scope = StateScope::new(state);
404    scope.local = element(state, def, key);
405    if parse_handler_key(key).is_some() {
406        scope.failure = Some(failure_value(state));
407    }
408    match eval(expr, &scope) {
409        Ok(output) => Event::StepSucceeded {
410            step: key.into(),
411            attempt: 1,
412            output,
413        },
414        Err(e) => Event::StepFailed {
415            step: key.into(),
416            error: TaskError::new(
417                codes::EXPRESSION_ERROR,
418                format!("evaluating `compute`: {e}"),
419                false,
420            ),
421        },
422    }
423}
424
425fn deps_done(state: &ExecutionState, step: &StepIr) -> bool {
426    step.dependencies.iter().all(|d| {
427        matches!(
428            unit(state, d).status(),
429            UnitStatus::Succeeded | UnitStatus::Skipped
430        )
431    })
432}
433
434/// Element unit keys of a fanned-out step, in order.
435fn element_keys(state: &ExecutionState, step: &str) -> Vec<String> {
436    let n = state
437        .units
438        .get(step)
439        .and_then(|u| u.input.as_ref())
440        .and_then(Value::as_array)
441        .map(Vec::len)
442        .unwrap_or(0);
443    (0..n).map(|i| element_key(step, i)).collect()
444}
445
446fn in_flight(status: UnitStatus) -> bool {
447    matches!(
448        status,
449        UnitStatus::Scheduled
450            | UnitStatus::Dispatched
451            | UnitStatus::AttemptFailed
452            | UnitStatus::Uncertain
453            | UnitStatus::RetryWait
454    )
455}
456
457/// Progress a running `for` step: schedule elements within `max_parallel`,
458/// and resolve the step once every element is done (or one failed).
459fn advance_fan_out(state: &ExecutionState, def: &WorkflowIr, step: &StepIr, out: &mut Vec<Event>) {
460    let keys = element_keys(state, &step.id);
461    let units: Vec<UnitState> = keys.iter().map(|k| unit(state, k)).collect();
462    if let Some(failed) = units.iter().find(|u| u.status() == UnitStatus::Failed) {
463        out.push(Event::StepFailed {
464            step: step.id.clone(),
465            error: failed
466                .error
467                .clone()
468                .unwrap_or_else(|| TaskError::new("UNKNOWN", "element failed", false)),
469        });
470        return;
471    }
472    if units.iter().all(|u| u.status() == UnitStatus::Succeeded) {
473        let output = Value::Array(
474            units
475                .iter()
476                .map(|u| u.output.clone().unwrap_or(Value::Null))
477                .collect(),
478        );
479        out.push(Event::StepSucceeded {
480            step: step.id.clone(),
481            attempt: 1,
482            output,
483        });
484        return;
485    }
486    if let Action::Compute { expr } = &step.action {
487        for (key, u) in keys.iter().zip(&units) {
488            if u.status() == UnitStatus::Pending {
489                out.push(compute(state, def, key, expr));
490            }
491        }
492        return;
493    }
494    let Some(inv) = step.invocation() else { return };
495    let limit = step
496        .for_each
497        .as_ref()
498        .and_then(|f| f.max_parallel)
499        .map(|n| n as usize)
500        .unwrap_or(usize::MAX);
501    let mut running = units.iter().filter(|u| in_flight(u.status())).count();
502    for (key, u) in keys.iter().zip(&units) {
503        if running >= limit {
504            break;
505        }
506        if u.status() == UnitStatus::Pending {
507            out.push(schedule(state, def, key, inv));
508            running += 1;
509        }
510    }
511}
512
513fn decide_running(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
514    let mut out = vec![];
515    if state.abort.is_none() {
516        for step in &def.steps {
517            let u = unit(state, &step.id);
518            if u.status() == UnitStatus::Running {
519                advance_fan_out(state, def, step, &mut out);
520                continue;
521            }
522            if u.status() != UnitStatus::Pending || !deps_done(state, step) {
523                continue;
524            }
525            if let Some(cond) = &step.condition {
526                match eval(cond, &StateScope::new(state)) {
527                    Ok(Value::Bool(true)) => {}
528                    Ok(Value::Bool(false)) | Ok(Value::Null) => {
529                        out.push(Event::StepSkipped {
530                            step: step.id.clone(),
531                            reason: "`when` condition was false".into(),
532                        });
533                        continue;
534                    }
535                    Ok(other) => {
536                        out.push(Event::StepFailed {
537                            step: step.id.clone(),
538                            error: TaskError::new(
539                                codes::EXPRESSION_ERROR,
540                                format!("`when` evaluated to {other}, expected Bool"),
541                                false,
542                            ),
543                        });
544                        continue;
545                    }
546                    Err(e) => {
547                        out.push(Event::StepFailed {
548                            step: step.id.clone(),
549                            error: TaskError::new(
550                                codes::EXPRESSION_ERROR,
551                                format!("evaluating `when`: {e}"),
552                                false,
553                            ),
554                        });
555                        continue;
556                    }
557                }
558            }
559            if let Some(fe) = &step.for_each {
560                match eval(&fe.items, &StateScope::new(state)) {
561                    Ok(Value::Array(items)) => out.push(Event::StepFanOut {
562                        step: step.id.clone(),
563                        items,
564                    }),
565                    Ok(Value::Null) => out.push(Event::StepFanOut {
566                        step: step.id.clone(),
567                        items: vec![],
568                    }),
569                    Ok(other) => out.push(Event::StepFailed {
570                        step: step.id.clone(),
571                        error: TaskError::new(
572                            codes::EXPRESSION_ERROR,
573                            format!("`for` expected a list, got {other}"),
574                            false,
575                        ),
576                    }),
577                    Err(e) => out.push(Event::StepFailed {
578                        step: step.id.clone(),
579                        error: TaskError::new(
580                            codes::EXPRESSION_ERROR,
581                            format!("evaluating `for`: {e}"),
582                            false,
583                        ),
584                    }),
585                }
586                continue;
587            }
588            match &step.action {
589                Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
590                    step: step.id.clone(),
591                    fire_at_ms: now + duration_ms,
592                }),
593                Action::Task(inv) | Action::Workflow(inv) => {
594                    out.push(schedule(state, def, &step.id, inv))
595                }
596                Action::Await { signal, timeout_ms } => out.push(Event::SignalAwaitStarted {
597                    step: step.id.clone(),
598                    signal: signal.clone(),
599                    deadline_ms: timeout_ms.map(|t| now + t),
600                }),
601                Action::Compute { expr } => out.push(compute(state, def, &step.id, expr)),
602            }
603        }
604        if !out.is_empty() {
605            return out;
606        }
607        let all_done = def
608            .steps
609            .iter()
610            .all(|s| unit(state, &s.id).status().is_terminal());
611        if all_done {
612            let output = match &def.result {
613                Some(expr) => match eval(expr, &StateScope::new(state)) {
614                    Ok(v) => v,
615                    Err(e) => {
616                        return vec![Event::WorkflowFailed {
617                            error: TaskError::new(
618                                codes::EXPRESSION_ERROR,
619                                format!("evaluating `return`: {e}"),
620                                false,
621                            ),
622                            compensation: CompensationOutcome::None,
623                        }]
624                    }
625                },
626                None => Value::Object(Default::default()),
627            };
628            if let Err(e) = def.output.validate(&output, false) {
629                return vec![Event::WorkflowFailed {
630                    error: TaskError::new(
631                        codes::OUTPUT_CONTRACT_VIOLATION,
632                        format!("workflow output is invalid at {e}"),
633                        false,
634                    ),
635                    compensation: CompensationOutcome::None,
636                }];
637            }
638            return vec![Event::WorkflowCompleted { output }];
639        }
640        return out;
641    }
642
643    // Aborting: withdraw work that has not started, wait for in-flight work,
644    // then compensate completed units in reverse completion order.
645    let mut waiting = false;
646    let withdraw =
647        |key: &str, u: &UnitState, out: &mut Vec<Event>, waiting: &mut bool| match u.status() {
648            UnitStatus::Pending | UnitStatus::Waiting | UnitStatus::RetryWait => {
649                out.push(Event::StepCancelled { step: key.into() })
650            }
651            UnitStatus::Scheduled if !u.redelivery => {
652                out.push(Event::StepCancelled { step: key.into() })
653            }
654            UnitStatus::Scheduled
655            | UnitStatus::Dispatched
656            | UnitStatus::AttemptFailed
657            | UnitStatus::Uncertain => *waiting = true,
658            _ => {}
659        };
660    for step in &def.steps {
661        let u = unit(state, &step.id);
662        if u.status() == UnitStatus::Running {
663            let mut element_waiting = false;
664            let mut failed = None;
665            for key in element_keys(state, &step.id) {
666                let eu = unit(state, &key);
667                if eu.status() == UnitStatus::Failed && failed.is_none() {
668                    failed = eu.error.clone();
669                }
670                withdraw(&key, &eu, &mut out, &mut element_waiting);
671            }
672            if element_waiting {
673                waiting = true;
674            } else {
675                // Every element is resolved: close the step itself.
676                out.push(match failed {
677                    Some(error) => Event::StepFailed {
678                        step: step.id.clone(),
679                        error,
680                    },
681                    None => Event::StepCancelled {
682                        step: step.id.clone(),
683                    },
684                });
685            }
686        } else {
687            withdraw(&step.id, &u, &mut out, &mut waiting);
688        }
689    }
690    if !out.is_empty() || waiting {
691        return out;
692    }
693    // Units that succeeded and have a compensation: whole steps, or each
694    // succeeded element of a `for` step.
695    let mut done: Vec<(String, u64)> = vec![];
696    for s in def.steps.iter().filter(|s| s.compensation.is_some()) {
697        let keys = if s.for_each.is_some() {
698            element_keys(state, &s.id)
699        } else {
700            vec![s.id.clone()]
701        };
702        for key in keys {
703            if let Some(u) = state.units.get(&key) {
704                if u.status() == UnitStatus::Succeeded {
705                    done.push((key, u.succeeded_seq.unwrap_or(0)));
706                }
707            }
708        }
709    }
710    done.sort_by_key(|d| std::cmp::Reverse(d.1));
711    if done.is_empty() {
712        return conclude(state, def, CompensationOutcome::None);
713    }
714    let reason = match &state.abort {
715        Some(AbortReason::Failed { error }) => error.message.clone(),
716        Some(AbortReason::Cancelled { reason }) => format!("cancelled: {reason}"),
717        Some(AbortReason::TimedOut) => "workflow timed out".into(),
718        None => String::new(),
719    };
720    vec![Event::CompensationStarted {
721        reason,
722        steps: done.into_iter().map(|(s, _)| s).collect(),
723    }]
724}
725
726fn decide_compensating(state: &ExecutionState, def: &WorkflowIr) -> Vec<Event> {
727    // Compensations run one at a time, newest completed unit first (Saga). A
728    // compensation that fails does not stop the others: every remaining effect
729    // is still undone, and each failure is reported at the end.
730    let mut failures = vec![];
731    for step in &state.compensation_order {
732        let key = compensation_key(step);
733        let u = unit(state, &key);
734        match u.status() {
735            UnitStatus::Succeeded => continue,
736            UnitStatus::Failed | UnitStatus::Cancelled => {
737                let error = u
738                    .error
739                    .unwrap_or_else(|| TaskError::new("UNKNOWN", "compensation failed", false));
740                failures.push(Event::CompensationFailed {
741                    step: step.clone(),
742                    error,
743                });
744                continue;
745            }
746            UnitStatus::Pending => {
747                return match invocation(def, &key) {
748                    Some(inv) => vec![schedule(state, def, &key, inv)],
749                    None => vec![Event::StepFailed {
750                        step: key,
751                        error: TaskError::new("UNKNOWN", "missing compensation", false),
752                    }],
753                };
754            }
755            _ => return vec![],
756        }
757    }
758    if !failures.is_empty() {
759        failures.extend(conclude(state, def, CompensationOutcome::Failed));
760        return failures;
761    }
762    let mut out = vec![Event::CompensationCompleted {}];
763    out.extend(conclude(state, def, CompensationOutcome::Completed));
764    out
765}
766
767/// The handler kind for the abort reason.
768fn handler_kind(state: &ExecutionState) -> &'static str {
769    match &state.abort {
770        Some(AbortReason::Cancelled { .. }) => "cancel",
771        Some(AbortReason::TimedOut) => "timeout",
772        _ => "failure",
773    }
774}
775
776/// After compensation: start the matching `on` handler, or finish.
777fn conclude(state: &ExecutionState, def: &WorkflowIr, outcome: CompensationOutcome) -> Vec<Event> {
778    let kind = handler_kind(state);
779    match def.handlers.get(kind) {
780        Some(steps) if !steps.is_empty() => vec![Event::HandlersStarted {
781            kind: kind.to_string(),
782            compensation: outcome,
783        }],
784        _ => vec![finish(state, outcome)],
785    }
786}
787
788/// Run the handler steps of the current phase; finish when they are all done.
789/// A failed handler step is recorded but does not change the outcome; steps
790/// that depend on it are cancelled.
791fn decide_handlers(
792    state: &ExecutionState,
793    def: &WorkflowIr,
794    phase: &HandlerPhase,
795    now: u64,
796) -> Vec<Event> {
797    let steps = def
798        .handlers
799        .get(&phase.kind)
800        .map(Vec::as_slice)
801        .unwrap_or(&[]);
802    let mut out = vec![];
803    let mut all_done = true;
804    for step in steps {
805        let key = handler_key(&phase.kind, &step.id);
806        let u = unit(state, &key);
807        if u.status().is_terminal() {
808            continue;
809        }
810        all_done = false;
811        if u.status() != UnitStatus::Pending {
812            continue;
813        }
814        let dep_status: Vec<UnitStatus> = step
815            .dependencies
816            .iter()
817            .map(|d| unit(state, &handler_key(&phase.kind, d)).status())
818            .collect();
819        if dep_status
820            .iter()
821            .any(|s| matches!(s, UnitStatus::Failed | UnitStatus::Cancelled))
822        {
823            out.push(Event::StepCancelled { step: key });
824            continue;
825        }
826        if !dep_status
827            .iter()
828            .all(|s| matches!(s, UnitStatus::Succeeded | UnitStatus::Skipped))
829        {
830            continue;
831        }
832        let mut scope = StateScope::new(state);
833        scope.failure = Some(failure_value(state));
834        if let Some(cond) = &step.condition {
835            match eval(cond, &scope) {
836                Ok(Value::Bool(true)) => {}
837                Ok(Value::Bool(false)) | Ok(Value::Null) => {
838                    out.push(Event::StepSkipped {
839                        step: key,
840                        reason: "`when` condition was false".into(),
841                    });
842                    continue;
843                }
844                Ok(other) => {
845                    out.push(Event::StepFailed {
846                        step: key,
847                        error: TaskError::new(
848                            codes::EXPRESSION_ERROR,
849                            format!("`when` evaluated to {other}, expected Bool"),
850                            false,
851                        ),
852                    });
853                    continue;
854                }
855                Err(e) => {
856                    out.push(Event::StepFailed {
857                        step: key,
858                        error: TaskError::new(
859                            codes::EXPRESSION_ERROR,
860                            format!("evaluating `when`: {e}"),
861                            false,
862                        ),
863                    });
864                    continue;
865                }
866            }
867        }
868        match &step.action {
869            Action::Task(inv) | Action::Workflow(inv) => out.push(schedule(state, def, &key, inv)),
870            Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
871                step: key,
872                fire_at_ms: now + duration_ms,
873            }),
874            Action::Compute { expr } => out.push(compute(state, def, &key, expr)),
875            Action::Await { .. } => out.push(Event::StepFailed {
876                step: key,
877                error: TaskError::new("UNKNOWN", "`await` is not allowed in a handler", false),
878            }),
879        }
880    }
881    if all_done && out.is_empty() {
882        return vec![finish(state, phase.compensation)];
883    }
884    out
885}