1use crate::model::*;
13use orbit_ir::{eval, Action, Invocation, Scope, StepIr, WorkflowIr};
14use serde_json::Value;
15
16pub struct StateScope<'a> {
20 pub state: &'a ExecutionState,
21 pub local: Option<(&'a str, &'a Value)>,
22 pub own_output: Option<(&'a str, &'a Value)>,
23}
24
25impl<'a> StateScope<'a> {
26 pub fn new(state: &'a ExecutionState) -> Self {
27 StateScope {
28 state,
29 local: None,
30 own_output: None,
31 }
32 }
33}
34
35impl Scope for StateScope<'_> {
36 fn input(&self) -> &Value {
37 &self.state.input
38 }
39
40 fn step_output(&self, step: &str) -> Option<&Value> {
41 if let Some((s, v)) = self.own_output {
42 if s == step {
43 return Some(v);
44 }
45 }
46 let u = self.state.units.get(step)?;
47 match u.status() {
48 UnitStatus::Succeeded | UnitStatus::Skipped => u.output.as_ref(),
49 _ => None,
50 }
51 }
52
53 fn execution_field(&self, field: &str) -> Option<Value> {
54 Some(Value::String(match field {
55 "id" => self.state.id.clone(),
56 "workflow" => self.state.workflow.clone(),
57 "version" => self.state.version.clone(),
58 _ => return None,
59 }))
60 }
61
62 fn local(&self, name: &str) -> Option<&Value> {
63 self.local.filter(|(n, _)| *n == name).map(|(_, v)| v)
64 }
65}
66
67pub fn invocation<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a Invocation> {
69 let k = parse_key(key);
70 let step = def.step(k.step)?;
71 if k.compensation {
72 step.compensation.as_ref()
73 } else {
74 step.invocation()
75 }
76}
77
78fn unit(state: &ExecutionState, key: &str) -> UnitState {
79 state.units.get(key).cloned().unwrap_or_default()
80}
81
82fn element<'a>(
84 state: &'a ExecutionState,
85 def: &'a WorkflowIr,
86 key: &str,
87) -> Option<(&'a str, &'a Value)> {
88 let k = parse_key(key);
89 let index = k.index?;
90 let fe = def.step(k.step)?.for_each.as_ref()?;
91 let items = state.units.get(k.step)?.input.as_ref()?.as_array()?;
92 Some((fe.variable.as_str(), items.get(index)?))
93}
94
95fn schedule(state: &ExecutionState, def: &WorkflowIr, key: &str, inv: &Invocation) -> Event {
97 let k = parse_key(key);
98 let mut scope = StateScope::new(state);
99 scope.local = element(state, def, key);
100 let element_unit = if k.compensation && k.index.is_some() {
102 key.strip_prefix("compensate:")
103 } else {
104 None
105 };
106 let own = element_unit
107 .and_then(|e| state.units.get(e))
108 .and_then(|u| u.output.as_ref());
109 if let Some(v) = own {
110 scope.own_output = Some((k.step, v));
111 }
112 let fail = |code: &str, msg: String| Event::StepFailed {
113 step: key.into(),
114 error: TaskError::new(code, msg, false),
115 };
116 let mut input = match eval(&inv.input, &scope) {
117 Ok(v) => v,
118 Err(e) => return fail(codes::EXPRESSION_ERROR, format!("evaluating input: {e}")),
119 };
120 if let Some(contract) = def.contracts.get(&inv.task) {
121 contract.input.apply_defaults(&mut input);
122 if let Err(e) = contract.input.validate(&input, false) {
123 return fail(
124 codes::INPUT_CONTRACT_VIOLATION,
125 format!("input for `{}` is invalid at {e}", inv.task),
126 );
127 }
128 }
129 let idempotency_key = match &inv.idempotency_key {
130 Some(expr) => match eval(expr, &scope) {
131 Ok(Value::String(s)) => s,
132 Ok(Value::Null) => format!("{}/{key}", state.id),
133 Ok(other) => other.to_string(),
134 Err(e) => {
135 return fail(
136 codes::EXPRESSION_ERROR,
137 format!("evaluating idempotency_key: {e}"),
138 )
139 }
140 },
141 None => format!("{}/{key}", state.id),
142 };
143 Event::StepScheduled {
144 step: key.into(),
145 attempt: 1,
146 input,
147 idempotency_key,
148 redelivery: false,
149 }
150}
151
152fn after_failure(
154 state: &ExecutionState,
155 key: &str,
156 u: &UnitState,
157 inv: Option<&Invocation>,
158 now: u64,
159) -> Event {
160 let error = u
161 .last_error
162 .clone()
163 .unwrap_or_else(|| TaskError::new("UNKNOWN", "attempt failed", true));
164 let is_compensation = key.starts_with("compensate:");
165 let aborting = state.abort.is_some() && !is_compensation;
166 if let Some(inv) = inv {
167 let policy = &inv.retry;
168 let retryable = error.retryable && !policy.non_retryable.contains(&error.code);
169 if retryable && !aborting && u.attempt < policy.max_attempts {
170 let next = u.attempt + 1;
171 let delay = policy.delay_before(next);
172 return Event::RetryScheduled {
173 step: key.into(),
174 next_attempt: next,
175 delay_ms: delay,
176 fire_at_ms: now + delay,
177 };
178 }
179 if retryable && u.attempt >= policy.max_attempts && policy.max_attempts > 1 {
180 let mut e = error.clone();
181 e.message = format!("{} (gave up after {} attempts)", e.message, u.attempt);
182 return Event::StepFailed {
183 step: key.into(),
184 error: e,
185 };
186 }
187 }
188 Event::StepFailed {
189 step: key.into(),
190 error,
191 }
192}
193
194fn finish(state: &ExecutionState, outcome: CompensationOutcome) -> Event {
195 match &state.abort {
196 Some(AbortReason::Cancelled { .. }) => Event::WorkflowCancelled {
197 compensation: outcome,
198 },
199 Some(AbortReason::TimedOut) => Event::WorkflowTimedOut {
200 compensation: outcome,
201 },
202 Some(AbortReason::Failed { error }) => Event::WorkflowFailed {
203 error: error.clone(),
204 compensation: outcome,
205 },
206 None => Event::WorkflowFailed {
207 error: TaskError::new("UNKNOWN", "workflow aborted", false),
208 compensation: outcome,
209 },
210 }
211}
212
213pub fn decide(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
215 if state.status.is_terminal() {
216 return vec![];
217 }
218 let mut out = vec![];
219
220 if state.status == WorkflowStatus::Running && state.abort.is_none() {
222 if let Some(t) = def.timeout_ms {
223 if now >= state.created_at_ms + t {
224 return vec![Event::WorkflowTimeoutReached {}];
225 }
226 }
227 }
228
229 let mut taken: Vec<u64> = vec![];
232 for (key, u) in &state.units {
233 let inv = invocation(def, key);
234 match u.status() {
235 UnitStatus::Dispatched => {
236 if u.deadline_ms.is_some_and(|d| now >= d) {
237 out.push(Event::StepAttemptFailed {
238 step: key.clone(),
239 attempt: u.attempt,
240 error: TaskError::new(
241 codes::TIMEOUT,
242 format!("attempt {} exceeded its timeout", u.attempt),
243 true,
244 ),
245 });
246 }
247 }
248 UnitStatus::Scheduled => {
249 let qt = inv.and_then(|i| i.queue_timeout_ms);
250 if let (Some(qt), Some(at)) = (qt, u.scheduled_at_ms) {
251 if !u.redelivery && now >= at + qt {
252 out.push(Event::StepAttemptFailed {
253 step: key.clone(),
254 attempt: u.attempt,
255 error: TaskError::new(
256 codes::QUEUE_TIMEOUT,
257 "no worker picked up the task before queue_timeout",
258 false,
259 ),
260 });
261 }
262 }
263 }
264 UnitStatus::Waiting => {
265 if let Some(signal) = awaited_signal(def, key) {
266 let next = state.signals.iter().find(|r| {
268 r.name == signal && r.delivered_to.is_none() && !taken.contains(&r.seq)
269 });
270 if let Some(r) = next {
271 taken.push(r.seq);
272 out.push(Event::SignalDelivered {
273 step: key.clone(),
274 signal_seq: r.seq,
275 });
276 } else if u.wake_at_ms.is_some_and(|w| now >= w) {
277 out.push(Event::StepFailed {
278 step: key.clone(),
279 error: TaskError::new(
280 codes::SIGNAL_TIMEOUT,
281 format!("no `{signal}` signal arrived before the step's timeout"),
282 false,
283 ),
284 });
285 }
286 } else if u.wake_at_ms.is_some_and(|w| now >= w) {
287 out.push(Event::TimerFired { step: key.clone() });
288 }
289 }
290 UnitStatus::RetryWait => {
291 if u.wake_at_ms.is_some_and(|w| now >= w) {
292 if state.abort.is_some() && !key.starts_with("compensate:") {
293 out.push(Event::StepFailed {
294 step: key.clone(),
295 error: u.last_error.clone().unwrap_or_else(|| {
296 TaskError::new(codes::CANCELLED, "aborted", false)
297 }),
298 });
299 } else {
300 out.push(Event::StepScheduled {
301 step: key.clone(),
302 attempt: u.attempt,
303 input: u.input.clone().unwrap_or(Value::Null),
304 idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
305 redelivery: false,
306 });
307 }
308 }
309 }
310 UnitStatus::AttemptFailed => out.push(after_failure(state, key, u, inv, now)),
311 UnitStatus::Uncertain => out.push(Event::StepScheduled {
312 step: key.clone(),
313 attempt: u.attempt,
314 input: u.input.clone().unwrap_or(Value::Null),
315 idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
316 redelivery: true,
317 }),
318 _ => {}
319 }
320 }
321 if !out.is_empty() {
322 return out;
323 }
324
325 match state.status {
326 WorkflowStatus::Running => decide_running(state, def, now),
327 WorkflowStatus::Compensating => decide_compensating(state, def),
328 _ => vec![],
329 }
330}
331
332fn awaited_signal<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a str> {
334 let k = parse_key(key);
335 if k.compensation {
336 return None;
337 }
338 match &def.step(k.step)?.action {
339 Action::Await { signal, .. } => Some(signal),
340 _ => None,
341 }
342}
343
344fn compute(state: &ExecutionState, def: &WorkflowIr, key: &str, expr: &orbit_ir::Expr) -> Event {
346 let mut scope = StateScope::new(state);
347 scope.local = element(state, def, key);
348 match eval(expr, &scope) {
349 Ok(output) => Event::StepSucceeded {
350 step: key.into(),
351 attempt: 1,
352 output,
353 },
354 Err(e) => Event::StepFailed {
355 step: key.into(),
356 error: TaskError::new(
357 codes::EXPRESSION_ERROR,
358 format!("evaluating `compute`: {e}"),
359 false,
360 ),
361 },
362 }
363}
364
365fn deps_done(state: &ExecutionState, step: &StepIr) -> bool {
366 step.dependencies.iter().all(|d| {
367 matches!(
368 unit(state, d).status(),
369 UnitStatus::Succeeded | UnitStatus::Skipped
370 )
371 })
372}
373
374fn element_keys(state: &ExecutionState, step: &str) -> Vec<String> {
376 let n = state
377 .units
378 .get(step)
379 .and_then(|u| u.input.as_ref())
380 .and_then(Value::as_array)
381 .map(Vec::len)
382 .unwrap_or(0);
383 (0..n).map(|i| element_key(step, i)).collect()
384}
385
386fn in_flight(status: UnitStatus) -> bool {
387 matches!(
388 status,
389 UnitStatus::Scheduled
390 | UnitStatus::Dispatched
391 | UnitStatus::AttemptFailed
392 | UnitStatus::Uncertain
393 | UnitStatus::RetryWait
394 )
395}
396
397fn advance_fan_out(state: &ExecutionState, def: &WorkflowIr, step: &StepIr, out: &mut Vec<Event>) {
400 let keys = element_keys(state, &step.id);
401 let units: Vec<UnitState> = keys.iter().map(|k| unit(state, k)).collect();
402 if let Some(failed) = units.iter().find(|u| u.status() == UnitStatus::Failed) {
403 out.push(Event::StepFailed {
404 step: step.id.clone(),
405 error: failed
406 .error
407 .clone()
408 .unwrap_or_else(|| TaskError::new("UNKNOWN", "element failed", false)),
409 });
410 return;
411 }
412 if units.iter().all(|u| u.status() == UnitStatus::Succeeded) {
413 let output = Value::Array(
414 units
415 .iter()
416 .map(|u| u.output.clone().unwrap_or(Value::Null))
417 .collect(),
418 );
419 out.push(Event::StepSucceeded {
420 step: step.id.clone(),
421 attempt: 1,
422 output,
423 });
424 return;
425 }
426 if let Action::Compute { expr } = &step.action {
427 for (key, u) in keys.iter().zip(&units) {
428 if u.status() == UnitStatus::Pending {
429 out.push(compute(state, def, key, expr));
430 }
431 }
432 return;
433 }
434 let Some(inv) = step.invocation() else { return };
435 let limit = step
436 .for_each
437 .as_ref()
438 .and_then(|f| f.max_parallel)
439 .map(|n| n as usize)
440 .unwrap_or(usize::MAX);
441 let mut running = units.iter().filter(|u| in_flight(u.status())).count();
442 for (key, u) in keys.iter().zip(&units) {
443 if running >= limit {
444 break;
445 }
446 if u.status() == UnitStatus::Pending {
447 out.push(schedule(state, def, key, inv));
448 running += 1;
449 }
450 }
451}
452
453fn decide_running(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
454 let mut out = vec![];
455 if state.abort.is_none() {
456 for step in &def.steps {
457 let u = unit(state, &step.id);
458 if u.status() == UnitStatus::Running {
459 advance_fan_out(state, def, step, &mut out);
460 continue;
461 }
462 if u.status() != UnitStatus::Pending || !deps_done(state, step) {
463 continue;
464 }
465 if let Some(cond) = &step.condition {
466 match eval(cond, &StateScope::new(state)) {
467 Ok(Value::Bool(true)) => {}
468 Ok(Value::Bool(false)) | Ok(Value::Null) => {
469 out.push(Event::StepSkipped {
470 step: step.id.clone(),
471 reason: "`when` condition was false".into(),
472 });
473 continue;
474 }
475 Ok(other) => {
476 out.push(Event::StepFailed {
477 step: step.id.clone(),
478 error: TaskError::new(
479 codes::EXPRESSION_ERROR,
480 format!("`when` evaluated to {other}, expected Bool"),
481 false,
482 ),
483 });
484 continue;
485 }
486 Err(e) => {
487 out.push(Event::StepFailed {
488 step: step.id.clone(),
489 error: TaskError::new(
490 codes::EXPRESSION_ERROR,
491 format!("evaluating `when`: {e}"),
492 false,
493 ),
494 });
495 continue;
496 }
497 }
498 }
499 if let Some(fe) = &step.for_each {
500 match eval(&fe.items, &StateScope::new(state)) {
501 Ok(Value::Array(items)) => out.push(Event::StepFanOut {
502 step: step.id.clone(),
503 items,
504 }),
505 Ok(Value::Null) => out.push(Event::StepFanOut {
506 step: step.id.clone(),
507 items: vec![],
508 }),
509 Ok(other) => out.push(Event::StepFailed {
510 step: step.id.clone(),
511 error: TaskError::new(
512 codes::EXPRESSION_ERROR,
513 format!("`for` expected a list, got {other}"),
514 false,
515 ),
516 }),
517 Err(e) => out.push(Event::StepFailed {
518 step: step.id.clone(),
519 error: TaskError::new(
520 codes::EXPRESSION_ERROR,
521 format!("evaluating `for`: {e}"),
522 false,
523 ),
524 }),
525 }
526 continue;
527 }
528 match &step.action {
529 Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
530 step: step.id.clone(),
531 fire_at_ms: now + duration_ms,
532 }),
533 Action::Task(inv) | Action::Workflow(inv) => {
534 out.push(schedule(state, def, &step.id, inv))
535 }
536 Action::Await { signal, timeout_ms } => out.push(Event::SignalAwaitStarted {
537 step: step.id.clone(),
538 signal: signal.clone(),
539 deadline_ms: timeout_ms.map(|t| now + t),
540 }),
541 Action::Compute { expr } => out.push(compute(state, def, &step.id, expr)),
542 }
543 }
544 if !out.is_empty() {
545 return out;
546 }
547 let all_done = def
548 .steps
549 .iter()
550 .all(|s| unit(state, &s.id).status().is_terminal());
551 if all_done {
552 let output = match &def.result {
553 Some(expr) => match eval(expr, &StateScope::new(state)) {
554 Ok(v) => v,
555 Err(e) => {
556 return vec![Event::WorkflowFailed {
557 error: TaskError::new(
558 codes::EXPRESSION_ERROR,
559 format!("evaluating `return`: {e}"),
560 false,
561 ),
562 compensation: CompensationOutcome::None,
563 }]
564 }
565 },
566 None => Value::Object(Default::default()),
567 };
568 if let Err(e) = def.output.validate(&output, false) {
569 return vec![Event::WorkflowFailed {
570 error: TaskError::new(
571 codes::OUTPUT_CONTRACT_VIOLATION,
572 format!("workflow output is invalid at {e}"),
573 false,
574 ),
575 compensation: CompensationOutcome::None,
576 }];
577 }
578 return vec![Event::WorkflowCompleted { output }];
579 }
580 return out;
581 }
582
583 let mut waiting = false;
586 let withdraw =
587 |key: &str, u: &UnitState, out: &mut Vec<Event>, waiting: &mut bool| match u.status() {
588 UnitStatus::Pending | UnitStatus::Waiting | UnitStatus::RetryWait => {
589 out.push(Event::StepCancelled { step: key.into() })
590 }
591 UnitStatus::Scheduled if !u.redelivery => {
592 out.push(Event::StepCancelled { step: key.into() })
593 }
594 UnitStatus::Scheduled
595 | UnitStatus::Dispatched
596 | UnitStatus::AttemptFailed
597 | UnitStatus::Uncertain => *waiting = true,
598 _ => {}
599 };
600 for step in &def.steps {
601 let u = unit(state, &step.id);
602 if u.status() == UnitStatus::Running {
603 let mut element_waiting = false;
604 let mut failed = None;
605 for key in element_keys(state, &step.id) {
606 let eu = unit(state, &key);
607 if eu.status() == UnitStatus::Failed && failed.is_none() {
608 failed = eu.error.clone();
609 }
610 withdraw(&key, &eu, &mut out, &mut element_waiting);
611 }
612 if element_waiting {
613 waiting = true;
614 } else {
615 out.push(match failed {
617 Some(error) => Event::StepFailed {
618 step: step.id.clone(),
619 error,
620 },
621 None => Event::StepCancelled {
622 step: step.id.clone(),
623 },
624 });
625 }
626 } else {
627 withdraw(&step.id, &u, &mut out, &mut waiting);
628 }
629 }
630 if !out.is_empty() || waiting {
631 return out;
632 }
633 let mut done: Vec<(String, u64)> = vec![];
636 for s in def.steps.iter().filter(|s| s.compensation.is_some()) {
637 let keys = if s.for_each.is_some() {
638 element_keys(state, &s.id)
639 } else {
640 vec![s.id.clone()]
641 };
642 for key in keys {
643 if let Some(u) = state.units.get(&key) {
644 if u.status() == UnitStatus::Succeeded {
645 done.push((key, u.succeeded_seq.unwrap_or(0)));
646 }
647 }
648 }
649 }
650 done.sort_by_key(|d| std::cmp::Reverse(d.1));
651 if done.is_empty() {
652 return vec![finish(state, CompensationOutcome::None)];
653 }
654 let reason = match &state.abort {
655 Some(AbortReason::Failed { error }) => error.message.clone(),
656 Some(AbortReason::Cancelled { reason }) => format!("cancelled: {reason}"),
657 Some(AbortReason::TimedOut) => "workflow timed out".into(),
658 None => String::new(),
659 };
660 vec![Event::CompensationStarted {
661 reason,
662 steps: done.into_iter().map(|(s, _)| s).collect(),
663 }]
664}
665
666fn decide_compensating(state: &ExecutionState, def: &WorkflowIr) -> Vec<Event> {
667 let mut failures = vec![];
671 for step in &state.compensation_order {
672 let key = compensation_key(step);
673 let u = unit(state, &key);
674 match u.status() {
675 UnitStatus::Succeeded => continue,
676 UnitStatus::Failed | UnitStatus::Cancelled => {
677 let error = u
678 .error
679 .unwrap_or_else(|| TaskError::new("UNKNOWN", "compensation failed", false));
680 failures.push(Event::CompensationFailed {
681 step: step.clone(),
682 error,
683 });
684 continue;
685 }
686 UnitStatus::Pending => {
687 return match invocation(def, &key) {
688 Some(inv) => vec![schedule(state, def, &key, inv)],
689 None => vec![Event::StepFailed {
690 step: key,
691 error: TaskError::new("UNKNOWN", "missing compensation", false),
692 }],
693 };
694 }
695 _ => return vec![],
696 }
697 }
698 if !failures.is_empty() {
699 failures.push(finish(state, CompensationOutcome::Failed));
700 return failures;
701 }
702 vec![
703 Event::CompensationCompleted {},
704 finish(state, CompensationOutcome::Completed),
705 ]
706}