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