1use chrono::{DateTime, Utc};
2use std::sync::Arc;
3use uuid::Uuid;
4
5use crate::error::{FlowError, Result};
6use crate::model::{
7 project_run, ActiveHookSnapshot, FlowEvent, FlowEventEnvelope, HookStatus, RuntimeCommand,
8 ScheduledWakeup, ScheduledWakeupKind, StepStatus, WaitStatus, WorkflowRunSnapshot,
9 WorkflowRunStatus, WorkflowRunSummary, WorkflowRunSuspension, WorkflowSpec,
10};
11use crate::observe::{FlowEventObserver, NoopFlowEventObserver};
12use crate::runtime::{FlowRuntime, WorkflowInvocation};
13use crate::store::{FlowEventStore, InMemoryEventStore};
14
15mod operations;
16mod steps;
17mod validation;
18use steps::StepExecutionContext;
19use validation::{
20 ensure_child_operation_matches, ensure_hook_command_matches, ensure_progress_matches,
21 ensure_same_start, ensure_step_batch_valid, ensure_step_command_matches,
22 ensure_wait_command_matches, is_event_conflict, validate_run_id,
23};
24
25pub struct FlowEngineBuilder {
27 store: Arc<dyn FlowEventStore>,
28 runtime: Arc<dyn FlowRuntime>,
29 observer: Arc<dyn FlowEventObserver>,
30 max_replay_iterations: usize,
31}
32
33impl FlowEngineBuilder {
34 pub fn new(runtime: Arc<dyn FlowRuntime>) -> Self {
35 Self {
36 store: Arc::new(InMemoryEventStore::new()),
37 runtime,
38 observer: Arc::new(NoopFlowEventObserver),
39 max_replay_iterations: 1024,
40 }
41 }
42
43 pub fn with_store(mut self, store: Arc<dyn FlowEventStore>) -> Self {
44 self.store = store;
45 self
46 }
47
48 pub fn with_observer(mut self, observer: Arc<dyn FlowEventObserver>) -> Self {
49 self.observer = observer;
50 self
51 }
52
53 pub fn with_max_replay_iterations(mut self, max_replay_iterations: usize) -> Self {
54 self.max_replay_iterations = max_replay_iterations.max(1);
55 self
56 }
57
58 pub fn build(self) -> FlowEngine {
59 FlowEngine {
60 store: self.store,
61 runtime: self.runtime,
62 observer: self.observer,
63 max_replay_iterations: self.max_replay_iterations,
64 }
65 }
66}
67
68#[derive(Clone)]
70pub struct FlowEngine {
71 store: Arc<dyn FlowEventStore>,
72 runtime: Arc<dyn FlowRuntime>,
73 observer: Arc<dyn FlowEventObserver>,
74 max_replay_iterations: usize,
75}
76
77impl FlowEngine {
78 pub fn builder(runtime: Arc<dyn FlowRuntime>) -> FlowEngineBuilder {
79 FlowEngineBuilder::new(runtime)
80 }
81
82 pub fn new(store: Arc<dyn FlowEventStore>, runtime: Arc<dyn FlowRuntime>) -> Self {
83 Self {
84 store,
85 runtime,
86 observer: Arc::new(NoopFlowEventObserver),
87 max_replay_iterations: 1024,
88 }
89 }
90
91 pub fn in_memory(runtime: Arc<dyn FlowRuntime>) -> Self {
92 Self::new(Arc::new(InMemoryEventStore::new()), runtime)
93 }
94
95 pub fn store(&self) -> Arc<dyn FlowEventStore> {
96 Arc::clone(&self.store)
97 }
98
99 pub fn observer(&self) -> Arc<dyn FlowEventObserver> {
100 Arc::clone(&self.observer)
101 }
102
103 pub async fn start(&self, spec: WorkflowSpec, input: serde_json::Value) -> Result<String> {
105 let run_id = Uuid::new_v4().to_string();
106 self.start_with_id(run_id, spec, input).await
107 }
108
109 pub async fn start_with_id(
114 &self,
115 run_id: impl Into<String>,
116 spec: WorkflowSpec,
117 input: serde_json::Value,
118 ) -> Result<String> {
119 spec.validate()?;
120 let run_id = run_id.into();
121 validate_run_id(&run_id)?;
122
123 for _ in 0..self.max_replay_iterations {
124 match self.store.list(&run_id).await {
125 Ok(history) => {
126 let snapshot = project_run(&run_id, &history)?;
127 ensure_same_start(&run_id, &snapshot, &spec, &input)?;
128 if !history
129 .iter()
130 .any(|event| matches!(event.event, FlowEvent::RunStarted))
131 {
132 match self
133 .record_event_at(&run_id, snapshot.last_sequence, FlowEvent::RunStarted)
134 .await
135 {
136 Ok(_) => {}
137 Err(err) if is_event_conflict(&err) => continue,
138 Err(err) => return Err(err),
139 }
140 }
141 match self.drive(&run_id).await {
142 Ok(_) => return Ok(run_id),
143 Err(err) if is_event_conflict(&err) => continue,
144 Err(err) => return Err(err),
145 }
146 }
147 Err(FlowError::RunNotFound(_)) => {
148 let created = match self
149 .record_event_at(
150 &run_id,
151 0,
152 FlowEvent::RunCreated {
153 spec: spec.clone(),
154 input: input.clone(),
155 },
156 )
157 .await
158 {
159 Ok(created) => created,
160 Err(err) if is_event_conflict(&err) => continue,
161 Err(err) => return Err(err),
162 };
163 match self
164 .record_event_at(&run_id, created.sequence, FlowEvent::RunStarted)
165 .await
166 {
167 Ok(_) => {}
168 Err(err) if is_event_conflict(&err) => continue,
169 Err(err) => return Err(err),
170 }
171 match self.drive(&run_id).await {
172 Ok(_) => return Ok(run_id),
173 Err(err) if is_event_conflict(&err) => continue,
174 Err(err) => return Err(err),
175 }
176 }
177 Err(err) => return Err(err),
178 }
179 }
180
181 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
182 }
183
184 pub async fn resume_wait(&self, run_id: &str, wait_id: &str) -> Result<()> {
186 for _ in 0..self.max_replay_iterations {
187 let snapshot = self.snapshot(run_id).await?;
188 if snapshot.status.is_terminal() {
189 return Err(FlowError::RunTerminal(run_id.to_string()));
190 }
191 match snapshot.waits.get(wait_id) {
192 Some(wait) if wait.status == WaitStatus::Waiting => {
193 match self
194 .record_event_at(
195 run_id,
196 snapshot.last_sequence,
197 FlowEvent::WaitCompleted {
198 wait_id: wait_id.to_string(),
199 },
200 )
201 .await
202 {
203 Ok(_) => {}
204 Err(err) if is_event_conflict(&err) => continue,
205 Err(err) => return Err(err),
206 }
207 match self.drive(run_id).await {
208 Ok(_) => return Ok(()),
209 Err(err) if is_event_conflict(&err) => continue,
210 Err(err) => return Err(err),
211 }
212 }
213 Some(_) => match self.drive(run_id).await {
214 Ok(_) => return Ok(()),
215 Err(err) if is_event_conflict(&err) => continue,
216 Err(err) => return Err(err),
217 },
218 None => {
219 return Err(FlowError::InvalidTransition(format!(
220 "wait {wait_id} does not exist for run {run_id}"
221 )))
222 }
223 }
224 }
225
226 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
227 }
228
229 pub async fn resume_hook(
231 &self,
232 run_id: &str,
233 hook_id: &str,
234 payload: serde_json::Value,
235 ) -> Result<()> {
236 for _ in 0..self.max_replay_iterations {
237 let snapshot = self.snapshot(run_id).await?;
238 if snapshot.status.is_terminal() {
239 return Err(FlowError::RunTerminal(run_id.to_string()));
240 }
241 match snapshot.hooks.get(hook_id) {
242 Some(hook) if hook.status == HookStatus::Active => {
243 match self
244 .record_event_at(
245 run_id,
246 snapshot.last_sequence,
247 FlowEvent::HookReceived {
248 hook_id: hook_id.to_string(),
249 payload: payload.clone(),
250 },
251 )
252 .await
253 {
254 Ok(_) => {}
255 Err(err) if is_event_conflict(&err) => continue,
256 Err(err) => return Err(err),
257 }
258 match self.drive(run_id).await {
259 Ok(_) => return Ok(()),
260 Err(err) if is_event_conflict(&err) => continue,
261 Err(err) => return Err(err),
262 }
263 }
264 Some(_) => match self.drive(run_id).await {
265 Ok(_) => return Ok(()),
266 Err(err) if is_event_conflict(&err) => continue,
267 Err(err) => return Err(err),
268 },
269 None => {
270 return Err(FlowError::InvalidTransition(format!(
271 "hook {hook_id} does not exist for run {run_id}"
272 )))
273 }
274 }
275 }
276
277 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
278 }
279
280 pub async fn dispose_hook(&self, run_id: &str, hook_id: &str) -> Result<()> {
288 for _ in 0..self.max_replay_iterations {
289 let snapshot = self.snapshot(run_id).await?;
290 if snapshot.status.is_terminal() {
291 return Err(FlowError::RunTerminal(run_id.to_string()));
292 }
293 match snapshot.hooks.get(hook_id) {
294 Some(hook) if hook.status == HookStatus::Active => {
295 match self
296 .record_event_at(
297 run_id,
298 snapshot.last_sequence,
299 FlowEvent::HookDisposed {
300 hook_id: hook_id.to_string(),
301 },
302 )
303 .await
304 {
305 Ok(_) => {}
306 Err(err) if is_event_conflict(&err) => continue,
307 Err(err) => return Err(err),
308 }
309 match self.drive(run_id).await {
310 Ok(_) => return Ok(()),
311 Err(err) if is_event_conflict(&err) => continue,
312 Err(err) => return Err(err),
313 }
314 }
315 Some(_) => match self.drive(run_id).await {
316 Ok(_) => return Ok(()),
317 Err(err) if is_event_conflict(&err) => continue,
318 Err(err) => return Err(err),
319 },
320 None => {
321 return Err(FlowError::InvalidTransition(format!(
322 "hook {hook_id} does not exist for run {run_id}"
323 )))
324 }
325 }
326 }
327
328 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
329 }
330
331 pub async fn resume_hook_by_token(
336 &self,
337 token: &str,
338 payload: serde_json::Value,
339 ) -> Result<(String, String)> {
340 let mut matches = self
341 .store
342 .find_active_hooks_by_token(token)
343 .await?
344 .into_iter()
345 .map(|active| (active.run_id, active.hook.hook_id))
346 .collect::<Vec<_>>();
347
348 match matches.len() {
349 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
350 1 => {
351 let (run_id, hook_id) = matches.remove(0);
352 self.resume_hook(&run_id, &hook_id, payload).await?;
353 Ok((run_id, hook_id))
354 }
355 _ => Err(FlowError::InvalidTransition(
356 "hook token is active in multiple runs (value redacted)".to_string(),
357 )),
358 }
359 }
360
361 pub async fn dispose_hook_by_token(&self, token: &str) -> Result<(String, String)> {
366 let mut matches = self
367 .store
368 .find_active_hooks_by_token(token)
369 .await?
370 .into_iter()
371 .map(|active| (active.run_id, active.hook.hook_id))
372 .collect::<Vec<_>>();
373
374 match matches.len() {
375 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
376 1 => {
377 let (run_id, hook_id) = matches.remove(0);
378 self.dispose_hook(&run_id, &hook_id).await?;
379 Ok((run_id, hook_id))
380 }
381 _ => Err(FlowError::InvalidTransition(
382 "hook token is active in multiple runs (value redacted)".to_string(),
383 )),
384 }
385 }
386
387 pub async fn list_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
392 let mut due = self
393 .list_due_wakeups(now)
394 .await?
395 .into_iter()
396 .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Wait)
397 .map(|wakeup| (wakeup.run_id, wakeup.subject_id))
398 .collect::<Vec<_>>();
399 due.sort();
400 Ok(due)
401 }
402
403 pub async fn resume_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
408 let due = self.list_due_waits(now).await?;
409 let mut resumed = Vec::with_capacity(due.len());
410 for (run_id, wait_id) in due {
411 self.resume_wait(&run_id, &wait_id).await?;
412 resumed.push((run_id, wait_id));
413 }
414 Ok(resumed)
415 }
416
417 pub async fn list_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
419 let mut due = self
420 .list_due_wakeups(now)
421 .await?
422 .into_iter()
423 .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Retry)
424 .map(|wakeup| (wakeup.run_id, wakeup.subject_id))
425 .collect::<Vec<_>>();
426 due.sort();
427 Ok(due)
428 }
429
430 pub async fn list_due_wakeups(&self, now: DateTime<Utc>) -> Result<Vec<ScheduledWakeup>> {
432 let mut wakeups = self.store.list_due_wakeups(now).await?;
433 wakeups.sort_by(|left, right| {
434 (left.kind, left.run_id.as_str(), left.subject_id.as_str()).cmp(&(
435 right.kind,
436 right.run_id.as_str(),
437 right.subject_id.as_str(),
438 ))
439 });
440 Ok(wakeups)
441 }
442
443 pub async fn resume_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
445 let due = self.list_due_retries(now).await?;
446 let mut run_ids = Vec::new();
447 for (run_id, _) in &due {
448 if !run_ids.contains(run_id) {
449 run_ids.push(run_id.clone());
450 }
451 }
452 for run_id in run_ids {
453 self.drive_at(&run_id, now).await?;
454 }
455 Ok(due)
456 }
457
458 pub async fn snapshot(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
459 let history = self.store.list(run_id).await?;
460 project_run(run_id, &history)
461 }
462
463 pub async fn history(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
464 self.store.list(run_id).await
465 }
466
467 pub async fn list_run_ids(&self) -> Result<Vec<String>> {
468 self.store.list_run_ids().await
469 }
470
471 pub async fn list_snapshots(&self) -> Result<Vec<WorkflowRunSnapshot>> {
472 let mut snapshots = Vec::new();
473 for run_id in self.store.list_run_ids().await? {
474 snapshots.push(self.snapshot(&run_id).await?);
475 }
476 Ok(snapshots)
477 }
478
479 pub async fn run_summary(&self) -> Result<WorkflowRunSummary> {
485 let snapshots = self.list_snapshots().await?;
486 Ok(WorkflowRunSummary::from_snapshots(&snapshots))
487 }
488
489 pub async fn list_open_suspensions(
495 &self,
496 now: DateTime<Utc>,
497 ) -> Result<Vec<WorkflowRunSuspension>> {
498 let mut suspensions = Vec::new();
499 for run_id in self.store.list_run_ids().await? {
500 let snapshot = self.snapshot(&run_id).await?;
501 if snapshot.status.is_terminal() {
502 continue;
503 }
504 for wait in snapshot.waits.values() {
505 if wait.status == WaitStatus::Waiting {
506 suspensions.push(WorkflowRunSuspension::Wait {
507 run_id: run_id.clone(),
508 wait: wait.clone(),
509 due: wait.resume_at <= now,
510 });
511 }
512 }
513 for hook in snapshot.hooks.values() {
514 if hook.status == HookStatus::Active {
515 suspensions.push(WorkflowRunSuspension::Hook {
516 run_id: run_id.clone(),
517 hook: hook.clone(),
518 });
519 }
520 }
521 for step in snapshot.steps.values() {
522 if step.status == StepStatus::Pending {
523 if let Some(retry_after) = step.retry_after {
524 suspensions.push(WorkflowRunSuspension::Retry {
525 run_id: run_id.clone(),
526 step: step.clone(),
527 due: retry_after <= now,
528 });
529 }
530 }
531 }
532 }
533 suspensions.sort_by(|left, right| {
534 (left.run_id(), left.kind_order(), left.subject_id()).cmp(&(
535 right.run_id(),
536 right.kind_order(),
537 right.subject_id(),
538 ))
539 });
540 Ok(suspensions)
541 }
542
543 pub async fn next_wakeup(&self, now: DateTime<Utc>) -> Result<Option<WorkflowRunSuspension>> {
549 for _ in 0..2 {
550 let Some(wakeup) = self.store.next_scheduled_wakeup().await? else {
551 return Ok(None);
552 };
553 match self.snapshot(&wakeup.run_id).await {
554 Ok(snapshot) => {
555 if let Some(suspension) = resolve_scheduled_wakeup(&snapshot, &wakeup, now) {
556 return Ok(Some(suspension));
557 }
558 }
559 Err(FlowError::RunNotFound(_)) => {}
560 Err(error) => return Err(error),
561 }
562 }
563
564 self.next_wakeup_by_replay(now).await
565 }
566
567 async fn next_wakeup_by_replay(
568 &self,
569 now: DateTime<Utc>,
570 ) -> Result<Option<WorkflowRunSuspension>> {
571 let mut wakeups = self.list_open_suspensions(now).await?;
572 wakeups.retain(|suspension| suspension.scheduled_at().is_some());
573 wakeups.sort_by(|left, right| {
574 (
575 left.scheduled_at(),
576 left.run_id(),
577 left.kind_order(),
578 left.subject_id(),
579 )
580 .cmp(&(
581 right.scheduled_at(),
582 right.run_id(),
583 right.kind_order(),
584 right.subject_id(),
585 ))
586 });
587 Ok(wakeups.into_iter().next())
588 }
589
590 pub async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
596 self.store.list_active_hooks().await
597 }
598
599 pub async fn drive(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
602 self.drive_at(run_id, Utc::now()).await
603 }
604
605 async fn drive_at(&self, run_id: &str, now: DateTime<Utc>) -> Result<WorkflowRunSnapshot> {
606 'replay: for _ in 0..self.max_replay_iterations {
607 let history = self.store.list(run_id).await?;
608 let snapshot = project_run(run_id, &history)?;
609 if snapshot.status.is_terminal()
610 || snapshot
611 .waits
612 .values()
613 .any(|wait| wait.status == WaitStatus::Waiting)
614 || snapshot
615 .hooks
616 .values()
617 .any(|hook| hook.status == HookStatus::Active)
618 || (snapshot.has_future_retry(now) && snapshot.due_retries(now).is_empty())
619 {
620 return Ok(snapshot);
621 }
622
623 let command = self
624 .runtime
625 .run_workflow(WorkflowInvocation {
626 run_id: run_id.to_string(),
627 spec: snapshot.spec.clone(),
628 input: snapshot.input.clone(),
629 history,
630 })
631 .await?;
632
633 match command {
634 RuntimeCommand::Complete { output } => {
635 if snapshot.status == WorkflowRunStatus::Cancelling {
636 return Err(FlowError::InvalidTransition(format!(
637 "workflow run {run_id} completed after cancellation was requested; cleanup-aware cancellation must return cancel or fail"
638 )));
639 }
640 match self
641 .record_event_at(
642 run_id,
643 snapshot.last_sequence,
644 FlowEvent::RunCompleted { output },
645 )
646 .await
647 {
648 Ok(_) => {}
649 Err(err) if is_event_conflict(&err) => continue,
650 Err(err) => return Err(err),
651 }
652 return self.snapshot(run_id).await;
653 }
654 RuntimeCommand::Fail { error } => {
655 match self
656 .record_event_at(
657 run_id,
658 snapshot.last_sequence,
659 FlowEvent::RunFailed { error },
660 )
661 .await
662 {
663 Ok(_) => {}
664 Err(err) if is_event_conflict(&err) => continue,
665 Err(err) => return Err(err),
666 }
667 return self.snapshot(run_id).await;
668 }
669 RuntimeCommand::Cancel => {
670 let cancellation = snapshot.cancellation.as_ref().ok_or_else(|| {
671 FlowError::InvalidTransition(format!(
672 "workflow run {run_id} returned cancel without a durable cancellation request"
673 ))
674 })?;
675 match self
676 .record_event_at(
677 run_id,
678 snapshot.last_sequence,
679 FlowEvent::RunCancelled {
680 reason: cancellation.request.reason.clone(),
681 },
682 )
683 .await
684 {
685 Ok(_) => {}
686 Err(err) if is_event_conflict(&err) => continue,
687 Err(err) => return Err(err),
688 }
689 return self.snapshot(run_id).await;
690 }
691 RuntimeCommand::Timeout { deadline, reason } => {
692 match self
693 .record_event_at(
694 run_id,
695 snapshot.last_sequence,
696 FlowEvent::RunTimedOut { deadline, reason },
697 )
698 .await
699 {
700 Ok(_) => {}
701 Err(err) if is_event_conflict(&err) => continue,
702 Err(err) => return Err(err),
703 }
704 return self.snapshot(run_id).await;
705 }
706 RuntimeCommand::RecordProgress { progress } => {
707 progress.validate()?;
708 if let Some(existing) = snapshot.progress(&progress.progress_id) {
709 ensure_progress_matches(run_id, existing, &progress)?;
710 return Err(FlowError::InvalidTransition(format!(
711 "workflow rescheduled progress {} without progress",
712 progress.progress_id
713 )));
714 }
715 match self
716 .record_event_at(
717 run_id,
718 snapshot.last_sequence,
719 FlowEvent::RunProgressRecorded { progress },
720 )
721 .await
722 {
723 Ok(_) => {}
724 Err(err) if is_event_conflict(&err) => continue,
725 Err(err) => return Err(err),
726 }
727 }
728 RuntimeCommand::LinkChildOperation { child } => {
729 child.validate()?;
730 if let Some(existing) = snapshot.child_operation(&child.reference_id) {
731 ensure_child_operation_matches(run_id, existing, &child)?;
732 return Err(FlowError::InvalidTransition(format!(
733 "workflow rescheduled child operation {} without progress",
734 child.reference_id
735 )));
736 }
737 match self
738 .record_event_at(
739 run_id,
740 snapshot.last_sequence,
741 FlowEvent::ChildOperationLinked { child },
742 )
743 .await
744 {
745 Ok(_) => {}
746 Err(err) if is_event_conflict(&err) => continue,
747 Err(err) => return Err(err),
748 }
749 }
750 RuntimeCommand::ScheduleStep {
751 step_id,
752 step_name,
753 input,
754 retry,
755 } => {
756 if let Some(step) = snapshot.steps.get(&step_id) {
757 ensure_step_command_matches(run_id, step, &step_name, &input, retry)?;
758 if matches!(
759 step.status,
760 StepStatus::Completed | StepStatus::Failed | StepStatus::Cancelled
761 ) {
762 return Err(FlowError::InvalidTransition(format!(
763 "workflow rescheduled terminal step {step_id} without progress"
764 )));
765 }
766 }
767 match self
768 .execute_step(
769 run_id,
770 &snapshot,
771 StepExecutionContext {
772 step_id,
773 step_name,
774 input,
775 retry,
776 now,
777 },
778 )
779 .await
780 {
781 Ok(()) => {}
782 Err(err) if is_event_conflict(&err) => continue,
783 Err(err) => return Err(err),
784 }
785 }
786 RuntimeCommand::ScheduleSteps { steps } => {
787 ensure_step_batch_valid(&steps)?;
788 for step in &steps {
789 if let Some(existing) = snapshot.steps.get(&step.step_id) {
790 ensure_step_command_matches(
791 run_id,
792 existing,
793 &step.step_name,
794 &step.input,
795 step.retry,
796 )?;
797 }
798 }
799 if steps.iter().all(|step| {
800 snapshot.steps.get(&step.step_id).is_some_and(|existing| {
801 matches!(
802 existing.status,
803 StepStatus::Completed | StepStatus::Failed | StepStatus::Cancelled
804 )
805 })
806 }) {
807 let step_ids = steps
808 .iter()
809 .map(|step| step.step_id.as_str())
810 .collect::<Vec<_>>()
811 .join(", ");
812 return Err(FlowError::InvalidTransition(format!(
813 "workflow rescheduled only terminal steps without progress: {step_ids}"
814 )));
815 }
816 match self.execute_step_batch(run_id, &snapshot, steps, now).await {
817 Ok(()) => {}
818 Err(err) if is_event_conflict(&err) => continue 'replay,
819 Err(err) => return Err(err),
820 }
821 }
822 RuntimeCommand::WaitUntil { wait_id, resume_at } => {
823 match snapshot.waits.get(&wait_id) {
824 Some(wait) => {
825 ensure_wait_command_matches(run_id, wait, resume_at)?;
826 match wait.status {
827 WaitStatus::Completed => continue,
828 WaitStatus::Waiting => return self.snapshot(run_id).await,
829 WaitStatus::Cancelled => {
830 return Err(FlowError::InvalidTransition(format!(
831 "workflow rescheduled cancelled wait {wait_id}; cancellation cleanup must use a distinct stable identity"
832 )))
833 }
834 }
835 }
836 None => {
837 match self
838 .record_event_at(
839 run_id,
840 snapshot.last_sequence,
841 FlowEvent::WaitCreated { wait_id, resume_at },
842 )
843 .await
844 {
845 Ok(_) => {}
846 Err(err) if is_event_conflict(&err) => continue,
847 Err(err) => return Err(err),
848 }
849 return self.snapshot(run_id).await;
850 }
851 }
852 }
853 RuntimeCommand::CreateHook {
854 hook_id,
855 token,
856 metadata,
857 } => match snapshot.hooks.get(&hook_id) {
858 Some(hook) => {
859 ensure_hook_command_matches(run_id, hook, &token, &metadata)?;
860 match hook.status {
861 HookStatus::Received | HookStatus::Disposed => continue,
862 HookStatus::Active => return self.snapshot(run_id).await,
863 HookStatus::Cancelled => {
864 return Err(FlowError::InvalidTransition(format!(
865 "workflow rescheduled cancelled hook {hook_id}; cancellation cleanup must use a distinct stable identity"
866 )))
867 }
868 }
869 }
870 None => {
871 self.ensure_hook_token_available(run_id, &hook_id, &token)
872 .await?;
873 match self
874 .record_event_at(
875 run_id,
876 snapshot.last_sequence,
877 FlowEvent::HookCreated {
878 hook_id,
879 token,
880 metadata,
881 },
882 )
883 .await
884 {
885 Ok(_) => {}
886 Err(err) if is_event_conflict(&err) => continue,
887 Err(err) => return Err(err),
888 }
889 return self.snapshot(run_id).await;
890 }
891 },
892 }
893 }
894
895 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
896 }
897
898 async fn terminate_run(&self, run_id: &str, event: FlowEvent) -> Result<()> {
899 for _ in 0..self.max_replay_iterations {
900 let snapshot = self.snapshot(run_id).await?;
901 if snapshot.status.is_terminal() {
902 return Ok(());
903 }
904 match self
905 .record_event_at(run_id, snapshot.last_sequence, event.clone())
906 .await
907 {
908 Ok(_) => return Ok(()),
909 Err(err) if is_event_conflict(&err) => continue,
910 Err(err) => return Err(err),
911 }
912 }
913 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
914 }
915
916 async fn record_event_at(
917 &self,
918 run_id: &str,
919 expected_sequence: u64,
920 event: FlowEvent,
921 ) -> Result<FlowEventEnvelope> {
922 let envelope = self
923 .store
924 .append_if_sequence(run_id, expected_sequence, event)
925 .await?;
926 self.observer.observe(envelope.clone()).await;
927 Ok(envelope)
928 }
929
930 async fn ensure_hook_token_available(
931 &self,
932 run_id: &str,
933 hook_id: &str,
934 token: &str,
935 ) -> Result<()> {
936 for active in self.store.find_active_hooks_by_token(token).await? {
937 if active.run_id == run_id && active.hook.hook_id == hook_id {
938 continue;
939 }
940 return Err(FlowError::HookTokenConflict {
941 token: token.to_string(),
942 existing_run_id: active.run_id,
943 existing_hook_id: active.hook.hook_id,
944 });
945 }
946 Ok(())
947 }
948}
949
950fn resolve_scheduled_wakeup(
951 snapshot: &WorkflowRunSnapshot,
952 wakeup: &ScheduledWakeup,
953 now: DateTime<Utc>,
954) -> Option<WorkflowRunSuspension> {
955 if snapshot.run_id != wakeup.run_id || snapshot.status.is_terminal() {
956 return None;
957 }
958 match wakeup.kind {
959 ScheduledWakeupKind::Wait => {
960 let wait = snapshot.waits.get(&wakeup.subject_id)?;
961 if wait.status != WaitStatus::Waiting || wait.resume_at != wakeup.scheduled_at {
962 return None;
963 }
964 Some(WorkflowRunSuspension::Wait {
965 run_id: wakeup.run_id.clone(),
966 wait: wait.clone(),
967 due: wakeup.scheduled_at <= now,
968 })
969 }
970 ScheduledWakeupKind::Retry => {
971 let step = snapshot.steps.get(&wakeup.subject_id)?;
972 if step.status != StepStatus::Pending || step.retry_after != Some(wakeup.scheduled_at) {
973 return None;
974 }
975 Some(WorkflowRunSuspension::Retry {
976 run_id: wakeup.run_id.clone(),
977 step: step.clone(),
978 due: wakeup.scheduled_at <= now,
979 })
980 }
981 }
982}