Skip to main content

a3s_flow/store/
mod.rs

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/// Append-only event store for durable workflow runs.
46#[async_trait]
47pub trait FlowEventStore: Send + Sync {
48    /// Append `event` to `run_id` and return its durable envelope.
49    async fn append(&self, run_id: &str, event: FlowEvent) -> Result<FlowEventEnvelope>;
50
51    /// Append `event` only when the run's latest sequence equals
52    /// `expected_sequence`.
53    ///
54    /// Implementations return [`crate::FlowError::EventConflict`] when another
55    /// writer has advanced the run.
56    async fn append_if_sequence(
57        &self,
58        run_id: &str,
59        expected_sequence: u64,
60        event: FlowEvent,
61    ) -> Result<FlowEventEnvelope>;
62
63    /// Load the complete event history for `run_id` in sequence order.
64    async fn list(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>>;
65
66    /// List all run IDs known to the store in stable order.
67    async fn list_run_ids(&self) -> Result<Vec<String>>;
68
69    /// List wait timers and delayed retries due at or before `now`.
70    ///
71    /// The default implementation replays every run for compatibility with
72    /// custom stores. SQL stores override it with an indexed projection.
73    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    /// Return the earliest wait timer or delayed retry across active runs.
87    ///
88    /// Active hooks are excluded because they do not have a scheduled time.
89    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    /// Find active hooks that own an external callback token.
110    ///
111    /// The default implementation replays every run for compatibility with
112    /// custom stores. SQL stores override it with their indexed projection.
113    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    /// List active external callback hooks in stable run/hook order.
123    ///
124    /// The default implementation preserves the append-only store contract by
125    /// projecting histories. Durable SQL adapters provide a materialized path.
126    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}