Skip to main content

qail_pg/driver/
notification.rs

1//! LISTEN/NOTIFY support for PostgreSQL connections.
2//!
3//! PostgreSQL sends `NotificationResponse` messages asynchronously when
4//! a channel the connection is LISTENing on receives a NOTIFY.
5//!
6//! This module provides:
7//! - `Notification` struct — channel name + payload
8//! - `listen()` / `unlisten()` — subscribe/unsubscribe to channels
9//! - `poll_notifications()` — drain buffered notifications (non-blocking)
10//! - `recv_notification()` — block-wait for the next notification
11
12use super::{
13    PgConnection, PgError, PgResult, io::MAX_MESSAGE_SIZE, is_ignorable_session_message,
14    unexpected_backend_message,
15};
16use crate::protocol::PgEncoder;
17
18/// A notification received from PostgreSQL LISTEN/NOTIFY.
19#[derive(Debug, Clone)]
20pub struct Notification {
21    /// The PID of the notifying backend process
22    pub process_id: i32,
23    /// The channel name
24    pub channel: String,
25    /// The payload (may be empty)
26    pub payload: String,
27}
28
29#[inline]
30fn return_with_desync<T>(conn: &mut PgConnection, err: PgError) -> PgResult<T> {
31    if matches!(
32        err,
33        PgError::Protocol(_) | PgError::Connection(_) | PgError::Timeout(_)
34    ) {
35        conn.mark_io_desynced();
36    }
37    Err(err)
38}
39
40impl PgConnection {
41    /// Subscribe to a notification channel.
42    ///
43    /// ```ignore
44    /// conn.listen("price_calendar_changed").await?;
45    /// ```
46    pub async fn listen(&mut self, channel: &str) -> PgResult<()> {
47        // Channel names are identifiers, quote them to prevent injection
48        let sql = format!("LISTEN \"{}\"", channel.replace('"', "\"\""));
49        self.execute_simple(&sql).await
50    }
51
52    /// Unsubscribe from a notification channel.
53    pub async fn unlisten(&mut self, channel: &str) -> PgResult<()> {
54        let sql = format!("UNLISTEN \"{}\"", channel.replace('"', "\"\""));
55        self.execute_simple(&sql).await
56    }
57
58    /// Unsubscribe from all notification channels.
59    pub async fn unlisten_all(&mut self) -> PgResult<()> {
60        self.execute_simple("UNLISTEN *").await
61    }
62
63    /// Drain all buffered notifications without blocking.
64    ///
65    /// Notifications arrive asynchronously from PostgreSQL and are buffered
66    /// whenever `recv()` encounters a `NotificationResponse`. This method
67    /// returns all currently buffered notifications.
68    pub fn poll_notifications(&mut self) -> Vec<Notification> {
69        self.notifications.drain(..).collect()
70    }
71
72    /// Wait for the next notification, blocking until one arrives.
73    ///
74    /// Unlike `recv()`, this does NOT use the 30-second Slowloris timeout
75    /// guard. LISTEN connections idle for long periods — that's normal,
76    /// not a DoS attack.
77    ///
78    /// Useful for a dedicated LISTEN connection in a background task.
79    pub async fn recv_notification(&mut self) -> PgResult<Notification> {
80        use crate::protocol::BackendMessage;
81
82        // Return buffered notification immediately if available
83        if let Some(n) = self.notifications.pop_front() {
84            return Ok(n);
85        }
86
87        // Send empty query to flush any pending notifications from server
88        let bytes = PgEncoder::try_encode_query_string("")?;
89        self.send_bytes(&bytes).await?;
90
91        // Read messages — use recv() for the initial empty query response
92        // (which completes quickly), then switch to no-timeout reads
93        let mut got_ready = false;
94        loop {
95            // Try to decode from the existing buffer first
96            if self.buffer.len() >= 5 {
97                let msg_len = u32::from_be_bytes([
98                    self.buffer[1],
99                    self.buffer[2],
100                    self.buffer[3],
101                    self.buffer[4],
102                ]) as usize;
103
104                if msg_len < 4 {
105                    return return_with_desync(
106                        self,
107                        PgError::Protocol(format!(
108                            "Invalid message length: {} (minimum 4)",
109                            msg_len
110                        )),
111                    );
112                }
113
114                if msg_len > MAX_MESSAGE_SIZE {
115                    return return_with_desync(
116                        self,
117                        PgError::Protocol(format!(
118                            "Message too large: {} bytes (max {})",
119                            msg_len, MAX_MESSAGE_SIZE
120                        )),
121                    );
122                }
123
124                if self.buffer.len() > msg_len {
125                    let msg_bytes = self.buffer.split_to(msg_len + 1);
126                    let (msg, _) = match BackendMessage::decode(&msg_bytes) {
127                        Ok(decoded) => decoded,
128                        Err(err) => return return_with_desync(self, PgError::Protocol(err)),
129                    };
130
131                    match msg {
132                        BackendMessage::NotificationResponse {
133                            process_id,
134                            channel,
135                            payload,
136                        } => {
137                            let notification = Notification {
138                                process_id,
139                                channel,
140                                payload,
141                            };
142                            if got_ready {
143                                return Ok(notification);
144                            }
145                            self.notifications.push_back(notification);
146                            continue;
147                        }
148                        BackendMessage::EmptyQueryResponse => continue,
149                        BackendMessage::NoticeResponse(_) => continue,
150                        BackendMessage::ParameterStatus { .. } => continue,
151                        BackendMessage::CommandComplete(_) => continue,
152                        BackendMessage::ReadyForQuery(_) => {
153                            got_ready = true;
154                            // Check buffer for notifications that arrived with this batch
155                            if let Some(n) = self.notifications.pop_front() {
156                                return Ok(n);
157                            }
158                            continue;
159                        }
160                        BackendMessage::ErrorResponse(err) => {
161                            self.mark_io_desynced();
162                            return Err(PgError::QueryServer(err.into()));
163                        }
164                        msg if is_ignorable_session_message(&msg) => continue,
165                        other => {
166                            return return_with_desync(
167                                self,
168                                unexpected_backend_message("listen/notify wait", &other),
169                            );
170                        }
171                    }
172                }
173            }
174
175            // Read from socket — use tokio read (no timeout!) if we've
176            // already gotten ReadyForQuery (now we're just waiting for NOTIFY)
177            if self.buffer.capacity() - self.buffer.len() < 65536 {
178                self.buffer.reserve(131072);
179            }
180
181            if got_ready {
182                // LISTEN connections can stay idle for hours (empty buffer),
183                // but a partially buffered backend frame should still timeout
184                // to fail-closed on slowloris-style partial writes.
185                let n = if self.buffer.is_empty() {
186                    self.read_without_timeout().await?
187                } else {
188                    self.read_with_timeout().await?
189                };
190                if n == 0 {
191                    return return_with_desync(
192                        self,
193                        PgError::Connection("Connection closed".to_string()),
194                    );
195                }
196            } else {
197                // Initial flush — use the normal timeout to avoid hanging
198                // if the server is unresponsive during the empty query
199                let n = self.read_with_timeout().await?;
200                if n == 0 {
201                    return return_with_desync(
202                        self,
203                        PgError::Connection("Connection closed".to_string()),
204                    );
205                }
206            }
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::return_with_desync;
214    use crate::driver::{PgConnection, PgError};
215
216    #[cfg(unix)]
217    fn test_conn_with_peer() -> (PgConnection, tokio::net::UnixStream) {
218        use crate::driver::connection::StatementCache;
219        use crate::driver::stream::PgStream;
220        use bytes::BytesMut;
221        use std::collections::{HashMap, VecDeque};
222        use std::num::NonZeroUsize;
223        use tokio::net::UnixStream;
224
225        let (unix_stream, peer) = UnixStream::pair().expect("unix stream pair");
226        (
227            PgConnection {
228                stream: PgStream::Unix(unix_stream),
229                buffer: BytesMut::with_capacity(1024),
230                write_buf: BytesMut::with_capacity(1024),
231                sql_buf: BytesMut::with_capacity(256),
232                params_buf: Vec::new(),
233                prepared_statements: HashMap::new(),
234                stmt_cache: StatementCache::new(NonZeroUsize::new(2).expect("non-zero")),
235                column_info_cache: HashMap::new(),
236                process_id: 0,
237                cancel_key_bytes: Vec::new(),
238                requested_protocol_minor: PgConnection::default_protocol_minor(),
239                negotiated_protocol_minor: PgConnection::default_protocol_minor(),
240                notifications: VecDeque::new(),
241                replication_stream_active: false,
242                replication_mode_enabled: false,
243                last_replication_wal_end: None,
244                io_desynced: false,
245                pending_statement_closes: Vec::new(),
246                draining_statement_closes: false,
247            },
248            peer,
249        )
250    }
251
252    #[cfg(unix)]
253    fn test_conn() -> PgConnection {
254        test_conn_with_peer().0
255    }
256
257    #[cfg(unix)]
258    fn push_backend_frame(conn: &mut PgConnection, msg_type: u8, payload: &[u8]) {
259        conn.buffer.extend_from_slice(&[msg_type]);
260        conn.buffer
261            .extend_from_slice(&((payload.len() + 4) as u32).to_be_bytes());
262        conn.buffer.extend_from_slice(payload);
263    }
264
265    #[cfg(unix)]
266    fn notification_payload(process_id: i32, channel: &str, payload: &str) -> Vec<u8> {
267        let mut bytes = Vec::new();
268        bytes.extend_from_slice(&process_id.to_be_bytes());
269        bytes.extend_from_slice(channel.as_bytes());
270        bytes.push(0);
271        bytes.extend_from_slice(payload.as_bytes());
272        bytes.push(0);
273        bytes
274    }
275
276    fn error_response_payload(code: &str, message: &str) -> Vec<u8> {
277        let mut payload = Vec::new();
278        payload.push(b'S');
279        payload.extend_from_slice(b"ERROR\0");
280        payload.push(b'C');
281        payload.extend_from_slice(code.as_bytes());
282        payload.push(0);
283        payload.push(b'M');
284        payload.extend_from_slice(message.as_bytes());
285        payload.push(0);
286        payload.push(0);
287        payload
288    }
289
290    #[cfg(unix)]
291    #[tokio::test]
292    async fn notification_return_with_desync_marks_protocol_error() {
293        let mut conn = test_conn();
294
295        let err =
296            return_with_desync::<()>(&mut conn, PgError::Protocol("bad notify frame".to_string()))
297                .expect_err("protocol error must be returned");
298
299        assert!(err.to_string().contains("bad notify frame"));
300        assert!(conn.is_io_desynced());
301    }
302
303    #[cfg(unix)]
304    #[tokio::test]
305    async fn recv_notification_drains_empty_query_before_returning_pre_ready_notify() {
306        let (mut conn, _peer) = test_conn_with_peer();
307        let payload = notification_payload(42, "jobs", "ready");
308
309        push_backend_frame(&mut conn, b'A', &payload);
310        push_backend_frame(&mut conn, b'I', &[]);
311        push_backend_frame(&mut conn, b'Z', b"I");
312
313        let notification = conn
314            .recv_notification()
315            .await
316            .expect("pre-ready notification should be returned after flush drain");
317
318        assert_eq!(notification.process_id, 42);
319        assert_eq!(notification.channel, "jobs");
320        assert_eq!(notification.payload, "ready");
321        assert!(
322            conn.buffer.is_empty(),
323            "empty-query flush frames must not remain buffered"
324        );
325        assert!(!conn.is_io_desynced());
326    }
327
328    #[cfg(unix)]
329    #[tokio::test]
330    async fn recv_notification_error_response_marks_connection_desynced() {
331        let (mut conn, _peer) = test_conn_with_peer();
332        let payload = error_response_payload("XX000", "notify wait failed");
333        push_backend_frame(&mut conn, b'E', &payload);
334
335        let err = conn
336            .recv_notification()
337            .await
338            .expect_err("server error must fail");
339
340        assert!(matches!(err, PgError::QueryServer(_)));
341        assert!(conn.is_io_desynced());
342    }
343}