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 StepStatus, WaitStatus, WorkflowRunSnapshot, WorkflowRunStatus, WorkflowRunSummary,
9 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 = Vec::new();
393 for run_id in self.store.list_run_ids().await? {
394 let snapshot = self.snapshot(&run_id).await?;
395 if snapshot.status.is_terminal() {
396 continue;
397 }
398 for wait in snapshot.waits.values() {
399 if wait.status == WaitStatus::Waiting && wait.resume_at <= now {
400 due.push((run_id.clone(), wait.wait_id.clone()));
401 }
402 }
403 }
404 due.sort();
405 Ok(due)
406 }
407
408 pub async fn resume_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
413 let due = self.list_due_waits(now).await?;
414 let mut resumed = Vec::with_capacity(due.len());
415 for (run_id, wait_id) in due {
416 self.resume_wait(&run_id, &wait_id).await?;
417 resumed.push((run_id, wait_id));
418 }
419 Ok(resumed)
420 }
421
422 pub async fn list_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
424 let mut due = Vec::new();
425 for run_id in self.store.list_run_ids().await? {
426 let snapshot = self.snapshot(&run_id).await?;
427 if snapshot.status.is_terminal() {
428 continue;
429 }
430 for (step_id, _) in snapshot.due_retries(now) {
431 due.push((run_id.clone(), step_id));
432 }
433 }
434 due.sort();
435 Ok(due)
436 }
437
438 pub async fn resume_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
440 let due = self.list_due_retries(now).await?;
441 let mut run_ids = Vec::new();
442 for (run_id, _) in &due {
443 if !run_ids.contains(run_id) {
444 run_ids.push(run_id.clone());
445 }
446 }
447 for run_id in run_ids {
448 self.drive_at(&run_id, now).await?;
449 }
450 Ok(due)
451 }
452
453 pub async fn snapshot(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
454 let history = self.store.list(run_id).await?;
455 project_run(run_id, &history)
456 }
457
458 pub async fn history(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
459 self.store.list(run_id).await
460 }
461
462 pub async fn list_run_ids(&self) -> Result<Vec<String>> {
463 self.store.list_run_ids().await
464 }
465
466 pub async fn list_snapshots(&self) -> Result<Vec<WorkflowRunSnapshot>> {
467 let mut snapshots = Vec::new();
468 for run_id in self.store.list_run_ids().await? {
469 snapshots.push(self.snapshot(&run_id).await?);
470 }
471 Ok(snapshots)
472 }
473
474 pub async fn run_summary(&self) -> Result<WorkflowRunSummary> {
480 let snapshots = self.list_snapshots().await?;
481 Ok(WorkflowRunSummary::from_snapshots(&snapshots))
482 }
483
484 pub async fn list_open_suspensions(
490 &self,
491 now: DateTime<Utc>,
492 ) -> Result<Vec<WorkflowRunSuspension>> {
493 let mut suspensions = Vec::new();
494 for run_id in self.store.list_run_ids().await? {
495 let snapshot = self.snapshot(&run_id).await?;
496 if snapshot.status.is_terminal() {
497 continue;
498 }
499 for wait in snapshot.waits.values() {
500 if wait.status == WaitStatus::Waiting {
501 suspensions.push(WorkflowRunSuspension::Wait {
502 run_id: run_id.clone(),
503 wait: wait.clone(),
504 due: wait.resume_at <= now,
505 });
506 }
507 }
508 for hook in snapshot.hooks.values() {
509 if hook.status == HookStatus::Active {
510 suspensions.push(WorkflowRunSuspension::Hook {
511 run_id: run_id.clone(),
512 hook: hook.clone(),
513 });
514 }
515 }
516 for step in snapshot.steps.values() {
517 if step.status == StepStatus::Pending {
518 if let Some(retry_after) = step.retry_after {
519 suspensions.push(WorkflowRunSuspension::Retry {
520 run_id: run_id.clone(),
521 step: step.clone(),
522 due: retry_after <= now,
523 });
524 }
525 }
526 }
527 }
528 suspensions.sort_by(|left, right| {
529 (left.run_id(), left.kind_order(), left.subject_id()).cmp(&(
530 right.run_id(),
531 right.kind_order(),
532 right.subject_id(),
533 ))
534 });
535 Ok(suspensions)
536 }
537
538 pub async fn next_wakeup(&self, now: DateTime<Utc>) -> Result<Option<WorkflowRunSuspension>> {
544 let mut wakeups = self.list_open_suspensions(now).await?;
545 wakeups.retain(|suspension| suspension.scheduled_at().is_some());
546 wakeups.sort_by(|left, right| {
547 (
548 left.scheduled_at(),
549 left.run_id(),
550 left.kind_order(),
551 left.subject_id(),
552 )
553 .cmp(&(
554 right.scheduled_at(),
555 right.run_id(),
556 right.kind_order(),
557 right.subject_id(),
558 ))
559 });
560 Ok(wakeups.into_iter().next())
561 }
562
563 pub async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
569 self.store.list_active_hooks().await
570 }
571
572 pub async fn drive(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
575 self.drive_at(run_id, Utc::now()).await
576 }
577
578 async fn drive_at(&self, run_id: &str, now: DateTime<Utc>) -> Result<WorkflowRunSnapshot> {
579 'replay: for _ in 0..self.max_replay_iterations {
580 let history = self.store.list(run_id).await?;
581 let snapshot = project_run(run_id, &history)?;
582 if snapshot.status.is_terminal()
583 || snapshot
584 .waits
585 .values()
586 .any(|wait| wait.status == WaitStatus::Waiting)
587 || snapshot
588 .hooks
589 .values()
590 .any(|hook| hook.status == HookStatus::Active)
591 || (snapshot.has_future_retry(now) && snapshot.due_retries(now).is_empty())
592 {
593 return Ok(snapshot);
594 }
595
596 let command = self
597 .runtime
598 .run_workflow(WorkflowInvocation {
599 run_id: run_id.to_string(),
600 spec: snapshot.spec.clone(),
601 input: snapshot.input.clone(),
602 history,
603 })
604 .await?;
605
606 match command {
607 RuntimeCommand::Complete { output } => {
608 if snapshot.status == WorkflowRunStatus::Cancelling {
609 return Err(FlowError::InvalidTransition(format!(
610 "workflow run {run_id} completed after cancellation was requested; cleanup-aware cancellation must return cancel or fail"
611 )));
612 }
613 match self
614 .record_event_at(
615 run_id,
616 snapshot.last_sequence,
617 FlowEvent::RunCompleted { output },
618 )
619 .await
620 {
621 Ok(_) => {}
622 Err(err) if is_event_conflict(&err) => continue,
623 Err(err) => return Err(err),
624 }
625 return self.snapshot(run_id).await;
626 }
627 RuntimeCommand::Fail { error } => {
628 match self
629 .record_event_at(
630 run_id,
631 snapshot.last_sequence,
632 FlowEvent::RunFailed { error },
633 )
634 .await
635 {
636 Ok(_) => {}
637 Err(err) if is_event_conflict(&err) => continue,
638 Err(err) => return Err(err),
639 }
640 return self.snapshot(run_id).await;
641 }
642 RuntimeCommand::Cancel => {
643 let cancellation = snapshot.cancellation.as_ref().ok_or_else(|| {
644 FlowError::InvalidTransition(format!(
645 "workflow run {run_id} returned cancel without a durable cancellation request"
646 ))
647 })?;
648 match self
649 .record_event_at(
650 run_id,
651 snapshot.last_sequence,
652 FlowEvent::RunCancelled {
653 reason: cancellation.request.reason.clone(),
654 },
655 )
656 .await
657 {
658 Ok(_) => {}
659 Err(err) if is_event_conflict(&err) => continue,
660 Err(err) => return Err(err),
661 }
662 return self.snapshot(run_id).await;
663 }
664 RuntimeCommand::Timeout { deadline, reason } => {
665 match self
666 .record_event_at(
667 run_id,
668 snapshot.last_sequence,
669 FlowEvent::RunTimedOut { deadline, reason },
670 )
671 .await
672 {
673 Ok(_) => {}
674 Err(err) if is_event_conflict(&err) => continue,
675 Err(err) => return Err(err),
676 }
677 return self.snapshot(run_id).await;
678 }
679 RuntimeCommand::RecordProgress { progress } => {
680 progress.validate()?;
681 if let Some(existing) = snapshot.progress(&progress.progress_id) {
682 ensure_progress_matches(run_id, existing, &progress)?;
683 return Err(FlowError::InvalidTransition(format!(
684 "workflow rescheduled progress {} without progress",
685 progress.progress_id
686 )));
687 }
688 match self
689 .record_event_at(
690 run_id,
691 snapshot.last_sequence,
692 FlowEvent::RunProgressRecorded { progress },
693 )
694 .await
695 {
696 Ok(_) => {}
697 Err(err) if is_event_conflict(&err) => continue,
698 Err(err) => return Err(err),
699 }
700 }
701 RuntimeCommand::LinkChildOperation { child } => {
702 child.validate()?;
703 if let Some(existing) = snapshot.child_operation(&child.reference_id) {
704 ensure_child_operation_matches(run_id, existing, &child)?;
705 return Err(FlowError::InvalidTransition(format!(
706 "workflow rescheduled child operation {} without progress",
707 child.reference_id
708 )));
709 }
710 match self
711 .record_event_at(
712 run_id,
713 snapshot.last_sequence,
714 FlowEvent::ChildOperationLinked { child },
715 )
716 .await
717 {
718 Ok(_) => {}
719 Err(err) if is_event_conflict(&err) => continue,
720 Err(err) => return Err(err),
721 }
722 }
723 RuntimeCommand::ScheduleStep {
724 step_id,
725 step_name,
726 input,
727 retry,
728 } => {
729 if let Some(step) = snapshot.steps.get(&step_id) {
730 ensure_step_command_matches(run_id, step, &step_name, &input, retry)?;
731 if matches!(
732 step.status,
733 StepStatus::Completed | StepStatus::Failed | StepStatus::Cancelled
734 ) {
735 return Err(FlowError::InvalidTransition(format!(
736 "workflow rescheduled terminal step {step_id} without progress"
737 )));
738 }
739 }
740 match self
741 .execute_step(
742 run_id,
743 &snapshot,
744 StepExecutionContext {
745 step_id,
746 step_name,
747 input,
748 retry,
749 now,
750 },
751 )
752 .await
753 {
754 Ok(()) => {}
755 Err(err) if is_event_conflict(&err) => continue,
756 Err(err) => return Err(err),
757 }
758 }
759 RuntimeCommand::ScheduleSteps { steps } => {
760 ensure_step_batch_valid(&steps)?;
761 for step in &steps {
762 if let Some(existing) = snapshot.steps.get(&step.step_id) {
763 ensure_step_command_matches(
764 run_id,
765 existing,
766 &step.step_name,
767 &step.input,
768 step.retry,
769 )?;
770 }
771 }
772 if steps.iter().all(|step| {
773 snapshot.steps.get(&step.step_id).is_some_and(|existing| {
774 matches!(
775 existing.status,
776 StepStatus::Completed | StepStatus::Failed | StepStatus::Cancelled
777 )
778 })
779 }) {
780 let step_ids = steps
781 .iter()
782 .map(|step| step.step_id.as_str())
783 .collect::<Vec<_>>()
784 .join(", ");
785 return Err(FlowError::InvalidTransition(format!(
786 "workflow rescheduled only terminal steps without progress: {step_ids}"
787 )));
788 }
789 match self.execute_step_batch(run_id, &snapshot, steps, now).await {
790 Ok(()) => {}
791 Err(err) if is_event_conflict(&err) => continue 'replay,
792 Err(err) => return Err(err),
793 }
794 }
795 RuntimeCommand::WaitUntil { wait_id, resume_at } => {
796 match snapshot.waits.get(&wait_id) {
797 Some(wait) => {
798 ensure_wait_command_matches(run_id, wait, resume_at)?;
799 match wait.status {
800 WaitStatus::Completed => continue,
801 WaitStatus::Waiting => return self.snapshot(run_id).await,
802 WaitStatus::Cancelled => {
803 return Err(FlowError::InvalidTransition(format!(
804 "workflow rescheduled cancelled wait {wait_id}; cancellation cleanup must use a distinct stable identity"
805 )))
806 }
807 }
808 }
809 None => {
810 match self
811 .record_event_at(
812 run_id,
813 snapshot.last_sequence,
814 FlowEvent::WaitCreated { wait_id, resume_at },
815 )
816 .await
817 {
818 Ok(_) => {}
819 Err(err) if is_event_conflict(&err) => continue,
820 Err(err) => return Err(err),
821 }
822 return self.snapshot(run_id).await;
823 }
824 }
825 }
826 RuntimeCommand::CreateHook {
827 hook_id,
828 token,
829 metadata,
830 } => match snapshot.hooks.get(&hook_id) {
831 Some(hook) => {
832 ensure_hook_command_matches(run_id, hook, &token, &metadata)?;
833 match hook.status {
834 HookStatus::Received | HookStatus::Disposed => continue,
835 HookStatus::Active => return self.snapshot(run_id).await,
836 HookStatus::Cancelled => {
837 return Err(FlowError::InvalidTransition(format!(
838 "workflow rescheduled cancelled hook {hook_id}; cancellation cleanup must use a distinct stable identity"
839 )))
840 }
841 }
842 }
843 None => {
844 self.ensure_hook_token_available(run_id, &hook_id, &token)
845 .await?;
846 match self
847 .record_event_at(
848 run_id,
849 snapshot.last_sequence,
850 FlowEvent::HookCreated {
851 hook_id,
852 token,
853 metadata,
854 },
855 )
856 .await
857 {
858 Ok(_) => {}
859 Err(err) if is_event_conflict(&err) => continue,
860 Err(err) => return Err(err),
861 }
862 return self.snapshot(run_id).await;
863 }
864 },
865 }
866 }
867
868 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
869 }
870
871 async fn terminate_run(&self, run_id: &str, event: FlowEvent) -> Result<()> {
872 for _ in 0..self.max_replay_iterations {
873 let snapshot = self.snapshot(run_id).await?;
874 if snapshot.status.is_terminal() {
875 return Ok(());
876 }
877 match self
878 .record_event_at(run_id, snapshot.last_sequence, event.clone())
879 .await
880 {
881 Ok(_) => return Ok(()),
882 Err(err) if is_event_conflict(&err) => continue,
883 Err(err) => return Err(err),
884 }
885 }
886 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
887 }
888
889 async fn record_event_at(
890 &self,
891 run_id: &str,
892 expected_sequence: u64,
893 event: FlowEvent,
894 ) -> Result<FlowEventEnvelope> {
895 let envelope = self
896 .store
897 .append_if_sequence(run_id, expected_sequence, event)
898 .await?;
899 self.observer.observe(envelope.clone()).await;
900 Ok(envelope)
901 }
902
903 async fn ensure_hook_token_available(
904 &self,
905 run_id: &str,
906 hook_id: &str,
907 token: &str,
908 ) -> Result<()> {
909 for active in self.store.find_active_hooks_by_token(token).await? {
910 if active.run_id == run_id && active.hook.hook_id == hook_id {
911 continue;
912 }
913 return Err(FlowError::HookTokenConflict {
914 token: token.to_string(),
915 existing_run_id: active.run_id,
916 existing_hook_id: active.hook.hook_id,
917 });
918 }
919 Ok(())
920 }
921}