Skip to main content

wasi_pg_client/
notification.rs

1//! PostgreSQL LISTEN/NOTIFY support.
2//!
3//! PostgreSQL provides an asynchronous notification system where clients can
4//! subscribe to named channels (`LISTEN`) and other clients can send
5//! notifications to those channels (`NOTIFY`). Notifications arrive as
6//! `NotificationResponse` backend messages, which can appear at any time
7//! between other messages.
8//!
9//! # Example
10//! ```ignore
11//! // Connection A: listen
12//! conn_a.listen("my_channel").await?;
13//!
14//! // Connection B: notify
15//! conn_b.notify("my_channel", "hello!").await?;
16//!
17//! // Connection A: receive
18//! conn_a.wait_for_notification(None).await?;
19//! let notifications = conn_a.notifications();
20//! ```
21
22use std::time::Duration;
23
24use crate::protocol::{BackendMessage, TransactionStatus};
25
26use crate::connection::{Connection, ConnectionState};
27use crate::error::{PgError, Result};
28
29#[cfg(feature = "tracing")]
30use crate::tracing_ext::TARGET_NOTIFICATION;
31
32// ---------------------------------------------------------------------------
33// Notification (re-exported from connection, but documented here)
34// ---------------------------------------------------------------------------
35
36/// An asynchronous notification received from PostgreSQL.
37///
38/// Notifications are delivered via the `LISTEN`/`NOTIFY` protocol. A
39/// notification includes the process ID of the notifying backend, the
40/// channel name, and an optional payload string.
41///
42/// Notifications can arrive at any time — they are interleaved with other
43/// backend messages. The connection buffers them in an internal queue.
44/// Use [`Connection::notifications`] or [`Connection::drain_notifications`]
45/// to retrieve buffered notifications, or [`Connection::wait_for_notification`]
46/// to block until one arrives.
47#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct Notification {
50    /// Backend process ID that sent the notification.
51    pub process_id: i32,
52    /// Channel name.
53    pub channel: String,
54    /// Payload string (empty string if no payload was sent).
55    pub payload: String,
56}
57
58// ---------------------------------------------------------------------------
59// Connection methods
60// ---------------------------------------------------------------------------
61
62impl Connection {
63    /// Start listening for notifications on a channel.
64    ///
65    /// This executes `LISTEN <channel>` on the server. After this call,
66    /// any `NOTIFY` on the same channel (from any connection) will cause
67    /// a `NotificationResponse` message to be sent to this connection.
68    ///
69    /// # Example
70    /// ```ignore
71    /// conn.listen("events").await?;
72    /// ```
73    #[must_use = "listen errors should be checked"]
74    pub async fn listen(&mut self, channel: &str) -> Result<()> {
75        let sql = Self::build_listen_sql(channel);
76        self.execute(&sql).await?;
77        self.session_state.track_listen(channel);
78        #[cfg(feature = "tracing")]
79        tracing::info!(target: TARGET_NOTIFICATION, channel = %channel, "LISTEN: subscribed to channel");
80        Ok(())
81    }
82
83    /// Stop listening on a channel.
84    ///
85    /// This executes `UNLISTEN <channel>` on the server.
86    #[must_use = "unlisten errors should be checked"]
87    pub async fn unlisten(&mut self, channel: &str) -> Result<()> {
88        let sql = Self::build_unlisten_sql(channel);
89        self.execute(&sql).await?;
90        self.session_state.track_unlisten(channel);
91        Ok(())
92    }
93
94    /// Stop listening on all channels.
95    ///
96    /// This executes `UNLISTEN *` on the server.
97    #[must_use = "unlisten errors should be checked"]
98    pub async fn unlisten_all(&mut self) -> Result<()> {
99        self.execute("UNLISTEN *").await?;
100        self.session_state.clear_listen_channels();
101        Ok(())
102    }
103
104    /// Send a notification on a channel.
105    ///
106    /// This uses `pg_notify(channel, payload)` which properly handles
107    /// identifier quoting and payload escaping.
108    ///
109    /// # Example
110    /// ```ignore
111    /// conn.notify("events", "user_logged_in").await?;
112    /// ```
113    #[must_use = "notify errors should be checked"]
114    pub async fn notify(&mut self, channel: &str, payload: &str) -> Result<()> {
115        #[cfg(feature = "tracing")]
116        tracing::debug!(target: TARGET_NOTIFICATION, channel = %channel, payload_len = payload.len(), "NOTIFY: sending notification");
117        self.execute_params("SELECT pg_notify($1, $2)", &[&channel, &payload])
118            .await?;
119        Ok(())
120    }
121
122    /// Take all buffered notifications from the internal queue.
123    ///
124    /// Notifications can arrive at any time during other operations. They
125    /// are buffered in an internal queue. This method drains the queue
126    /// and returns all notifications that have arrived since the last call.
127    ///
128    /// This is a synchronous operation — no I/O is performed.
129    pub fn notifications(&mut self) -> Vec<Notification> {
130        self.notification_queue.drain(..).collect()
131    }
132
133    #[allow(dead_code)]
134    async fn read_next_notification_blocking(&mut self) -> Result<Notification> {
135        if !self.is_idle() {
136            return Err(PgError::InvalidState(
137                "connection must be idle while waiting for notifications".into(),
138            ));
139        }
140
141        loop {
142            let msg = self.codec.read_message(&mut self.transport).await?;
143            match msg {
144                BackendMessage::NotificationResponse(body) => {
145                    return Ok(Notification {
146                        process_id: body.process_id(),
147                        channel: body.channel().unwrap_or("").to_string(),
148                        payload: body.message().unwrap_or("").to_string(),
149                    });
150                }
151                BackendMessage::NoticeResponse(body) => {
152                    if let Ok(notice) = crate::query::Notice::from_fields(&body) {
153                        self.handle_notice(&notice);
154                    }
155                }
156                BackendMessage::ParameterStatus(body) => {
157                    if let (Ok(name), Ok(value)) = (body.name(), body.value()) {
158                        self.server_params
159                            .params
160                            .insert(name.to_string(), value.to_string());
161                    }
162                }
163                BackendMessage::ReadyForQuery(body) => {
164                    self.transaction_status = TransactionStatus::from_u8(body.status())
165                        .unwrap_or(TransactionStatus::Idle);
166                    self.state = ConnectionState::Idle;
167                }
168                BackendMessage::EmptyQueryResponse => {}
169                _ => {
170                    return Err(PgError::InvalidState(
171                        "received unexpected backend message while waiting for notification".into(),
172                    ));
173                }
174            }
175        }
176    }
177
178    /// Wait for the next notification to arrive.
179    ///
180    /// If a notification is already buffered, it is returned immediately.
181    /// Otherwise, this method blocks (async) until a notification arrives
182    /// or the optional timeout expires.
183    ///
184    /// On native tokio builds, timeout semantics are real: the read wait is
185    /// raced against the requested deadline. On other targets the timeout path
186    /// remains best-effort and uses an empty query cycle to flush pending
187    /// notifications.
188    ///
189    /// # Example
190    /// ```ignore
191    /// // Wait up to 5 seconds for a notification
192    /// if let Some(notification) = conn.wait_for_notification(Some(Duration::from_secs(5))).await? {
193    ///     println!("Got notification on {}: {}", notification.channel, notification.payload);
194    /// }
195    /// ```
196    #[must_use = "notification errors should be checked"]
197    pub async fn wait_for_notification(
198        &mut self,
199        timeout: Option<Duration>,
200    ) -> Result<Option<Notification>> {
201        // Check queue first
202        if let Some(n) = self.notification_queue.pop_front() {
203            return Ok(Some(n));
204        }
205
206        // If timeout is Some(0), return immediately without sending a query
207        if let Some(d) = timeout {
208            if d.is_zero() {
209                return Ok(None);
210            }
211        }
212
213        #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-transport"))]
214        {
215            match timeout {
216                Some(deadline) => {
217                    match tokio::time::timeout(deadline, self.read_next_notification_blocking())
218                        .await
219                    {
220                        Ok(result) => result.map(Some),
221                        Err(_) => Ok(None),
222                    }
223                }
224                None => self.read_next_notification_blocking().await.map(Some),
225            }
226        }
227
228        #[cfg(not(all(not(target_arch = "wasm32"), feature = "tokio-transport")))]
229        {
230            // Best-effort fallback for targets without a runtime timeout race.
231            let _ = timeout;
232
233            // Send an empty query to trigger a ReadyForQuery cycle.
234            // The server will deliver any pending notifications before
235            // sending ReadyForQuery.
236            self.transition(ConnectionState::ActiveSimpleQuery)?;
237
238            self.codec
239                .send(
240                    &mut self.transport,
241                    &crate::protocol::FrontendMessage::Query { sql: String::new() },
242                )
243                .await
244                .map_err(crate::error::Error::from)?;
245
246            // Read messages, collecting notifications
247            loop {
248                let msg = self.codec.read_message(&mut self.transport).await?;
249                match msg {
250                    BackendMessage::NotificationResponse(body) => {
251                        let notification = Notification {
252                            process_id: body.process_id(),
253                            channel: body.channel().unwrap_or("").to_string(),
254                            payload: body.message().unwrap_or("").to_string(),
255                        };
256
257                        // Continue reading until ReadyForQuery to drain the cycle
258                        self.read_until_ready().await?;
259
260                        return Ok(Some(notification));
261                    }
262                    BackendMessage::EmptyQueryResponse => {}
263                    BackendMessage::ReadyForQuery(body) => {
264                        self.transaction_status = TransactionStatus::from_u8(body.status())
265                            .unwrap_or(TransactionStatus::Idle);
266                        self.state = ConnectionState::Idle;
267                        break;
268                    }
269                    BackendMessage::NoticeResponse(body) => {
270                        if let Ok(notice) = crate::query::Notice::from_fields(&body) {
271                            self.handle_notice(&notice);
272                        }
273                    }
274                    BackendMessage::ParameterStatus(body) => {
275                        if let (Ok(name), Ok(value)) = (body.name(), body.value()) {
276                            self.server_params
277                                .params
278                                .insert(name.to_string(), value.to_string());
279                        }
280                    }
281                    _ => {}
282                }
283            }
284
285            // No notification arrived during this cycle
286            Ok(self.notification_queue.pop_front())
287        }
288    }
289
290    /// Wait for a notification with a timeout.
291    ///
292    /// If the notification queue already contains a notification, returns it
293    /// immediately. Otherwise, waits up to `timeout` for a new notification.
294    /// Returns `Ok(None)` if no notification arrives within the timeout.
295    ///
296    /// # Example
297    /// ```ignore
298    /// if let Some(n) = conn.wait_for_notification_with_timeout(Duration::from_secs(5)).await? {
299    ///     println!("Got notification on {}: {}", n.channel, n.payload);
300    /// }
301    /// ```
302    #[must_use = "notification errors should be checked"]
303    pub async fn wait_for_notification_with_timeout(
304        &mut self,
305        timeout: Duration,
306    ) -> Result<Option<Notification>> {
307        self.wait_for_notification(Some(timeout)).await
308    }
309}
310
311// ---------------------------------------------------------------------------
312// Tests
313// ---------------------------------------------------------------------------
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::auth::{Codec, ServerParams};
319    use crate::config::Config;
320    use crate::connection::ConnectionState;
321    use crate::protocol::TransactionStatus;
322    use crate::transport::{BufferedTransport, ClientTransport, MockTransport, PgTransport};
323    use std::collections::VecDeque;
324
325    fn make_connection(read_data: Vec<u8>) -> Connection {
326        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
327            MockTransport::new(read_data),
328        )));
329        Connection {
330            transport,
331            codec: Codec::new(),
332            server_params: ServerParams::default(),
333            state: ConnectionState::Idle,
334            config: Config::new(),
335            transaction_status: TransactionStatus::Idle,
336            notification_queue: VecDeque::new(),
337            notice_handler: None,
338            statement_counter: 0,
339            needs_recovery: false,
340            health: crate::reconnect::session::ConnectionHealth::new(),
341            session_state: crate::reconnect::session::SessionState::new(),
342        }
343    }
344
345    fn build_command_complete_msg(tag: &str) -> Vec<u8> {
346        let mut buf = vec![b'C'];
347        let mut body = Vec::new();
348        body.extend_from_slice(tag.as_bytes());
349        body.push(0);
350        let len = (body.len() + 4) as i32;
351        buf.extend_from_slice(&len.to_be_bytes());
352        buf.extend_from_slice(&body);
353        buf
354    }
355
356    fn build_ready_for_query(status: u8) -> Vec<u8> {
357        vec![b'Z', 0, 0, 0, 5, status]
358    }
359
360    fn build_notification_response(pid: i32, channel: &str, payload: &str) -> Vec<u8> {
361        let mut buf = vec![b'A'];
362        let mut body = Vec::new();
363        body.extend_from_slice(&pid.to_be_bytes());
364        body.extend_from_slice(channel.as_bytes());
365        body.push(0);
366        body.extend_from_slice(payload.as_bytes());
367        body.push(0);
368        let len = (body.len() + 4) as i32;
369        buf.extend_from_slice(&len.to_be_bytes());
370        buf.extend_from_slice(&body);
371        buf
372    }
373
374    fn build_row_description_msg(fields: &[(&str, u32)]) -> Vec<u8> {
375        let mut buf = vec![b'T'];
376        let mut body = Vec::new();
377        body.extend_from_slice(&(fields.len() as i16).to_be_bytes());
378        for (name, type_oid) in fields {
379            body.extend_from_slice(name.as_bytes());
380            body.push(0);
381            body.extend_from_slice(&0u32.to_be_bytes()); // table_oid
382            body.extend_from_slice(&0i16.to_be_bytes()); // column_id
383            body.extend_from_slice(&type_oid.to_be_bytes()); // type_oid
384            body.extend_from_slice(&(-1i16).to_be_bytes()); // type_size
385            body.extend_from_slice(&(-1i32).to_be_bytes()); // type_modifier
386            body.extend_from_slice(&0i16.to_be_bytes()); // format
387        }
388        let len = (body.len() + 4) as i32;
389        buf.extend_from_slice(&len.to_be_bytes());
390        buf.extend_from_slice(&body);
391        buf
392    }
393
394    fn build_data_row_msg(values: &[Option<&str>]) -> Vec<u8> {
395        let mut buf = vec![b'D'];
396        let mut body = Vec::new();
397        body.extend_from_slice(&(values.len() as i16).to_be_bytes());
398        for val in values {
399            match val {
400                Some(v) => {
401                    let bytes = v.as_bytes();
402                    body.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
403                    body.extend_from_slice(bytes);
404                }
405                None => {
406                    body.extend_from_slice(&(-1i32).to_be_bytes());
407                }
408            }
409        }
410        let len = (body.len() + 4) as i32;
411        buf.extend_from_slice(&len.to_be_bytes());
412        buf.extend_from_slice(&body);
413        buf
414    }
415
416    #[tokio::test]
417    async fn test_listen() {
418        let mut data = Vec::new();
419        data.extend_from_slice(&build_command_complete_msg("LISTEN"));
420        data.extend_from_slice(&build_ready_for_query(b'I'));
421
422        let mut conn = make_connection(data);
423        conn.listen("my_channel").await.unwrap();
424        assert!(conn.is_idle());
425        assert!(conn
426            .session_state()
427            .listen_channels()
428            .contains("my_channel"));
429    }
430
431    #[tokio::test]
432    async fn test_unlisten() {
433        let mut data = Vec::new();
434        data.extend_from_slice(&build_command_complete_msg("UNLISTEN"));
435        data.extend_from_slice(&build_ready_for_query(b'I'));
436
437        let mut conn = make_connection(data);
438        conn.session_state.track_listen("my_channel");
439        conn.unlisten("my_channel").await.unwrap();
440        assert!(conn.is_idle());
441        assert!(!conn
442            .session_state()
443            .listen_channels()
444            .contains("my_channel"));
445    }
446
447    #[tokio::test]
448    async fn test_unlisten_all() {
449        let mut data = Vec::new();
450        data.extend_from_slice(&build_command_complete_msg("UNLISTEN"));
451        data.extend_from_slice(&build_ready_for_query(b'I'));
452
453        let mut conn = make_connection(data);
454        conn.session_state.track_listen("ch1");
455        conn.session_state.track_listen("ch2");
456        conn.unlisten_all().await.unwrap();
457        assert!(conn.is_idle());
458        assert!(conn.session_state().listen_channels().is_empty());
459    }
460
461    #[tokio::test]
462    async fn test_notify() {
463        let mut data = Vec::new();
464        // pg_notify returns a row
465        data.extend_from_slice(&build_row_description_msg(&[(
466            "pg_notify",
467            crate::types::TEXT_OID,
468        )]));
469        data.extend_from_slice(&build_data_row_msg(&[Some("LISTEN")]));
470        data.extend_from_slice(&build_command_complete_msg("SELECT 1"));
471        data.extend_from_slice(&build_ready_for_query(b'I'));
472
473        let mut conn = make_connection(data);
474        conn.notify("my_channel", "hello").await.unwrap();
475        assert!(conn.is_idle());
476    }
477
478    #[tokio::test]
479    async fn test_notifications_buffered() {
480        let mut conn = make_connection(vec![]);
481
482        // Manually push notifications into the queue
483        conn.notification_queue.push_back(Notification {
484            process_id: 1,
485            channel: "ch1".to_string(),
486            payload: "hello".to_string(),
487        });
488        conn.notification_queue.push_back(Notification {
489            process_id: 2,
490            channel: "ch2".to_string(),
491            payload: "world".to_string(),
492        });
493
494        let notifications = conn.notifications();
495        assert_eq!(notifications.len(), 2);
496        assert_eq!(notifications[0].channel, "ch1");
497        assert_eq!(notifications[1].channel, "ch2");
498
499        // Queue should be empty now
500        assert!(conn.notifications().is_empty());
501    }
502
503    #[tokio::test]
504    async fn test_wait_for_notification_from_queue() {
505        let mut conn = make_connection(vec![]);
506
507        // Pre-buffer a notification
508        conn.notification_queue.push_back(Notification {
509            process_id: 42,
510            channel: "test".to_string(),
511            payload: "payload".to_string(),
512        });
513
514        // Should return immediately from the queue
515        let n = conn.wait_for_notification(None).await.unwrap();
516        assert!(n.is_some());
517        let n = n.unwrap();
518        assert_eq!(n.process_id, 42);
519        assert_eq!(n.channel, "test");
520        assert_eq!(n.payload, "payload");
521    }
522
523    #[tokio::test]
524    async fn test_wait_for_notification_from_server() {
525        let mut data = Vec::new();
526        // EmptyQueryResponse for the empty query
527        data.extend_from_slice(&[b'I', 0, 0, 0, 4]); // EmptyQueryResponse
528                                                     // NotificationResponse
529        data.extend_from_slice(&build_notification_response(99, "events", "user_login"));
530        // ReadyForQuery
531        data.extend_from_slice(&build_ready_for_query(b'I'));
532
533        let mut conn = make_connection(data);
534        let n = conn.wait_for_notification(None).await.unwrap();
535        assert!(n.is_some());
536        let n = n.unwrap();
537        assert_eq!(n.process_id, 99);
538        assert_eq!(n.channel, "events");
539        assert_eq!(n.payload, "user_login");
540    }
541
542    #[tokio::test]
543    async fn test_wait_for_notification_with_timeout_from_queue() {
544        let mut conn = make_connection(vec![]);
545
546        // Pre-buffer a notification
547        conn.notification_queue.push_back(Notification {
548            process_id: 7,
549            channel: "timeout_ch".to_string(),
550            payload: "timeout_payload".to_string(),
551        });
552
553        // Should return immediately from the queue, ignoring the timeout
554        let n = conn
555            .wait_for_notification_with_timeout(Duration::from_secs(60))
556            .await
557            .unwrap();
558        assert!(n.is_some());
559        let n = n.unwrap();
560        assert_eq!(n.process_id, 7);
561        assert_eq!(n.channel, "timeout_ch");
562        assert_eq!(n.payload, "timeout_payload");
563    }
564
565    #[tokio::test]
566    async fn test_wait_for_notification_zero_timeout() {
567        let mut conn = make_connection(vec![]);
568
569        // With zero timeout and empty queue, should return None immediately
570        let n = conn
571            .wait_for_notification(Some(Duration::ZERO))
572            .await
573            .unwrap();
574        assert!(n.is_none());
575    }
576
577    #[tokio::test]
578    async fn test_wait_for_notification_with_timeout_zero() {
579        let mut conn = make_connection(vec![]);
580
581        // wait_for_notification_with_timeout with zero duration
582        let n = conn
583            .wait_for_notification_with_timeout(Duration::ZERO)
584            .await
585            .unwrap();
586        assert!(n.is_none());
587    }
588}