postrust_graphql/subscription/
broker.rs1use futures::stream::{Stream, StreamExt};
7use sqlx::postgres::PgListener;
8use sqlx::PgPool;
9use std::collections::{HashMap, HashSet};
10use std::pin::Pin;
11use std::sync::Arc;
12use tokio::sync::broadcast;
13use tokio::sync::{watch, RwLock};
14use tracing::{debug, error, info};
15
16const DEFAULT_CHANNEL_CAPACITY: usize = 256;
18
19#[derive(Debug, Clone)]
21pub struct PgNotification {
22 pub channel: String,
24 pub payload: String,
26 pub process_id: u32,
28}
29
30pub struct NotifyBroker {
32 pool: PgPool,
34 channels: Arc<RwLock<HashMap<String, broadcast::Sender<PgNotification>>>>,
36 channel_capacity: usize,
38 running: Arc<RwLock<bool>>,
40 listening: Arc<RwLock<HashSet<String>>>,
44 stops: watch::Sender<u64>,
50}
51
52impl NotifyBroker {
53 pub fn new(pool: PgPool) -> Self {
55 Self::with_capacity(pool, DEFAULT_CHANNEL_CAPACITY)
56 }
57
58 pub fn with_capacity(pool: PgPool, capacity: usize) -> Self {
60 Self {
61 pool,
62 channels: Arc::new(RwLock::new(HashMap::new())),
63 channel_capacity: capacity,
64 running: Arc::new(RwLock::new(false)),
65 listening: Arc::new(RwLock::new(HashSet::new())),
66 stops: watch::channel(0).0,
67 }
68 }
69
70 pub async fn start(&self, listen_channels: Vec<String>) -> Result<(), BrokerError> {
75 {
76 let mut running = self.running.write().await;
77 if *running {
78 return Err(BrokerError::AlreadyRunning);
79 }
80 *running = true;
81 }
82 self.listen(listen_channels).await
83 }
84
85 pub async fn listen(&self, channels: Vec<String>) -> Result<(), BrokerError> {
92 let mut listening = self.listening.write().await;
93 let mut added = HashSet::new();
94 let new: Vec<String> = channels
95 .into_iter()
96 .filter(|channel| !listening.contains(channel) && added.insert(channel.clone()))
97 .collect();
98 if new.is_empty() {
99 return Ok(());
100 }
101
102 {
103 let mut senders = self.channels.write().await;
104 for channel in &new {
105 senders
106 .entry(channel.clone())
107 .or_insert_with(|| broadcast::channel(self.channel_capacity).0);
108 }
109 }
110
111 let mut listener = PgListener::connect_with(&self.pool)
112 .await
113 .map_err(BrokerError::Database)?;
114 for channel in &new {
115 listener
116 .listen(channel)
117 .await
118 .map_err(BrokerError::Database)?;
119 info!("Listening on PostgreSQL channel: {}", channel);
120 }
121 listening.extend(new);
122 drop(listening);
123
124 let channels = Arc::clone(&self.channels);
125 let mut stops = self.stops.subscribe();
126 let started_under = *stops.borrow_and_update();
127
128 tokio::spawn(async move {
129 loop {
130 let received = tokio::select! {
131 _ = stops.wait_for(|stops| *stops != started_under) => break,
132 received = listener.try_recv() => received,
133 };
134 match received {
135 Ok(Some(notification)) => {
136 let pg_notification = PgNotification {
137 channel: notification.channel().to_string(),
138 payload: notification.payload().to_string(),
139 process_id: notification.process_id(),
140 };
141
142 debug!(
143 "Received notification on channel '{}': {}",
144 pg_notification.channel,
145 &pg_notification.payload[..pg_notification.payload.len().min(100)]
146 );
147
148 let channels_read = channels.read().await;
150 if let Some(sender) = channels_read.get(&pg_notification.channel) {
151 let _ = sender.send(pg_notification);
153 }
154 }
155 Ok(None) => {
156 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
158 }
159 Err(e) => {
160 error!("Error receiving notification: {:?}", e);
161 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
163 }
164 }
165 }
166 info!("Broker stopped, exiting listener loop");
167 });
168
169 Ok(())
170 }
171
172 pub async fn stop(&self) {
174 *self.running.write().await = false;
175 self.listening.write().await.clear();
176 self.stops.send_modify(|stops| *stops += 1);
177 info!("Broker stop requested");
178 }
179
180 pub async fn subscribe(
184 &self,
185 channel: &str,
186 ) -> Result<Pin<Box<dyn Stream<Item = PgNotification> + Send>>, BrokerError> {
187 let channels = self.channels.read().await;
188
189 let sender = channels
190 .get(channel)
191 .ok_or_else(|| BrokerError::ChannelNotFound(channel.to_string()))?;
192
193 let receiver = sender.subscribe();
194
195 let stream = tokio_stream::wrappers::BroadcastStream::new(receiver)
197 .filter_map(|result| futures::future::ready(result.ok()));
198
199 Ok(Box::pin(stream))
200 }
201
202 pub async fn subscribe_or_create(
207 &self,
208 channel: &str,
209 ) -> Pin<Box<dyn Stream<Item = PgNotification> + Send>> {
210 {
212 let channels = self.channels.read().await;
213 if let Some(sender) = channels.get(channel) {
214 let receiver = sender.subscribe();
215 let stream = tokio_stream::wrappers::BroadcastStream::new(receiver)
216 .filter_map(|result| futures::future::ready(result.ok()));
217 return Box::pin(stream);
218 }
219 }
220
221 {
223 let mut channels = self.channels.write().await;
224 if !channels.contains_key(channel) {
226 let (tx, _) = broadcast::channel(self.channel_capacity);
227 channels.insert(channel.to_string(), tx);
228 }
229 }
230
231 let channels = self.channels.read().await;
233 let sender = channels.get(channel).expect("just created");
234 let receiver = sender.subscribe();
235 let stream = tokio_stream::wrappers::BroadcastStream::new(receiver)
236 .filter_map(|result| futures::future::ready(result.ok()));
237 Box::pin(stream)
238 }
239
240 pub async fn listen_channel(&self, channel: &str) -> Result<(), BrokerError> {
242 self.listen(vec![channel.to_string()]).await
243 }
244
245 pub async fn is_running(&self) -> bool {
247 *self.running.read().await
248 }
249
250 pub async fn channel_count(&self) -> usize {
252 self.channels.read().await.len()
253 }
254}
255
256#[derive(Debug, thiserror::Error)]
258pub enum BrokerError {
259 #[error("Database error: {0}")]
261 Database(#[from] sqlx::Error),
262
263 #[error("Channel not found: {0}")]
265 ChannelNotFound(String),
266
267 #[error("Broker is already running")]
270 AlreadyRunning,
271}
272
273pub fn table_channel_name(schema: &str, table: &str) -> String {
275 format!("postrust_{}_{}", schema, table)
276}
277
278pub fn create_notify_trigger_sql(schema: &str, table: &str) -> String {
280 let channel = table_channel_name(schema, table);
281 let trigger_name = format!("postrust_notify_{}_{}", schema, table);
282 let function_name = format!("postrust_notify_{}_{}_fn", schema, table);
283
284 format!(
285 r#"
286-- Create notification function
287CREATE OR REPLACE FUNCTION {schema}.{function_name}()
288RETURNS TRIGGER AS $$
289DECLARE
290 payload jsonb;
291BEGIN
292 IF TG_OP = 'DELETE' THEN
293 payload := jsonb_build_object(
294 'operation', 'DELETE',
295 'table', TG_TABLE_NAME,
296 'schema', TG_TABLE_SCHEMA,
297 'old', row_to_json(OLD)
298 );
299 ELSIF TG_OP = 'UPDATE' THEN
300 payload := jsonb_build_object(
301 'operation', 'UPDATE',
302 'table', TG_TABLE_NAME,
303 'schema', TG_TABLE_SCHEMA,
304 'old', row_to_json(OLD),
305 'new', row_to_json(NEW)
306 );
307 ELSIF TG_OP = 'INSERT' THEN
308 payload := jsonb_build_object(
309 'operation', 'INSERT',
310 'table', TG_TABLE_NAME,
311 'schema', TG_TABLE_SCHEMA,
312 'new', row_to_json(NEW)
313 );
314 END IF;
315
316 PERFORM pg_notify('{channel}', payload::text);
317
318 RETURN COALESCE(NEW, OLD);
319END;
320$$ LANGUAGE plpgsql;
321
322-- Create trigger
323DROP TRIGGER IF EXISTS {trigger_name} ON {schema}.{table};
324CREATE TRIGGER {trigger_name}
325 AFTER INSERT OR UPDATE OR DELETE ON {schema}.{table}
326 FOR EACH ROW
327 EXECUTE FUNCTION {schema}.{function_name}();
328"#,
329 schema = schema,
330 table = table,
331 channel = channel,
332 function_name = function_name,
333 trigger_name = trigger_name
334 )
335}
336
337pub fn drop_notify_trigger_sql(schema: &str, table: &str) -> String {
339 let trigger_name = format!("postrust_notify_{}_{}", schema, table);
340 let function_name = format!("postrust_notify_{}_{}_fn", schema, table);
341
342 format!(
343 r#"
344DROP TRIGGER IF EXISTS {trigger_name} ON {schema}.{table};
345DROP FUNCTION IF EXISTS {schema}.{function_name}();
346"#,
347 schema = schema,
348 table = table,
349 trigger_name = trigger_name,
350 function_name = function_name
351 )
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_table_channel_name() {
360 assert_eq!(
361 table_channel_name("public", "users"),
362 "postrust_public_users"
363 );
364 assert_eq!(table_channel_name("api", "orders"), "postrust_api_orders");
365 }
366
367 #[test]
368 fn test_create_notify_trigger_sql() {
369 let sql = create_notify_trigger_sql("public", "users");
370 assert!(sql.contains("CREATE OR REPLACE FUNCTION"));
371 assert!(sql.contains("postrust_notify_public_users_fn"));
372 assert!(sql.contains("CREATE TRIGGER"));
373 assert!(sql.contains("pg_notify"));
374 assert!(sql.contains("postrust_public_users"));
375 }
376
377 #[test]
378 fn test_drop_notify_trigger_sql() {
379 let sql = drop_notify_trigger_sql("public", "users");
380 assert!(sql.contains("DROP TRIGGER IF EXISTS"));
381 assert!(sql.contains("DROP FUNCTION IF EXISTS"));
382 }
383}