1use std::sync::Arc;
2use std::sync::atomic::AtomicBool;
3
4use crate::sqlx;
5
6use cratestack_core::{
7 CoolError, CoolEventBus, CoolEventEnvelope, CoolEventFuture, ModelEventKind, SubscriptionHandle,
8};
9
10use crate::error::cool_error_from_sqlx;
11
12#[derive(Debug, Clone)]
13pub struct SqlxRuntime {
14 pool: sqlx::PgPool,
15 events: CoolEventBus,
16 audit_table_ensured: Arc<AtomicBool>,
23}
24
25impl SqlxRuntime {
26 pub fn new(pool: sqlx::PgPool) -> Self {
27 Self {
28 pool,
29 events: CoolEventBus::default(),
30 audit_table_ensured: Arc::new(AtomicBool::new(false)),
31 }
32 }
33
34 pub fn pool(&self) -> &sqlx::PgPool {
35 &self.pool
36 }
37
38 pub(crate) fn audit_table_ensured(&self) -> &AtomicBool {
39 &self.audit_table_ensured
40 }
41
42 #[doc(hidden)]
43 pub fn subscribe<F>(
44 &self,
45 model: &'static str,
46 operation: ModelEventKind,
47 handler: F,
48 ) -> SubscriptionHandle
49 where
50 F: Fn(CoolEventEnvelope) -> CoolEventFuture + Send + Sync + 'static,
51 {
52 self.events.subscribe(model, operation, handler)
53 }
54
55 #[doc(hidden)]
60 pub fn events_bus(&self) -> CoolEventBus {
61 self.events.clone()
62 }
63
64 #[doc(hidden)]
65 pub async fn drain_event_outbox(&self) -> Result<usize, CoolError> {
66 ensure_event_outbox_table(&self.pool).await?;
67
68 let rows = sqlx::query_as::<_, EventOutboxRow>(
69 "SELECT event_id, model, operation, occurred_at, payload, attempts, last_error \
70 FROM cratestack_event_outbox \
71 WHERE delivered_at IS NULL \
72 ORDER BY occurred_at ASC, event_id ASC",
73 )
74 .fetch_all(&self.pool)
75 .await
76 .map_err(cool_error_from_sqlx)?;
77
78 let mut delivered = 0usize;
79 for row in rows {
80 let event_id = row.event_id;
81 let envelope = row.try_into_envelope()?;
82 match self.events.emit(envelope).await {
83 Ok(()) => {
84 sqlx::query(
85 "UPDATE cratestack_event_outbox \
86 SET delivered_at = NOW(), last_error = NULL, attempts = attempts + 1 \
87 WHERE event_id = $1",
88 )
89 .bind(event_id)
90 .execute(&self.pool)
91 .await
92 .map_err(cool_error_from_sqlx)?;
93 delivered += 1;
94 }
95 Err(error) => {
96 sqlx::query(
97 "UPDATE cratestack_event_outbox \
98 SET attempts = attempts + 1, last_error = $2 \
99 WHERE event_id = $1",
100 )
101 .bind(event_id)
102 .bind(error.to_string())
103 .execute(&self.pool)
104 .await
105 .map_err(cool_error_from_sqlx)?;
106 }
107 }
108 }
109
110 Ok(delivered)
111 }
112}
113
114#[derive(Debug, Clone)]
115pub(crate) struct EventOutboxRow {
116 pub(crate) event_id: uuid::Uuid,
117 pub(crate) model: String,
118 pub(crate) operation: String,
119 pub(crate) occurred_at: chrono::DateTime<chrono::Utc>,
120 pub(crate) payload: serde_json::Value,
121 pub(crate) attempts: i64,
122 pub(crate) last_error: Option<String>,
123}
124
125impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for EventOutboxRow {
129 fn from_row(row: &'r sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
130 use sqlx::Row;
131 Ok(Self {
132 event_id: row.try_get("event_id")?,
133 model: row.try_get("model")?,
134 operation: row.try_get("operation")?,
135 occurred_at: row.try_get("occurred_at")?,
136 payload: row.try_get("payload")?,
137 attempts: row.try_get("attempts")?,
138 last_error: row.try_get("last_error")?,
139 })
140 }
141}
142
143impl EventOutboxRow {
144 pub(crate) fn try_into_envelope(self) -> Result<CoolEventEnvelope, CoolError> {
145 let _ = self.attempts;
146 let _ = &self.last_error;
147 Ok(CoolEventEnvelope {
148 event_id: self.event_id,
149 model: self.model,
150 operation: ModelEventKind::parse(&self.operation)?,
151 occurred_at: self.occurred_at,
152 data: self.payload,
153 })
154 }
155}
156
157pub(crate) async fn ensure_event_outbox_table<'e, E>(executor: E) -> Result<(), CoolError>
158where
159 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
160{
161 sqlx::query(
162 "CREATE TABLE IF NOT EXISTS cratestack_event_outbox (\
163 event_id UUID PRIMARY KEY, \
164 model TEXT NOT NULL, \
165 operation TEXT NOT NULL, \
166 occurred_at TIMESTAMPTZ NOT NULL, \
167 payload JSONB NOT NULL, \
168 delivered_at TIMESTAMPTZ, \
169 attempts BIGINT NOT NULL DEFAULT 0, \
170 last_error TEXT\
171 )",
172 )
173 .execute(executor)
174 .await
175 .map_err(cool_error_from_sqlx)?;
176
177 Ok(())
178}
179
180pub(crate) async fn enqueue_event_outbox<'e, E, T>(
181 executor: E,
182 model: &'static str,
183 operation: ModelEventKind,
184 data: &T,
185) -> Result<(), CoolError>
186where
187 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
188 T: serde::Serialize,
189{
190 let payload = serde_json::to_value(data)
191 .map_err(|error| CoolError::Codec(format!("failed to encode event payload: {error}")))?;
192 sqlx::query(
193 "INSERT INTO cratestack_event_outbox (event_id, model, operation, occurred_at, payload) \
194 VALUES ($1, $2, $3, $4, $5)",
195 )
196 .bind(uuid::Uuid::new_v4())
197 .bind(model)
198 .bind(operation.as_str())
199 .bind(chrono::Utc::now())
200 .bind(payload)
201 .execute(executor)
202 .await
203 .map_err(cool_error_from_sqlx)?;
204
205 Ok(())
206}