cratestack_sqlx/backplane/
pg_notify.rs1use std::sync::{Arc, Weak};
15use std::time::Duration;
16
17use cratestack_core::backplane::LocalHub;
18use cratestack_core::{BackplaneFuture, BackplaneReceiver, CratestackError, SubscriptionBackplane};
19use tokio::sync::{OnceCell, watch};
20
21use super::notify_payload::{NOTIFY_CHANNEL, Notification, encode};
22use crate::error::cratestack_error_from_sqlx;
23use crate::sqlx;
24use crate::sqlx::postgres::PgListener;
25
26pub struct PgNotifyBackplane {
27 pool: sqlx::PgPool,
28 hub: Arc<LocalHub>,
29 listening: OnceCell<()>,
30 alive: watch::Sender<()>,
33}
34
35impl PgNotifyBackplane {
36 pub fn new(pool: sqlx::PgPool) -> Self {
40 Self {
41 pool,
42 hub: Arc::default(),
43 listening: OnceCell::new(),
44 alive: watch::channel(()).0,
45 }
46 }
47
48 async fn ensure_listening(&self) -> Result<(), CratestackError> {
49 self.listening
50 .get_or_try_init(|| async {
51 let mut listener = PgListener::connect_with(&self.pool)
52 .await
53 .map_err(cratestack_error_from_sqlx)?;
54 listener
55 .listen(NOTIFY_CHANNEL)
56 .await
57 .map_err(cratestack_error_from_sqlx)?;
58 tokio::spawn(listen(
59 listener,
60 Arc::downgrade(&self.hub),
61 self.pool.clone(),
62 self.alive.subscribe(),
63 ));
64 Ok(())
65 })
66 .await
67 .map(|_| ())
68 }
69}
70
71impl SubscriptionBackplane for PgNotifyBackplane {
72 fn publish<'a>(
73 &'a self,
74 channel: &'a str,
75 payload: &'a str,
76 ) -> BackplaneFuture<'a, Result<(), CratestackError>> {
77 Box::pin(async move {
78 let Some(notification) = encode(channel, payload, None) else {
79 return Err(CratestackError::BadRequest(
80 "a backplane payload must stay under Postgres' 8000-byte NOTIFY limit"
81 .to_owned(),
82 ));
83 };
84 sqlx::query("SELECT pg_notify($1, $2)")
85 .bind(NOTIFY_CHANNEL)
86 .bind(notification)
87 .execute(&self.pool)
88 .await
89 .map_err(cratestack_error_from_sqlx)?;
90 Ok(())
91 })
92 }
93
94 fn subscribe<'a>(
95 &'a self,
96 channel: &'a str,
97 ) -> BackplaneFuture<'a, Result<BackplaneReceiver, CratestackError>> {
98 Box::pin(async move {
99 let receiver = self.hub.subscribe(channel);
102 self.ensure_listening().await?;
103 Ok(receiver)
104 })
105 }
106
107 fn publishes_model_events_in_tx(&self) -> bool {
108 true
109 }
110}
111
112async fn listen(
116 mut listener: PgListener,
117 hub: Weak<LocalHub>,
118 pool: sqlx::PgPool,
119 mut alive: watch::Receiver<()>,
120) {
121 loop {
122 let received = tokio::select! {
123 received = listener.try_recv() => received,
124 _ = alive.changed() => return,
126 };
127 let Some(hub) = hub.upgrade() else { return };
128 match received {
129 Ok(Some(notification)) => deliver(&hub, &pool, notification.payload()).await,
130 Ok(None) => {
131 tracing::warn!(target: "cratestack", "backplane listener reconnecting; subscriptions resync");
132 hub.resync_all();
133 }
134 Err(error) => {
135 tracing::warn!(target: "cratestack", %error, "backplane listener failed; retrying");
136 hub.resync_all();
137 tokio::time::sleep(Duration::from_secs(1)).await;
138 }
139 }
140 }
141}
142
143async fn deliver(hub: &LocalHub, pool: &sqlx::PgPool, raw: &str) {
144 let Ok(notification) = serde_json::from_str::<Notification<'_>>(raw) else {
145 tracing::warn!(target: "cratestack", "unreadable backplane notification ignored");
146 return;
147 };
148 let payload = match (notification.payload, notification.event_id) {
149 (Some(payload), _) => Arc::from(payload.as_ref()),
150 (None, Some(event_id)) => {
151 match crate::descriptor::outbox_envelope_json(pool, event_id).await {
152 Ok(payload) => Arc::from(payload),
153 Err(error) => {
154 tracing::warn!(target: "cratestack", %error, "large model event unreadable; its channel resyncs");
156 hub.resync(¬ification.channel);
157 return;
158 }
159 }
160 }
161 (None, None) => return,
162 };
163 hub.deliver(¬ification.channel, payload);
164}