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
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/// Append-only event store for durable workflow runs.
38#[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    /// List wait timers and delayed retries due at or before `now`.
54    ///
55    /// The default implementation replays every run for compatibility with
56    /// custom stores. SQL stores override it with an indexed projection.
57    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    /// Return the earliest wait timer or delayed retry across active runs.
71    ///
72    /// Active hooks are excluded because they do not have a scheduled time.
73    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    /// Find active hooks that own an external callback token.
94    ///
95    /// The default implementation replays every run for compatibility with
96    /// custom stores. SQL stores override it with their indexed projection.
97    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    /// List active external callback hooks in stable run/hook order.
107    ///
108    /// The default implementation preserves the append-only store contract by
109    /// projecting histories. Durable SQL adapters provide a materialized path.
110    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}