1use std::fmt;
2
3use a3s_orm::{
4 sql_query, Database, Executor, FromRow, PostgresDialect, PostgresError, PostgresExecutor,
5 PostgresRow, PostgresTransaction, PostgresTransactionError, Query, SqlQuery,
6};
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use uuid::Uuid;
10
11use crate::error::{FlowError, Result};
12use crate::model::{
13 ActiveHookSnapshot, FlowEvent, FlowEventEnvelope, HookSnapshot, HookStatus, ScheduledWakeup,
14};
15
16use super::{
17 migrate_postgres_flow, scheduled_wakeup_from_row, scheduled_wakeup_key, verify_postgres_flow,
18 FlowEventStore,
19};
20
21mod retention;
22
23#[derive(Clone)]
30pub struct PostgresEventStore {
31 executor: PostgresExecutor,
32}
33
34impl fmt::Debug for PostgresEventStore {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter
37 .debug_struct("PostgresEventStore")
38 .finish_non_exhaustive()
39 }
40}
41
42impl PostgresEventStore {
43 pub async fn connect(database_url: impl AsRef<str>) -> Result<Self> {
48 let executor = PostgresExecutor::connect_no_tls(database_url.as_ref(), 5)
49 .map_err(postgres_driver_error)?;
50 Self::from_executor(executor).await
51 }
52
53 pub async fn connect_verified(database_url: impl AsRef<str>) -> Result<Self> {
56 let executor = PostgresExecutor::connect_no_tls(database_url.as_ref(), 5)
57 .map_err(postgres_driver_error)?;
58 Self::from_executor_verified(executor).await
59 }
60
61 pub async fn from_executor(executor: PostgresExecutor) -> Result<Self> {
63 migrate_postgres_flow(&executor).await?;
64 Ok(Self { executor })
65 }
66
67 pub async fn from_executor_verified(executor: PostgresExecutor) -> Result<Self> {
70 verify_postgres_flow(&executor).await?;
71 Ok(Self { executor })
72 }
73
74 pub fn executor(&self) -> &PostgresExecutor {
76 &self.executor
77 }
78
79 async fn append_with_expected_sequence(
80 &self,
81 run_id: &str,
82 expected_sequence: Option<u64>,
83 event: FlowEvent,
84 ) -> Result<FlowEventEnvelope> {
85 let run_id = run_id.to_string();
86 let result = self
87 .executor
88 .transaction(|transaction| {
89 Box::pin(async move {
90 retention::lock_postgres_retention_guard_shared(transaction).await?;
91 let linked_run_id =
92 retention::required_linked_flow_run_id(&event).map(str::to_string);
93 let mut locked_run_ids = vec![run_id.as_str()];
94 if let Some(linked_run_id) = linked_run_id.as_deref() {
95 locked_run_ids.push(linked_run_id);
96 }
97 locked_run_ids.sort_unstable();
98 locked_run_ids.dedup();
99 for locked_run_id in locked_run_ids {
100 lock_postgres_run(transaction, locked_run_id).await?;
101 }
102 retention::ensure_postgres_history_not_tombstoned(transaction, &run_id).await?;
103 if let Some(linked_run_id) = linked_run_id.as_deref() {
104 retention::ensure_postgres_history_not_tombstoned(
105 transaction,
106 linked_run_id,
107 )
108 .await?;
109 if latest_postgres_sequence(transaction, linked_run_id).await? == 0 {
110 return Err(FlowError::RunNotFound(linked_run_id.to_string()));
111 }
112 }
113 let actual_sequence = latest_postgres_sequence(transaction, &run_id).await?;
114 if let Some(expected_sequence) = expected_sequence {
115 if actual_sequence != expected_sequence {
116 return Err(FlowError::EventConflict {
117 run_id,
118 expected_sequence,
119 actual_sequence,
120 });
121 }
122 }
123 if let FlowEvent::HookCreated { hook_id, token, .. } = &event {
124 ensure_postgres_active_hook_available(transaction, &run_id, hook_id, token)
125 .await?;
126 }
127
128 let envelope = FlowEventEnvelope {
129 run_id,
130 sequence: actual_sequence + 1,
131 event_id: Uuid::new_v4(),
132 timestamp: Utc::now(),
133 event,
134 };
135 insert_postgres_envelope(transaction, &envelope).await?;
136 Ok(envelope)
137 })
138 })
139 .await;
140 map_postgres_transaction(result)
141 }
142}
143
144#[async_trait]
145impl FlowEventStore for PostgresEventStore {
146 async fn append(&self, run_id: &str, event: FlowEvent) -> Result<FlowEventEnvelope> {
147 self.append_with_expected_sequence(run_id, None, event)
148 .await
149 }
150
151 async fn append_if_sequence(
152 &self,
153 run_id: &str,
154 expected_sequence: u64,
155 event: FlowEvent,
156 ) -> Result<FlowEventEnvelope> {
157 self.append_with_expected_sequence(run_id, Some(expected_sequence), event)
158 .await
159 }
160
161 async fn list(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
162 let database = Database::new(PostgresDialect, self.executor.clone());
163 let rows = database
164 .fetch_all_as(
165 sql_query::<(String, i64, String, String, String)>(
166 "SELECT run_id, sequence, event_id, timestamp, event_json \
167 FROM flow_events WHERE run_id = ",
168 )
169 .bind(run_id)
170 .append(" ORDER BY sequence ASC"),
171 )
172 .await
173 .map_err(postgres_orm_error)?
174 .rows;
175 if rows.is_empty() {
176 return Err(FlowError::RunNotFound(run_id.to_string()));
177 }
178 rows.into_iter().map(row_to_envelope).collect()
179 }
180
181 async fn list_run_ids(&self) -> Result<Vec<String>> {
182 let database = Database::new(PostgresDialect, self.executor.clone());
183 Ok(database
184 .fetch_all_as(sql_query::<String>(
185 "SELECT DISTINCT run_id FROM flow_events ORDER BY run_id ASC",
186 ))
187 .await
188 .map_err(postgres_orm_error)?
189 .rows)
190 }
191
192 async fn list_due_wakeups(&self, now: DateTime<Utc>) -> Result<Vec<ScheduledWakeup>> {
193 let database = Database::new(PostgresDialect, self.executor.clone());
194 database
195 .fetch_all_as(
196 sql_query::<(String, i64, String, String, Option<String>)>(
197 "SELECT wakeup.run_id, wakeup.wakeup_kind, wakeup.subject_id, \
198 wakeup.scheduled_at_key, \
199 created.event_json::jsonb -> 'spec' ->> 'runtime_build_id' \
200 FROM flow_scheduled_wakeups AS wakeup \
201 JOIN flow_events AS created \
202 ON created.run_id = wakeup.run_id AND created.sequence = 1 \
203 WHERE wakeup.scheduled_at_key <= ",
204 )
205 .bind(scheduled_wakeup_key(now))
206 .append(" ORDER BY wakeup.wakeup_kind, wakeup.run_id, wakeup.subject_id"),
207 )
208 .await
209 .map_err(postgres_orm_error)?
210 .rows
211 .into_iter()
212 .map(scheduled_wakeup_from_row)
213 .collect()
214 }
215
216 async fn next_scheduled_wakeup(&self) -> Result<Option<ScheduledWakeup>> {
217 let database = Database::new(PostgresDialect, self.executor.clone());
218 database
219 .fetch_all_as(sql_query::<(String, i64, String, String, Option<String>)>(
220 "SELECT wakeup.run_id, wakeup.wakeup_kind, wakeup.subject_id, \
221 wakeup.scheduled_at_key, \
222 created.event_json::jsonb -> 'spec' ->> 'runtime_build_id' \
223 FROM flow_scheduled_wakeups AS wakeup \
224 JOIN flow_events AS created \
225 ON created.run_id = wakeup.run_id AND created.sequence = 1 \
226 ORDER BY wakeup.scheduled_at_key, wakeup.run_id, \
227 wakeup.wakeup_kind, wakeup.subject_id LIMIT 1",
228 ))
229 .await
230 .map_err(postgres_orm_error)?
231 .rows
232 .into_iter()
233 .next()
234 .map(scheduled_wakeup_from_row)
235 .transpose()
236 }
237
238 async fn find_active_hooks_by_token(&self, token: &str) -> Result<Vec<ActiveHookSnapshot>> {
239 let database = Database::new(PostgresDialect, self.executor.clone());
240 database
241 .fetch_all_as(
242 sql_query::<(String, String, String, String)>(
243 "SELECT run_id, hook_id, token, metadata_json \
244 FROM flow_active_hooks WHERE token = ",
245 )
246 .bind(token)
247 .append(" ORDER BY run_id, hook_id"),
248 )
249 .await
250 .map_err(postgres_orm_error)?
251 .rows
252 .into_iter()
253 .map(active_hook_from_row)
254 .collect()
255 }
256
257 async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
258 let database = Database::new(PostgresDialect, self.executor.clone());
259 database
260 .fetch_all_as(sql_query::<(String, String, String, String)>(
261 "SELECT run_id, hook_id, token, metadata_json \
262 FROM flow_active_hooks ORDER BY run_id, hook_id",
263 ))
264 .await
265 .map_err(postgres_orm_error)?
266 .rows
267 .into_iter()
268 .map(active_hook_from_row)
269 .collect()
270 }
271}
272
273async fn execute_postgres<E>(executor: &E, query: SqlQuery<()>) -> Result<u64>
274where
275 E: Executor<Row = PostgresRow, Error = PostgresError>,
276{
277 let query = query
278 .compile(&PostgresDialect)
279 .map_err(postgres_query_error)?;
280 Ok(executor
281 .execute(&query)
282 .await
283 .map_err(postgres_driver_error)?
284 .rows_affected)
285}
286
287async fn fetch_all_postgres<T, E>(executor: &E, query: SqlQuery<T>) -> Result<Vec<T>>
288where
289 T: FromRow + Send,
290 E: Executor<Row = PostgresRow, Error = PostgresError>,
291{
292 let query = query
293 .compile(&PostgresDialect)
294 .map_err(postgres_query_error)?;
295 executor
296 .fetch_all(&query)
297 .await
298 .map_err(postgres_driver_error)?
299 .rows
300 .iter()
301 .map(T::from_row)
302 .collect::<std::result::Result<Vec<_>, _>>()
303 .map_err(postgres_decode_error)
304}
305
306async fn fetch_optional_postgres<T, E>(executor: &E, query: SqlQuery<T>) -> Result<Option<T>>
307where
308 T: FromRow + Send,
309 E: Executor<Row = PostgresRow, Error = PostgresError>,
310{
311 let mut rows = fetch_all_postgres(executor, query).await?;
312 match rows.len() {
313 0 => Ok(None),
314 1 => Ok(rows.pop()),
315 actual => Err(FlowError::Store(format!(
316 "PostgreSQL Flow query returned {actual} rows where at most one was expected"
317 ))),
318 }
319}
320
321async fn lock_postgres_run(transaction: &PostgresTransaction, run_id: &str) -> Result<()> {
322 let query = sql_query::<i64>("SELECT 1 FROM pg_advisory_xact_lock(hashtext(")
325 .bind(run_id)
326 .append("), 0)")
327 .compile(&PostgresDialect)
328 .map_err(postgres_query_error)?;
329 transaction
330 .fetch_all(&query)
331 .await
332 .map_err(postgres_driver_error)?;
333 Ok(())
334}
335
336async fn lock_postgres_active_hook_token(
337 transaction: &PostgresTransaction,
338 token: &str,
339) -> Result<()> {
340 let query = sql_query::<i64>("SELECT 1 FROM pg_advisory_xact_lock(hashtext(")
343 .bind(token)
344 .append("), 2)")
345 .compile(&PostgresDialect)
346 .map_err(postgres_query_error)?;
347 transaction
348 .fetch_all(&query)
349 .await
350 .map_err(postgres_driver_error)?;
351 Ok(())
352}
353
354async fn lock_postgres_retention_guard_shared(
355 transaction: &PostgresTransaction,
356 lock_id: &str,
357) -> Result<()> {
358 let query = sql_query::<i64>("SELECT 1 FROM pg_advisory_xact_lock_shared(hashtext(")
359 .bind(lock_id)
360 .append("), 1)")
361 .compile(&PostgresDialect)
362 .map_err(postgres_query_error)?;
363 transaction
364 .fetch_all(&query)
365 .await
366 .map_err(postgres_driver_error)?;
367 Ok(())
368}
369
370async fn lock_postgres_retention_guard_exclusive(
371 transaction: &PostgresTransaction,
372 lock_id: &str,
373) -> Result<()> {
374 let query = sql_query::<i64>("SELECT 1 FROM pg_advisory_xact_lock(hashtext(")
375 .bind(lock_id)
376 .append("), 1)")
377 .compile(&PostgresDialect)
378 .map_err(postgres_query_error)?;
379 transaction
380 .fetch_all(&query)
381 .await
382 .map_err(postgres_driver_error)?;
383 Ok(())
384}
385
386async fn latest_postgres_sequence(transaction: &PostgresTransaction, run_id: &str) -> Result<u64> {
387 let query = sql_query::<i64>(
388 "SELECT COALESCE(MAX(sequence), 0)::BIGINT FROM flow_events WHERE run_id = ",
389 )
390 .bind(run_id)
391 .compile(&PostgresDialect)
392 .map_err(postgres_query_error)?;
393 let rows = transaction
394 .fetch_all(&query)
395 .await
396 .map_err(postgres_driver_error)?
397 .rows;
398 let row = rows
399 .first()
400 .ok_or_else(|| FlowError::Store("PostgreSQL sequence query returned no row".to_string()))?;
401 let sequence = i64::from_row(row).map_err(postgres_decode_error)?;
402 u64::try_from(sequence).map_err(|error| {
403 FlowError::Store(format!(
404 "invalid PostgreSQL event sequence {sequence}: {error}"
405 ))
406 })
407}
408
409async fn ensure_postgres_active_hook_available(
410 transaction: &PostgresTransaction,
411 run_id: &str,
412 hook_id: &str,
413 token: &str,
414) -> Result<()> {
415 lock_postgres_active_hook_token(transaction, token).await?;
416 let owners = fetch_all_postgres::<(String, String), _>(
417 transaction,
418 sql_query::<(String, String)>(
419 "SELECT run_id, hook_id FROM flow_active_hooks WHERE token = ",
420 )
421 .bind(token),
422 )
423 .await?;
424 if let Some((existing_run_id, existing_hook_id)) = owners.into_iter().next() {
425 if existing_run_id == run_id && existing_hook_id == hook_id {
426 return Ok(());
427 }
428 return Err(FlowError::HookTokenConflict {
429 token: token.to_string(),
430 existing_run_id,
431 existing_hook_id,
432 });
433 }
434
435 let existing_tokens = fetch_all_postgres::<String, _>(
436 transaction,
437 sql_query::<String>("SELECT token FROM flow_active_hooks WHERE run_id = ")
438 .bind(run_id)
439 .append(" AND hook_id = ")
440 .bind(hook_id),
441 )
442 .await?;
443 if existing_tokens
444 .first()
445 .is_some_and(|existing_token| existing_token != token)
446 {
447 return Err(FlowError::InvalidTransition(format!(
448 "active hook {hook_id} for run {run_id} already uses a different token (value redacted)"
449 )));
450 }
451 Ok(())
452}
453
454async fn insert_postgres_envelope(
455 transaction: &PostgresTransaction,
456 envelope: &FlowEventEnvelope,
457) -> Result<()> {
458 let sequence = i64::try_from(envelope.sequence).map_err(|error| {
459 FlowError::Store(format!(
460 "event sequence {} exceeds PostgreSQL bigint range: {error}",
461 envelope.sequence
462 ))
463 })?;
464 let query = sql_query::<()>(
465 "INSERT INTO flow_events (run_id, sequence, event_id, timestamp, event_json) VALUES (",
466 )
467 .bind(envelope.run_id.clone())
468 .append(", ")
469 .bind(sequence)
470 .append(", ")
471 .bind(envelope.event_id.to_string())
472 .append(", ")
473 .bind(envelope.timestamp.to_rfc3339())
474 .append(", ")
475 .bind(serde_json::to_string(&envelope.event)?)
476 .append(")")
477 .compile(&PostgresDialect)
478 .map_err(postgres_query_error)?;
479 transaction
480 .execute(&query)
481 .await
482 .map_err(postgres_driver_error)?;
483 Ok(())
484}
485
486fn row_to_envelope(
487 (run_id, sequence, event_id, timestamp, event_json): (String, i64, String, String, String),
488) -> Result<FlowEventEnvelope> {
489 Ok(FlowEventEnvelope {
490 run_id,
491 sequence: u64::try_from(sequence).map_err(|error| {
492 FlowError::Store(format!(
493 "invalid PostgreSQL event sequence {sequence}: {error}"
494 ))
495 })?,
496 event_id: event_id.parse().map_err(|error| {
497 FlowError::Store(format!("invalid PostgreSQL event id {event_id}: {error}"))
498 })?,
499 timestamp: timestamp.parse().map_err(|error| {
500 FlowError::Store(format!(
501 "invalid PostgreSQL event timestamp {timestamp}: {error}"
502 ))
503 })?,
504 event: serde_json::from_str(&event_json)?,
505 })
506}
507
508fn active_hook_from_row(
509 (run_id, hook_id, token, metadata_json): (String, String, String, String),
510) -> Result<ActiveHookSnapshot> {
511 Ok(ActiveHookSnapshot {
512 run_id,
513 hook: HookSnapshot {
514 hook_id,
515 token,
516 status: HookStatus::Active,
517 metadata: serde_json::from_str(&metadata_json)?,
518 payload: None,
519 },
520 })
521}
522
523fn map_postgres_transaction<T>(
524 result: std::result::Result<T, PostgresTransactionError<FlowError>>,
525) -> Result<T> {
526 match result {
527 Ok(value) => Ok(value),
528 Err(PostgresTransactionError::Operation(error)) => Err(error),
529 Err(error) => Err(FlowError::Store(format!(
530 "PostgreSQL Flow transaction failed: {error}"
531 ))),
532 }
533}
534
535fn postgres_query_error(error: a3s_orm::Error) -> FlowError {
536 FlowError::Store(format!("PostgreSQL Flow query build failed: {error}"))
537}
538
539fn postgres_driver_error(error: PostgresError) -> FlowError {
540 FlowError::Store(format!("PostgreSQL Flow storage failed: {error}"))
541}
542
543fn postgres_decode_error(error: a3s_orm::DecodeError) -> FlowError {
544 FlowError::Store(format!("PostgreSQL Flow row decoding failed: {error}"))
545}
546
547fn postgres_orm_error(error: a3s_orm::DatabaseError<PostgresError>) -> FlowError {
548 FlowError::Store(format!("PostgreSQL Flow storage failed: {error}"))
549}