1use crate::engine::{ContractAnnouncement, RegisterWorker, TaskAssignment};
12use crate::model::*;
13use crate::{Engine, EngineConfig, Store};
14use orbit_ir::testing::{value_matches, Expectation, MockOutcome, TestCase};
15use orbit_ir::{eval, WORKFLOW_CONTRACT_PREFIX};
16use orbit_ir::{Action, Scope, SourceLocation, TaskContract, Type, WorkflowIr};
17use serde::Serialize;
18use serde_json::{json, Value};
19use std::collections::{BTreeMap, BTreeSet, HashMap};
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::sync::Arc;
22
23pub const SIMULATION_EPOCH_MS: u64 = 1_767_225_600_000;
25pub const ROOT_EXECUTION_ID: &str = "sim";
27const MAX_ROUNDS: usize = 100_000;
29const DEFAULT_RUNTIME: &str = "simulator";
31const MAX_SIMULATED_MS: u64 = 10 * 365 * 24 * 3600 * 1000;
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum TaskOutcome {
37 Complete(Value),
38 Fail(TaskError),
39 Hang,
41 Abort(String),
43}
44
45pub struct TaskCall<'a> {
47 pub assignment: &'a TaskAssignment,
48 pub contract: Option<&'a TaskContract>,
49 pub call: u32,
52}
53
54pub trait TaskHandler {
55 fn handle(&mut self, call: &TaskCall) -> TaskOutcome;
56}
57
58#[derive(Debug, Clone, Serialize)]
60pub struct SimulatedExecution {
61 pub state: ExecutionState,
62 pub history: Vec<HistoryEvent>,
63}
64
65#[derive(Debug, Clone, Serialize)]
66pub struct Simulation {
67 pub root: SimulatedExecution,
69 pub children: Vec<SimulatedExecution>,
71 pub problems: Vec<String>,
74 pub unsent_signals: Vec<String>,
76 pub elapsed_ms: u64,
78}
79
80impl Simulation {
81 pub fn finished(&self) -> bool {
82 self.root.state.status.is_terminal()
83 }
84}
85
86pub struct Simulator {
87 engine: Engine,
88 clock: Arc<AtomicU64>,
89 sessions: Vec<(String, String)>,
91 contracts: HashMap<String, TaskContract>,
92}
93
94impl Simulator {
95 pub fn new(workflows: &[WorkflowIr]) -> Result<Simulator, String> {
97 let clock = Arc::new(AtomicU64::new(SIMULATION_EPOCH_MS));
98 let c = clock.clone();
99 let config = EngineConfig {
100 session_timeout_ms: 1 << 50,
102 heartbeat_interval_ms: 1 << 40,
103 http_adapter: true,
105 ..EngineConfig::default()
106 };
107 let store = Store::in_memory().map_err(|e| e.to_string())?;
108 let mut engine = Engine::open(store, config, Box::new(move || c.load(Ordering::SeqCst)))
109 .map_err(|e| e.to_string())?;
110 let mut pending: Vec<&WorkflowIr> = workflows.iter().collect();
112 while !pending.is_empty() {
113 let mut next = vec![];
114 let mut last_error = None;
115 for wf in &pending {
116 match engine.register_definition((*wf).clone()) {
117 Ok(_) => {}
118 Err(e) if e.code == "CHILD_NOT_REGISTERED" => {
119 last_error = Some(e.message.clone());
120 next.push(*wf);
121 }
122 Err(e) => return Err(format!("{}@{}: {}", wf.name, wf.version, e.message)),
123 }
124 }
125 if next.len() == pending.len() {
126 return Err(last_error.unwrap_or_default());
127 }
128 pending = next;
129 }
130 let mut contracts = HashMap::new();
133 let mut runtimes = BTreeSet::from([DEFAULT_RUNTIME.to_string()]);
134 for wf in workflows {
135 for c in wf.contracts.values() {
136 if !c.name.starts_with(WORKFLOW_CONTRACT_PREFIX) {
137 contracts.insert(c.name.clone(), c.clone());
138 }
139 }
140 for s in &wf.steps {
141 for inv in s.invocation().into_iter().chain(s.compensation.as_ref()) {
142 if let Some(l) = &inv.language {
143 runtimes.insert(l.to_lowercase());
144 }
145 }
146 }
147 }
148 let announced: BTreeSet<(String, String)> = contracts
149 .values()
150 .filter(|c| !c.builtin)
151 .map(|c| (c.name.clone(), c.version.clone()))
152 .collect();
153 let mut sessions = vec![];
154 if !announced.is_empty() {
155 for runtime in runtimes {
156 let reg = engine
157 .register_worker(RegisterWorker {
158 worker_id: if runtime == DEFAULT_RUNTIME {
159 "simulator".to_string()
160 } else {
161 format!("simulator-{runtime}")
162 },
163 runtime: runtime.clone(),
164 sdk: Some("orbit-simulator".into()),
165 contracts: announced
166 .iter()
167 .map(|(n, v)| ContractAnnouncement {
168 task_name: n.clone(),
169 contract_version: v.clone(),
170 })
171 .collect(),
172 })
173 .map_err(|e| e.message)?;
174 sessions.push((runtime, reg.worker_session_id));
175 }
176 }
177 Ok(Simulator {
178 engine,
179 clock,
180 sessions,
181 contracts,
182 })
183 }
184
185 fn now(&self) -> u64 {
186 self.clock.load(Ordering::SeqCst)
187 }
188
189 pub fn run(
191 mut self,
192 workflow: &str,
193 version: Option<&str>,
194 input: Value,
195 signals: &[(String, Value)],
196 handler: &mut dyn TaskHandler,
197 ) -> Result<Simulation, String> {
198 let root = ROOT_EXECUTION_ID.to_string();
199 self.engine
200 .start(workflow, version, input, Some(root.clone()))
201 .map_err(|e| e.message)?;
202 let mut calls: HashMap<(String, String), u32> = HashMap::new();
203 let mut problems = vec![];
204 let mut next_signal = 0;
205 let mut rounds = 0;
206 let mut seen_progress = 0;
207 'run: loop {
208 rounds += 1;
209 if rounds > MAX_ROUNDS {
210 problems.push(format!(
211 "the simulation did not finish within {MAX_ROUNDS} rounds"
212 ));
213 break;
214 }
215 let mut work = self.engine.tick().map_err(|e| e.message)?;
216 for (_, session) in self.sessions.clone() {
217 while let Some(a) = self.engine.poll(&session).map_err(|e| e.message)? {
218 work.push(a);
219 }
220 }
221 for a in work {
222 let n = calls
223 .entry((a.execution_id.clone(), a.step_id.clone()))
224 .or_default();
225 *n += 1;
226 let call = TaskCall {
227 assignment: &a,
228 contract: self.contracts.get(&a.task_name),
229 call: *n,
230 };
231 match handler.handle(&call) {
232 TaskOutcome::Complete(v) => {
233 self.engine.complete(&a.token, v).map_err(|e| e.message)?;
234 }
235 TaskOutcome::Fail(e) => {
236 self.engine.fail(&a.token, e).map_err(|e| e.message)?;
237 }
238 TaskOutcome::Hang => {}
239 TaskOutcome::Abort(msg) => {
240 problems.push(msg);
241 break 'run;
242 }
243 }
244 }
245 let state = self.engine.execution(&root).map_err(|e| e.message)?;
246 if state.status.is_terminal() {
247 break;
248 }
249 let progress = self.progress()?;
252 if progress != seen_progress {
253 seen_progress = progress;
254 continue;
255 }
256 if let Some((name, payload)) = signals.get(next_signal) {
258 if self.awaiting(&state).contains(name.as_str()) {
259 self.engine
260 .signal(&root, name, payload.clone())
261 .map_err(|e| e.message)?;
262 next_signal += 1;
263 continue;
264 }
265 }
266 match self.next_wake_up()? {
268 Some(at) if at - SIMULATION_EPOCH_MS <= MAX_SIMULATED_MS => {
269 self.clock.store(at, Ordering::SeqCst);
270 }
271 Some(_) => {
272 problems.push("the simulation ran past ten years of simulated time".into());
273 break;
274 }
275 None => {
276 problems.push(self.stuck_reason(&state, signals.get(next_signal)));
277 break;
278 }
279 }
280 }
281 let unsent_signals: Vec<String> = signals[next_signal.min(signals.len())..]
282 .iter()
283 .map(|(n, _)| n.clone())
284 .collect();
285 let mut children = vec![];
286 for s in self
287 .engine
288 .list_executions(None, None, 1000)
289 .map_err(|e| e.message)?
290 {
291 if s.execution_id != root {
292 children.push(self.collect(&s.execution_id)?);
293 }
294 }
295 children.sort_by(|a, b| a.state.id.cmp(&b.state.id));
296 let root = self.collect(&root)?;
297 let end = root.state.finished_at_ms.unwrap_or(self.now());
298 Ok(Simulation {
299 elapsed_ms: end.saturating_sub(root.state.created_at_ms),
300 root,
301 children,
302 problems,
303 unsent_signals,
304 })
305 }
306
307 fn progress(&self) -> Result<u64, String> {
309 let mut total = 0;
310 for s in self
311 .engine
312 .list_executions(None, None, 1000)
313 .map_err(|e| e.message)?
314 {
315 total += 1 + self
316 .engine
317 .execution(&s.execution_id)
318 .map_err(|e| e.message)?
319 .last_seq;
320 }
321 Ok(total)
322 }
323
324 fn collect(&self, id: &str) -> Result<SimulatedExecution, String> {
325 Ok(SimulatedExecution {
326 state: self.engine.execution(id).map_err(|e| e.message)?,
327 history: self.engine.history(id).map_err(|e| e.message)?,
328 })
329 }
330
331 fn definition(&self, state: &ExecutionState) -> Option<Arc<WorkflowIr>> {
332 self.engine
333 .definition(&state.workflow, Some(&state.version))
334 }
335
336 fn awaiting(&self, state: &ExecutionState) -> BTreeSet<String> {
338 let Some(def) = self.definition(state) else {
339 return BTreeSet::new();
340 };
341 state
342 .units
343 .iter()
344 .filter(|(_, u)| u.status() == UnitStatus::Waiting)
345 .filter_map(|(key, _)| awaited(&def, key).map(str::to_string))
346 .collect()
347 }
348
349 fn next_wake_up(&self) -> Result<Option<u64>, String> {
352 let now = self.now();
353 let mut best: Option<u64> = None;
354 let mut consider = |t: u64| {
355 if t > now && best.is_none_or(|b| t < b) {
356 best = Some(t);
357 }
358 };
359 for s in self
360 .engine
361 .list_executions(None, None, 1000)
362 .map_err(|e| e.message)?
363 {
364 let state = self
365 .engine
366 .execution(&s.execution_id)
367 .map_err(|e| e.message)?;
368 if state.status.is_terminal() {
369 continue;
370 }
371 let def = self.definition(&state);
372 if let (Some(def), None) = (&def, &state.abort) {
373 if let Some(t) = def.timeout_ms {
374 consider(state.created_at_ms + t);
375 }
376 }
377 for (key, u) in &state.units {
378 match u.status() {
379 UnitStatus::Waiting | UnitStatus::RetryWait => {
380 if let Some(t) = u.wake_at_ms {
381 consider(t);
382 }
383 }
384 UnitStatus::Dispatched => {
385 if let Some(t) = u.deadline_ms {
386 consider(t);
387 }
388 }
389 UnitStatus::Scheduled => {
390 let qt = def
391 .as_ref()
392 .and_then(|d| crate::decide::invocation(d, key))
393 .and_then(|i| i.queue_timeout_ms);
394 if let (Some(qt), Some(at), false) = (qt, u.scheduled_at_ms, u.redelivery) {
395 consider(at + qt);
396 }
397 }
398 _ => {}
399 }
400 }
401 }
402 Ok(best)
403 }
404
405 fn stuck_reason(&self, state: &ExecutionState, next: Option<&(String, Value)>) -> String {
406 let def = self.definition(state);
407 let mut waits = vec![];
408 for (key, u) in &state.units {
409 match u.status() {
410 UnitStatus::Waiting => {
411 if let Some(sig) = def.as_ref().and_then(|d| awaited(d, key)) {
412 let detail = match next {
413 Some((n, _)) if n != sig => format!(
414 "signal `{sig}`, but the next signal to send is `{n}` (signals are sent in order)"
415 ),
416 Some(_) => format!("signal `{sig}`"),
417 None => format!("signal `{sig}`, and no signal is left to send"),
418 };
419 waits.push(format!("step `{key}` waits for {detail}"));
420 }
421 }
422 UnitStatus::Dispatched => {
423 let task = def
424 .as_ref()
425 .and_then(|d| crate::decide::invocation(d, key))
426 .map(|i| i.task.clone())
427 .unwrap_or_default();
428 if !task.starts_with(WORKFLOW_CONTRACT_PREFIX) {
429 waits.push(format!(
430 "step `{key}` waits for task `{task}`, which never answers and has no timeout"
431 ));
432 }
433 }
434 _ => {}
435 }
436 }
437 if waits.is_empty() {
438 format!(
439 "the execution stopped making progress while {}",
440 state.status.as_str()
441 )
442 } else {
443 format!("the execution cannot make progress: {}", waits.join("; "))
444 }
445 }
446}
447
448fn awaited<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a str> {
450 let k = parse_key(key);
451 if k.compensation {
452 return None;
453 }
454 match &def.step(k.step)?.action {
455 Action::Await { signal, .. } => Some(signal),
456 _ => None,
457 }
458}
459
460pub fn simulate(
462 workflows: &[WorkflowIr],
463 workflow: &str,
464 version: Option<&str>,
465 input: Value,
466 signals: &[(String, Value)],
467 handler: &mut dyn TaskHandler,
468) -> Result<Simulation, String> {
469 Simulator::new(workflows)?.run(workflow, version, input, signals, handler)
470}
471
472pub fn sample_value(ty: &Type, name: &str) -> Value {
493 match ty {
494 Type::String => json!(format!("{name}-sample")),
495 Type::Int => json!(1),
496 Type::Float => json!(1.0),
497 Type::Bool => json!(true),
498 Type::Bytes => json!("c2FtcGxl"),
499 Type::Duration => json!(1000),
500 Type::Timestamp => json!("2026-01-01T00:00:00Z"),
501 Type::Uuid => json!("00000000-0000-4000-8000-000000000001"),
502 Type::Json => json!({}),
503 Type::Null | Type::Optional { .. } => Value::Null,
504 Type::List { item } => json!([sample_value(item, name)]),
505 Type::Map { value, .. } => json!({ "key": sample_value(value, name) }),
506 Type::Record { fields, .. } => Value::Object(
507 fields
508 .iter()
509 .map(|f| {
510 let v = f
511 .default
512 .clone()
513 .unwrap_or_else(|| sample_value(&f.ty, &f.name));
514 (f.name.clone(), v)
515 })
516 .collect(),
517 ),
518 }
519}
520
521#[derive(Debug, Clone, Default)]
524pub struct SampleHandler {
525 pub results: BTreeMap<String, Value>,
527 pub failures: BTreeMap<String, String>,
529}
530
531impl TaskHandler for SampleHandler {
532 fn handle(&mut self, call: &TaskCall) -> TaskOutcome {
533 let task = &call.assignment.task_name;
534 if let Some(code) = self.failures.get(task) {
535 return TaskOutcome::Fail(TaskError::new(
536 code,
537 format!("simulated failure of `{task}` (--fail)"),
538 false,
539 ));
540 }
541 if let Some(v) = self.results.get(task) {
542 return TaskOutcome::Complete(v.clone());
543 }
544 TaskOutcome::Complete(
545 call.contract
546 .map(|c| sample_value(&c.output, "value"))
547 .unwrap_or_else(|| json!({})),
548 )
549 }
550}
551
552pub struct MockHandler<'a> {
556 pub test: &'a TestCase,
557}
558
559struct TaskScope<'a> {
560 input: &'a Value,
561 assignment: &'a TaskAssignment,
562}
563
564impl Scope for TaskScope<'_> {
565 fn input(&self) -> &Value {
566 self.input
567 }
568 fn step_output(&self, _: &str) -> Option<&Value> {
569 None
570 }
571 fn execution_field(&self, field: &str) -> Option<Value> {
572 Some(Value::String(match field {
573 "id" => self.assignment.execution_id.clone(),
574 "workflow" => self.assignment.workflow.clone(),
575 "version" => self.assignment.workflow_version.clone(),
576 _ => return None,
577 }))
578 }
579}
580
581impl TaskHandler for MockHandler<'_> {
582 fn handle(&mut self, call: &TaskCall) -> TaskOutcome {
583 let a = call.assignment;
584 let Some(behaviours) = self.test.mocks.get(&a.task_name) else {
585 return TaskOutcome::Abort(format!(
586 "task `{}` (step `{}`) is not mocked; add `mock \"{}\" returns {{ ... }}` to the test",
587 a.task_name, a.step_id, a.task_name
588 ));
589 };
590 let mut covered = 0u32;
591 let mut chosen = behaviours.last();
592 for b in behaviours {
593 match b.times {
594 Some(n) => {
595 covered = covered.saturating_add(n);
596 if call.call <= covered {
597 chosen = Some(b);
598 break;
599 }
600 }
601 None => {
602 chosen = Some(b);
603 break;
604 }
605 }
606 }
607 match chosen.map(|b| &b.outcome) {
608 Some(MockOutcome::Returns { output }) => {
609 let scope = TaskScope {
610 input: &a.input,
611 assignment: a,
612 };
613 match eval(output, &scope) {
614 Ok(v) => TaskOutcome::Complete(v),
615 Err(e) => TaskOutcome::Abort(format!(
616 "the mock of `{}` (step `{}`) failed to evaluate: {e}",
617 a.task_name, a.step_id
618 )),
619 }
620 }
621 Some(MockOutcome::Fails {
622 code,
623 message,
624 retryable,
625 }) => TaskOutcome::Fail(TaskError::new(code, message.clone(), *retryable)),
626 Some(MockOutcome::Hangs) => TaskOutcome::Hang,
627 None => TaskOutcome::Abort(format!("the mock of `{}` has no behaviour", a.task_name)),
628 }
629 }
630}
631
632#[derive(Debug, Clone, Serialize)]
634pub struct TestResult {
635 pub name: String,
636 pub workflow: String,
637 pub version: String,
638 pub location: SourceLocation,
639 pub passed: bool,
640 pub failures: Vec<String>,
642 #[serde(skip)]
643 pub simulation: Option<Simulation>,
644}
645
646pub fn run_test(workflows: &[WorkflowIr], test: &TestCase) -> TestResult {
649 let mut result = TestResult {
650 name: test.name.clone(),
651 workflow: test.workflow.clone(),
652 version: test.version.clone(),
653 location: test.location.clone(),
654 passed: false,
655 failures: vec![],
656 simulation: None,
657 };
658 let signals: Vec<(String, Value)> = test
659 .signals
660 .iter()
661 .map(|s| (s.name.clone(), s.payload.clone()))
662 .collect();
663 let mut handler = MockHandler { test };
664 let sim = match simulate(
665 workflows,
666 &test.workflow,
667 Some(&test.version),
668 test.input.clone(),
669 &signals,
670 &mut handler,
671 ) {
672 Ok(s) => s,
673 Err(e) => {
674 result
675 .failures
676 .push(format!("could not start the execution: {e}"));
677 return result;
678 }
679 };
680 result.failures.extend(sim.problems.iter().cloned());
681 if sim.finished() {
682 for e in &test.expectations {
683 if let Some(f) = check(e, &sim.root.state) {
684 result.failures.push(f);
685 }
686 }
687 for s in &sim.unsent_signals {
688 result.failures.push(format!(
689 "signal `{s}` was never sent: the execution finished without waiting for it"
690 ));
691 }
692 }
693 result.passed = result.failures.is_empty();
694 result.simulation = Some(sim);
695 result
696}
697
698fn compact(v: &Value) -> String {
699 serde_json::to_string(v).unwrap_or_default()
700}
701
702fn status_name(s: UnitStatus) -> String {
703 serde_json::to_value(s)
704 .ok()
705 .and_then(|v| v.as_str().map(str::to_string))
706 .unwrap_or_default()
707}
708
709fn describe_end(state: &ExecutionState) -> String {
710 match &state.error {
711 Some(e) => format!("{} ({}: {})", state.status.as_str(), e.code, e.message),
712 None => state.status.as_str().to_string(),
713 }
714}
715
716fn check(e: &Expectation, state: &ExecutionState) -> Option<String> {
718 let unit = |step: &str| state.units.get(step).cloned().unwrap_or_default();
719 match e {
720 Expectation::Status { status } => (state.status.as_str() != status)
721 .then(|| format!("expected status {status}, got {}", describe_end(state))),
722 Expectation::Output { expected } => {
723 let actual = state.output.clone().unwrap_or(Value::Null);
724 (!value_matches(expected, &actual)).then(|| {
725 if state.output.is_none() {
726 format!(
727 "expected output {}, but the workflow produced none (it ended {})",
728 compact(expected),
729 describe_end(state)
730 )
731 } else {
732 format!(
733 "expected output {}, got {}",
734 compact(expected),
735 compact(&actual)
736 )
737 }
738 })
739 }
740 Expectation::Error { code } => match &state.error {
741 Some(err) if err.code == *code => None,
742 Some(err) => Some(format!(
743 "expected error {code:?}, got {:?} ({})",
744 err.code, err.message
745 )),
746 None => Some(format!(
747 "expected error {code:?}, but the workflow ended {} without an error",
748 state.status.as_str()
749 )),
750 },
751 Expectation::StepStatus { step, status } => {
752 let actual = status_name(unit(step).status());
753 (actual != *status).then(|| {
754 let u = unit(step);
755 let why = u
756 .error
757 .as_ref()
758 .map(|e| format!(" ({}: {})", e.code, e.message))
759 .unwrap_or_default();
760 format!("expected step `{step}` to be {status}, got {actual}{why}")
761 })
762 }
763 Expectation::StepOutput { step, expected } => {
764 let u = unit(step);
765 let actual = u.output.clone().unwrap_or(Value::Null);
766 (!value_matches(expected, &actual)).then(|| {
767 format!(
768 "expected step `{step}` output {}, got {} (step is {})",
769 compact(expected),
770 compact(&actual),
771 status_name(u.status())
772 )
773 })
774 }
775 Expectation::StepAttempts { step, attempts } => {
776 let actual = unit(step).attempt;
777 (actual != *attempts).then(|| {
778 format!("expected step `{step}` to take {attempts} attempt(s), took {actual}")
779 })
780 }
781 Expectation::Compensated { step } => {
782 let element = format!("{}[", compensation_key(step));
783 let units: Vec<(&String, &UnitState)> = state
784 .units
785 .iter()
786 .filter(|(k, _)| **k == compensation_key(step) || k.starts_with(&element))
787 .collect();
788 if units.is_empty() {
789 return Some(format!(
790 "expected step `{step}` to be compensated, but its compensation never ran"
791 ));
792 }
793 units
794 .iter()
795 .find(|(_, u)| u.status() != UnitStatus::Succeeded)
796 .map(|(k, u)| {
797 let why = u
798 .error
799 .as_ref()
800 .map(|e| format!(" ({}: {})", e.code, e.message))
801 .unwrap_or_default();
802 format!(
803 "expected step `{step}` to be compensated, but `{k}` is {}{why}",
804 status_name(u.status())
805 )
806 })
807 }
808 }
809}