1use async_trait::async_trait;
2#[cfg(any(feature = "postgres", feature = "sqlite"))]
3use chrono::SecondsFormat;
4use chrono::{DateTime, Utc};
5
6use crate::error::Result;
7use crate::model::{
8 project_run, ActiveHookSnapshot, FlowEvent, FlowEventEnvelope, HookStatus, ScheduledWakeup,
9 ScheduledWakeupKind, StepStatus, WaitStatus, WorkflowRunSnapshot,
10};
11#[cfg(any(feature = "postgres", feature = "sqlite"))]
12use crate::runtime_build::RuntimeBuildId;
13
14mod local_file;
15mod memory;
16#[cfg(any(feature = "postgres", feature = "sqlite"))]
17mod migrations;
18#[cfg(feature = "postgres")]
19mod postgres;
20#[cfg(feature = "postgres")]
21mod postgres_schema;
22mod retention;
23#[cfg(feature = "sqlite")]
24mod sqlite;
25
26pub use local_file::LocalFileEventStore;
27pub use memory::InMemoryEventStore;
28#[cfg(feature = "postgres")]
29pub(crate) use migrations::postgres_migrations;
30#[cfg(feature = "sqlite")]
31pub(crate) use migrations::sqlite_migrations;
32#[cfg(feature = "postgres")]
33pub use postgres::PostgresEventStore;
34#[cfg(feature = "postgres")]
35pub use postgres_schema::migrate_postgres_flow;
36#[cfg(feature = "postgres")]
37pub(crate) use postgres_schema::verify_postgres_flow;
38#[cfg(any(feature = "postgres", feature = "sqlite"))]
39pub use retention::{
40 FlowHistoryHold, FlowHistoryRetentionPolicy, FlowHistoryRetentionReport, FlowHistoryTombstone,
41};
42#[cfg(feature = "sqlite")]
43pub use sqlite::SqliteEventStore;
44
45#[async_trait]
47pub trait FlowEventStore: Send + Sync {
48 async fn append(&self, run_id: &str, event: FlowEvent) -> Result<FlowEventEnvelope>;
50
51 async fn append_if_sequence(
57 &self,
58 run_id: &str,
59 expected_sequence: u64,
60 event: FlowEvent,
61 ) -> Result<FlowEventEnvelope>;
62
63 async fn list(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>>;
65
66 async fn list_run_ids(&self) -> Result<Vec<String>>;
68
69 async fn list_due_wakeups(&self, now: DateTime<Utc>) -> Result<Vec<ScheduledWakeup>> {
74 let mut wakeups = replay_scheduled_wakeups(self).await?;
75 wakeups.retain(|wakeup| wakeup.scheduled_at <= now);
76 wakeups.sort_by(|left, right| {
77 (left.kind, left.run_id.as_str(), left.subject_id.as_str()).cmp(&(
78 right.kind,
79 right.run_id.as_str(),
80 right.subject_id.as_str(),
81 ))
82 });
83 Ok(wakeups)
84 }
85
86 async fn next_scheduled_wakeup(&self) -> Result<Option<ScheduledWakeup>> {
90 Ok(replay_scheduled_wakeups(self)
91 .await?
92 .into_iter()
93 .min_by(|left, right| {
94 (
95 left.scheduled_at,
96 left.run_id.as_str(),
97 left.kind,
98 left.subject_id.as_str(),
99 )
100 .cmp(&(
101 right.scheduled_at,
102 right.run_id.as_str(),
103 right.kind,
104 right.subject_id.as_str(),
105 ))
106 }))
107 }
108
109 async fn find_active_hooks_by_token(&self, token: &str) -> Result<Vec<ActiveHookSnapshot>> {
114 Ok(self
115 .list_active_hooks()
116 .await?
117 .into_iter()
118 .filter(|active| active.hook.token == token)
119 .collect())
120 }
121
122 async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
127 let mut hooks = Vec::new();
128 for run_id in self.list_run_ids().await? {
129 let history = self.list(&run_id).await?;
130 let snapshot = project_run(&run_id, &history)?;
131 if snapshot.status.is_terminal() {
132 continue;
133 }
134 for hook in snapshot.hooks.values() {
135 if hook.status == HookStatus::Active {
136 hooks.push(ActiveHookSnapshot {
137 run_id: run_id.clone(),
138 hook: hook.clone(),
139 });
140 }
141 }
142 }
143 hooks.sort_by(|left, right| {
144 (left.run_id.as_str(), left.hook.hook_id.as_str())
145 .cmp(&(right.run_id.as_str(), right.hook.hook_id.as_str()))
146 });
147 Ok(hooks)
148 }
149}
150
151async fn replay_scheduled_wakeups<S>(store: &S) -> Result<Vec<ScheduledWakeup>>
152where
153 S: FlowEventStore + ?Sized,
154{
155 let mut wakeups = Vec::new();
156 for run_id in store.list_run_ids().await? {
157 let history = store.list(&run_id).await?;
158 let snapshot = project_run(&run_id, &history)?;
159 wakeups.extend(scheduled_wakeups_for_snapshot(&snapshot));
160 }
161 Ok(wakeups)
162}
163
164pub(crate) fn scheduled_wakeups_for_snapshot(
165 snapshot: &WorkflowRunSnapshot,
166) -> Vec<ScheduledWakeup> {
167 if snapshot.status.is_terminal() {
168 return Vec::new();
169 }
170
171 let mut wakeups = Vec::new();
172 for wait in snapshot.waits.values() {
173 if wait.status == WaitStatus::Waiting {
174 wakeups.push(ScheduledWakeup {
175 run_id: snapshot.run_id.clone(),
176 kind: ScheduledWakeupKind::Wait,
177 subject_id: wait.wait_id.clone(),
178 scheduled_at: wait.resume_at,
179 runtime_build_id: snapshot.spec.runtime_build_id.clone(),
180 });
181 }
182 }
183 for step in snapshot.steps.values() {
184 if step.status == StepStatus::Pending {
185 if let Some(retry_after) = step.retry_after {
186 wakeups.push(ScheduledWakeup {
187 run_id: snapshot.run_id.clone(),
188 kind: ScheduledWakeupKind::Retry,
189 subject_id: step.step_id.clone(),
190 scheduled_at: retry_after,
191 runtime_build_id: snapshot.spec.runtime_build_id.clone(),
192 });
193 }
194 }
195 }
196 wakeups
197}
198
199#[cfg(any(feature = "postgres", feature = "sqlite"))]
200pub(super) fn scheduled_wakeup_key(timestamp: DateTime<Utc>) -> String {
201 timestamp.to_rfc3339_opts(SecondsFormat::Nanos, true)
202}
203
204#[cfg(any(feature = "postgres", feature = "sqlite"))]
205pub(super) fn scheduled_wakeup_from_row(
206 (run_id, wakeup_kind, subject_id, scheduled_at_key, runtime_build_id): (
207 String,
208 i64,
209 String,
210 String,
211 Option<String>,
212 ),
213) -> Result<ScheduledWakeup> {
214 let scheduled_at = DateTime::parse_from_rfc3339(&scheduled_at_key)
215 .map_err(|error| {
216 crate::error::FlowError::Store(format!(
217 "invalid scheduled wakeup timestamp {scheduled_at_key:?}: {error}"
218 ))
219 })?
220 .with_timezone(&Utc);
221 let runtime_build_id = runtime_build_id
222 .map(RuntimeBuildId::new)
223 .transpose()
224 .map_err(|error| {
225 crate::error::FlowError::Store(format!(
226 "invalid runtime build identity for scheduled wakeup {run_id}: {error}"
227 ))
228 })?;
229 Ok(ScheduledWakeup {
230 run_id,
231 kind: ScheduledWakeupKind::from_database_code(wakeup_kind)?,
232 subject_id,
233 scheduled_at,
234 runtime_build_id,
235 })
236}