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