cratestack_sqlx/
descriptor.rs1mod event_outbox;
2
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5
6use crate::sqlx;
7
8use cratestack_core::{
9 AuditSink, CoolError, CoolEventBus, CoolEventEnvelope, CoolEventFuture, ModelEventKind,
10 NoopAuditSink, SubscriptionHandle,
11};
12
13use crate::error::cool_error_from_sqlx;
14use event_outbox::EventOutboxRow;
15
16pub(crate) use event_outbox::{enqueue_event_outbox, ensure_event_outbox_table};
17
18#[derive(Clone)]
19pub struct SqlxRuntime {
20 pool: sqlx::PgPool,
21 events: CoolEventBus,
22 audit_table_ensured: Arc<AtomicBool>,
29 audit_sink: Arc<dyn AuditSink>,
38}
39
40impl std::fmt::Debug for SqlxRuntime {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("SqlxRuntime")
47 .field("pool", &self.pool)
48 .field("events", &self.events)
49 .field(
50 "audit_table_ensured",
51 &self
52 .audit_table_ensured
53 .load(std::sync::atomic::Ordering::Relaxed),
54 )
55 .finish_non_exhaustive()
56 }
57}
58
59impl SqlxRuntime {
60 pub fn new(pool: sqlx::PgPool) -> Self {
61 Self {
62 pool,
63 events: CoolEventBus::default(),
64 audit_table_ensured: Arc::new(AtomicBool::new(false)),
65 audit_sink: Arc::new(NoopAuditSink),
66 }
67 }
68
69 pub fn pool(&self) -> &sqlx::PgPool {
70 &self.pool
71 }
72
73 pub(crate) fn audit_table_ensured(&self) -> &AtomicBool {
74 &self.audit_table_ensured
75 }
76
77 pub fn with_audit_sink(mut self, sink: Arc<dyn AuditSink>) -> Self {
84 self.audit_sink = sink;
85 self
86 }
87
88 pub(crate) fn audit_sink(&self) -> &Arc<dyn AuditSink> {
89 &self.audit_sink
90 }
91
92 #[doc(hidden)]
93 pub fn subscribe<F>(
94 &self,
95 model: &'static str,
96 operation: ModelEventKind,
97 handler: F,
98 ) -> SubscriptionHandle
99 where
100 F: Fn(CoolEventEnvelope) -> CoolEventFuture + Send + Sync + 'static,
101 {
102 self.events.subscribe(model, operation, handler)
103 }
104
105 #[doc(hidden)]
110 pub fn events_bus(&self) -> CoolEventBus {
111 self.events.clone()
112 }
113
114 #[doc(hidden)]
115 pub async fn drain_event_outbox(&self) -> Result<usize, CoolError> {
116 ensure_event_outbox_table(&self.pool).await?;
117
118 let rows = sqlx::query_as::<_, EventOutboxRow>(
119 "SELECT event_id, model, operation, occurred_at, payload, attempts, last_error \
120 FROM cratestack_event_outbox \
121 WHERE delivered_at IS NULL \
122 ORDER BY occurred_at ASC, event_id ASC",
123 )
124 .fetch_all(&self.pool)
125 .await
126 .map_err(cool_error_from_sqlx)?;
127
128 let mut delivered = 0usize;
129 for row in rows {
130 let event_id = row.event_id;
131 let envelope = row.try_into_envelope()?;
132 match self.events.emit(envelope).await {
133 Ok(()) => {
134 sqlx::query(
135 "UPDATE cratestack_event_outbox \
136 SET delivered_at = NOW(), last_error = NULL, attempts = attempts + 1 \
137 WHERE event_id = $1",
138 )
139 .bind(event_id)
140 .execute(&self.pool)
141 .await
142 .map_err(cool_error_from_sqlx)?;
143 delivered += 1;
144 }
145 Err(error) => {
146 sqlx::query(
147 "UPDATE cratestack_event_outbox \
148 SET attempts = attempts + 1, last_error = $2 \
149 WHERE event_id = $1",
150 )
151 .bind(event_id)
152 .bind(error.to_string())
153 .execute(&self.pool)
154 .await
155 .map_err(cool_error_from_sqlx)?;
156 }
157 }
158 }
159
160 Ok(delivered)
161 }
162}