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