a3s_flow/engine/
inspection.rs1use chrono::{DateTime, Utc};
2
3use crate::error::{FlowError, Result};
4use crate::model::{
5 project_run, ActiveHookSnapshot, HookStatus, ScheduledWakeup, ScheduledWakeupKind, StepStatus,
6 WaitStatus, WorkflowRunSnapshot, WorkflowRunSummary, WorkflowRunSuspension,
7};
8
9use super::FlowEngine;
10
11impl FlowEngine {
12 pub async fn snapshot(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
14 let history = self.store.list(run_id).await?;
15 project_run(run_id, &history)
16 }
17
18 pub async fn history(&self, run_id: &str) -> Result<Vec<crate::model::FlowEventEnvelope>> {
20 self.store.list(run_id).await
21 }
22
23 pub async fn list_run_ids(&self) -> Result<Vec<String>> {
25 self.store.list_run_ids().await
26 }
27
28 pub async fn list_snapshots(&self) -> Result<Vec<WorkflowRunSnapshot>> {
30 let mut snapshots = Vec::new();
31 for run_id in self.store.list_run_ids().await? {
32 snapshots.push(self.snapshot(&run_id).await?);
33 }
34 Ok(snapshots)
35 }
36
37 pub async fn run_summary(&self) -> Result<WorkflowRunSummary> {
42 let snapshots = self.list_snapshots().await?;
43 Ok(WorkflowRunSummary::from_snapshots(&snapshots))
44 }
45
46 pub async fn list_open_suspensions(
52 &self,
53 now: DateTime<Utc>,
54 ) -> Result<Vec<WorkflowRunSuspension>> {
55 let mut suspensions = Vec::new();
56 for run_id in self.store.list_run_ids().await? {
57 let snapshot = self.snapshot(&run_id).await?;
58 if snapshot.status.is_terminal() {
59 continue;
60 }
61 for wait in snapshot.waits.values() {
62 if wait.status == WaitStatus::Waiting {
63 suspensions.push(WorkflowRunSuspension::Wait {
64 run_id: run_id.clone(),
65 wait: wait.clone(),
66 due: wait.resume_at <= now,
67 });
68 }
69 }
70 for hook in snapshot.hooks.values() {
71 if hook.status == HookStatus::Active {
72 suspensions.push(WorkflowRunSuspension::Hook {
73 run_id: run_id.clone(),
74 hook: hook.clone(),
75 });
76 }
77 }
78 for step in snapshot.steps.values() {
79 if step.status == StepStatus::Pending {
80 if let Some(retry_after) = step.retry_after {
81 suspensions.push(WorkflowRunSuspension::Retry {
82 run_id: run_id.clone(),
83 step: step.clone(),
84 due: retry_after <= now,
85 });
86 }
87 }
88 }
89 for child in snapshot.child_workflows.values() {
90 if child.is_open() {
91 suspensions.push(WorkflowRunSuspension::ChildWorkflow {
92 run_id: run_id.clone(),
93 child: child.clone(),
94 });
95 }
96 }
97 for wait in snapshot.signal_waits.values() {
98 if wait.status == crate::model::SignalWaitStatus::Waiting {
99 suspensions.push(WorkflowRunSuspension::Signal {
100 run_id: run_id.clone(),
101 wait: wait.clone(),
102 });
103 }
104 }
105 }
106 suspensions.sort_by(|left, right| {
107 (left.run_id(), left.kind_order(), left.subject_id()).cmp(&(
108 right.run_id(),
109 right.kind_order(),
110 right.subject_id(),
111 ))
112 });
113 Ok(suspensions)
114 }
115
116 pub async fn next_wakeup(&self, now: DateTime<Utc>) -> Result<Option<WorkflowRunSuspension>> {
121 for _ in 0..2 {
122 let Some(wakeup) = self.store.next_scheduled_wakeup().await? else {
123 return Ok(None);
124 };
125 match self.snapshot(&wakeup.run_id).await {
126 Ok(snapshot) => {
127 if let Some(suspension) = resolve_scheduled_wakeup(&snapshot, &wakeup, now) {
128 return Ok(Some(suspension));
129 }
130 }
131 Err(FlowError::RunNotFound(_)) => {}
132 Err(error) => return Err(error),
133 }
134 }
135
136 self.next_wakeup_by_replay(now).await
137 }
138
139 async fn next_wakeup_by_replay(
140 &self,
141 now: DateTime<Utc>,
142 ) -> Result<Option<WorkflowRunSuspension>> {
143 let mut wakeups = self.list_open_suspensions(now).await?;
144 wakeups.retain(|suspension| suspension.scheduled_at().is_some());
145 wakeups.sort_by(|left, right| {
146 (
147 left.scheduled_at(),
148 left.run_id(),
149 left.kind_order(),
150 left.subject_id(),
151 )
152 .cmp(&(
153 right.scheduled_at(),
154 right.run_id(),
155 right.kind_order(),
156 right.subject_id(),
157 ))
158 });
159 Ok(wakeups.into_iter().next())
160 }
161
162 pub async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
164 self.store.list_active_hooks().await
165 }
166}
167
168fn resolve_scheduled_wakeup(
169 snapshot: &WorkflowRunSnapshot,
170 wakeup: &ScheduledWakeup,
171 now: DateTime<Utc>,
172) -> Option<WorkflowRunSuspension> {
173 if snapshot.run_id != wakeup.run_id || snapshot.status.is_terminal() {
174 return None;
175 }
176 match wakeup.kind {
177 ScheduledWakeupKind::Wait => {
178 let wait = snapshot.waits.get(&wakeup.subject_id)?;
179 if wait.status != WaitStatus::Waiting || wait.resume_at != wakeup.scheduled_at {
180 return None;
181 }
182 Some(WorkflowRunSuspension::Wait {
183 run_id: wakeup.run_id.clone(),
184 wait: wait.clone(),
185 due: wakeup.scheduled_at <= now,
186 })
187 }
188 ScheduledWakeupKind::Retry => {
189 let step = snapshot.steps.get(&wakeup.subject_id)?;
190 if step.status != StepStatus::Pending || step.retry_after != Some(wakeup.scheduled_at) {
191 return None;
192 }
193 Some(WorkflowRunSuspension::Retry {
194 run_id: wakeup.run_id.clone(),
195 step: step.clone(),
196 due: wakeup.scheduled_at <= now,
197 })
198 }
199 }
200}