cratestack_sqlx/descriptor.rs
1mod drain;
2mod event_enqueue;
3mod event_outbox;
4mod outbox_read;
5mod outbox_upgrade;
6
7use std::sync::Arc;
8use std::sync::atomic::AtomicBool;
9
10use crate::sqlx;
11
12use cratestack_core::{
13 AuditSink, CratestackEventBus, CratestackEventEnvelope, CratestackEventFuture, ModelEventKind,
14 NoopAuditSink, SubscriptionBackplane, SubscriptionHandle,
15};
16
17pub use event_enqueue::enqueue_event_outbox;
18pub(crate) use event_enqueue::{enqueue_model_event, enqueue_model_update};
19pub use event_outbox::ensure_event_outbox_table;
20pub(crate) use outbox_read::outbox_envelope_json;
21pub(crate) use outbox_upgrade::OutboxColumn;
22
23#[derive(Clone)]
24pub struct SqlxRuntime {
25 pool: sqlx::PgPool,
26 events: CratestackEventBus,
27 // Shared (not per-clone) so every handle onto the same logical
28 // runtime agrees on whether `cratestack_audit` has been
29 // bootstrapped. See `crate::audit::ensure_audit_table` — this is
30 // what lets it skip re-issuing `CREATE INDEX IF NOT EXISTS` after
31 // the first call, which is what self-deadlocked chained
32 // `run_in_tx` audit writes in a caller-managed transaction.
33 audit_table_ensured: Arc<AtomicBool>,
34 // Shared for the same reason: whether the outbox table is known to have
35 // `payload_before` (`outbox_upgrade.rs`).
36 outbox_column: Arc<outbox_upgrade::OutboxColumn>,
37 // Installation point for cratestack#473: defaults to `NoopAuditSink`
38 // so existing callers of `new()` see no behavior change. Installed
39 // via `with_audit_sink` (mirrors `IdempotencyLayer::new`/
40 // `with_principal_fingerprint`'s builder shape). The DB write in
41 // `crate::audit::enqueue_audit_event` remains the sole source of
42 // truth; this is a best-effort downstream projection dispatched
43 // from `crate::audit::dispatch_audit_sink` after the owning
44 // transaction commits.
45 audit_sink: Arc<dyn AuditSink>,
46 // `Some` only on the per-attempt runtime `run_isolated` builds for an
47 // `@isolation` procedure: every executing path then runs on this
48 // transaction instead of the pool (docs/design/procedure-isolation.md §4).
49 pub(crate) bound: Option<Arc<crate::bound::BoundTx>>,
50 pub(crate) isolation_max_retries: u32,
51 // Where model events reach subscribers on every replica
52 // (docs/design/subscriptions-websocket.md §4). `PgNotifyBackplane` by
53 // default; `with_backplane` swaps it (an `InProcessBackplane` for a
54 // single replica behind a pooler that drops `LISTEN`).
55 backplane: Arc<dyn SubscriptionBackplane>,
56}
57
58// `dyn AuditSink` has no `Debug` bound (matching `IdempotencyStore` /
59// `RateLimitStore`, neither of which require one either), so this can't
60// be `#[derive(Debug)]` — same reason `CratestackEventBus` hand-rolls its own
61// `Debug` impl instead of deriving one.
62impl std::fmt::Debug for SqlxRuntime {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("SqlxRuntime")
65 .field("pool", &self.pool)
66 .field("events", &self.events)
67 .field("bound", &self.bound.is_some())
68 .field("isolation_max_retries", &self.isolation_max_retries)
69 .field(
70 "audit_table_ensured",
71 &self
72 .audit_table_ensured
73 .load(std::sync::atomic::Ordering::Relaxed),
74 )
75 .finish_non_exhaustive()
76 }
77}
78
79impl SqlxRuntime {
80 pub fn new(pool: sqlx::PgPool) -> Self {
81 let backplane: Arc<dyn SubscriptionBackplane> =
82 Arc::new(crate::backplane::PgNotifyBackplane::new(pool.clone()));
83 Self {
84 pool,
85 events: CratestackEventBus::default(),
86 audit_table_ensured: Arc::new(AtomicBool::new(false)),
87 outbox_column: Arc::default(),
88 audit_sink: Arc::new(NoopAuditSink),
89 bound: None,
90 isolation_max_retries: crate::isolation::MAX_RETRIES_DEFAULT,
91 backplane,
92 }
93 }
94
95 pub fn pool(&self) -> &sqlx::PgPool {
96 &self.pool
97 }
98
99 pub(crate) fn audit_table_ensured(&self) -> &AtomicBool {
100 &self.audit_table_ensured
101 }
102
103 /// Install a custom [`AuditSink`] that every `@@audit` mutation on
104 /// this runtime fans out to, in addition to the in-database
105 /// `cratestack_audit` table row `enqueue_audit_event` always
106 /// writes. Composable via [`cratestack_core::MulticastAuditSink`]
107 /// for more than one downstream (Kafka, Redis pubsub, a webhook —
108 /// this crate ships none of them; see `AuditSink`'s doc comment).
109 pub fn with_audit_sink(mut self, sink: Arc<dyn AuditSink>) -> Self {
110 self.audit_sink = sink;
111 self
112 }
113
114 /// Replace the subscription backplane (default: `PgNotifyBackplane` on
115 /// this runtime's pool).
116 pub fn with_backplane(mut self, backplane: Arc<dyn SubscriptionBackplane>) -> Self {
117 self.backplane = backplane;
118 self
119 }
120
121 pub fn backplane(&self) -> &Arc<dyn SubscriptionBackplane> {
122 &self.backplane
123 }
124
125 pub(crate) fn audit_sink(&self) -> &Arc<dyn AuditSink> {
126 &self.audit_sink
127 }
128
129 #[doc(hidden)]
130 pub fn subscribe<F>(
131 &self,
132 model: &'static str,
133 operation: ModelEventKind,
134 handler: F,
135 ) -> SubscriptionHandle
136 where
137 F: Fn(CratestackEventEnvelope) -> CratestackEventFuture + Send + Sync + 'static,
138 {
139 self.events.subscribe(model, operation, handler)
140 }
141
142 /// An owned, cheaply-cloneable handle onto the underlying
143 /// `CratestackEventBus` — needed by callers (e.g. `@@subscribe` SSE
144 /// dispatch, cratestack#390) that outlive the `&SqlxRuntime` borrow
145 /// `subscribe`/`unsubscribe` would otherwise require.
146 #[doc(hidden)]
147 pub fn events_bus(&self) -> CratestackEventBus {
148 self.events.clone()
149 }
150}