Skip to main content

pg_proto/
demux.rs

1//! Filtering projection from backend messages to the typed session stream.
2
3use std::collections::{BTreeMap, VecDeque};
4
5use bytes::Bytes;
6
7use crate::codec::{BackendMessage, DiagnosticResponse, TransactionStatus};
8
9/// Position of a command within a connection's session.
10#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
11pub struct CommandIndex(pub u64);
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14/// A backend notice attributed to the command active when it arrived.
15pub struct TaggedNotice {
16    /// Command to which the notice belongs.
17    pub command: CommandIndex,
18    /// Structured notice fields.
19    pub fields: DiagnosticResponse,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
23/// A decoded asynchronous notification.
24pub struct Notification {
25    /// Process identifier of the notifying backend.
26    pub process_id: u32,
27    /// Notification channel.
28    pub channel: Bytes,
29    /// Notification payload.
30    pub payload: Bytes,
31}
32
33/// One ordered `ParameterStatus` update retained for proxy forwarding.
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ParameterStatus {
36    /// Parameter name.
37    pub name: Bytes,
38    /// Current parameter value.
39    pub value: Bytes,
40}
41
42/// A causally independent backend event retained in its original wire order.
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub enum AsyncEvent {
45    /// A positionally tagged notice.
46    Notice(TaggedNotice),
47    /// A run-time parameter update.
48    ParameterStatus(ParameterStatus),
49    /// A `LISTEN`/`NOTIFY` notification.
50    Notification(Notification),
51}
52
53/// Ordering and command attribution for an asynchronous backend event.
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct OrderedAsyncEvent {
56    /// Monotonic sequence number across all asynchronous event kinds.
57    pub sequence: u64,
58    /// Command active when the event arrived.
59    pub command: CommandIndex,
60    /// Decoded event.
61    pub event: AsyncEvent,
62}
63
64#[derive(Clone, Eq, Hash, PartialEq)]
65/// Backend cancellation credentials captured during startup.
66pub struct CancelKey {
67    /// Backend process identifier.
68    pub process_id: u32,
69    /// Opaque cancellation secret; its debug representation is redacted.
70    pub secret_key: Bytes,
71}
72
73impl std::fmt::Debug for CancelKey {
74    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        formatter
76            .debug_struct("CancelKey")
77            .field("process_id", &self.process_id)
78            .field("secret_key", &"[REDACTED]")
79            .finish()
80    }
81}
82
83/// A protocol-advancing message, optionally closing a command boundary.
84#[derive(Clone, Debug, Eq, PartialEq)]
85pub enum SessionItem {
86    /// An ordinary protocol-advancing backend message.
87    Message(BackendMessage),
88    /// Readiness together with pooling-relevant state accumulated by the demux.
89    ReadyForQuery {
90        /// Backend transaction status.
91        status: TransactionStatus,
92        /// Whether run-time parameters differ from their startup baseline.
93        parameters_changed: bool,
94    },
95    /// Command completion with notices accumulated since the previous boundary.
96    CommandComplete {
97        /// Backend command tag.
98        tag: Bytes,
99        /// Completed command's position in the session.
100        command: CommandIndex,
101        /// Notices attributed to this command.
102        notices: Vec<TaggedNotice>,
103    },
104}
105
106/// State owned below the typestate API for causally independent backend messages.
107#[derive(Debug, Default)]
108pub struct Demux {
109    command: CommandIndex,
110    pending_notices: Vec<TaggedNotice>,
111    notices: VecDeque<TaggedNotice>,
112    notifications: VecDeque<Notification>,
113    parameter_statuses: VecDeque<ParameterStatus>,
114    async_events: VecDeque<OrderedAsyncEvent>,
115    next_async_sequence: u64,
116    parameters: BTreeMap<Bytes, Bytes>,
117    startup_parameters: Option<BTreeMap<Bytes, Bytes>>,
118    parameters_changed: bool,
119    cancel_key: Option<CancelKey>,
120    transaction_status: Option<TransactionStatus>,
121}
122
123impl Demux {
124    /// Routes one decoded backend message.
125    ///
126    /// Async messages are consumed and recorded; only session-advancing messages
127    /// are returned.
128    pub fn route(&mut self, message: BackendMessage) -> Option<SessionItem> {
129        match message {
130            BackendMessage::NoticeResponse(fields) => {
131                let notice = TaggedNotice {
132                    command: self.command,
133                    fields,
134                };
135                self.pending_notices.push(notice.clone());
136                self.notices.push_back(notice.clone());
137                self.push_async(AsyncEvent::Notice(notice));
138                None
139            }
140            BackendMessage::ParameterStatus { name, value } => {
141                self.parameters.insert(name.clone(), value.clone());
142                let status = ParameterStatus { name, value };
143                self.parameter_statuses.push_back(status.clone());
144                self.push_async(AsyncEvent::ParameterStatus(status));
145                if let Some(startup_parameters) = &self.startup_parameters {
146                    self.parameters_changed = self.parameters != *startup_parameters;
147                }
148                None
149            }
150            BackendMessage::NotificationResponse {
151                process_id,
152                channel,
153                payload,
154            } => {
155                let notification = Notification {
156                    process_id,
157                    channel,
158                    payload,
159                };
160                self.notifications.push_back(notification.clone());
161                self.push_async(AsyncEvent::Notification(notification));
162                None
163            }
164            BackendMessage::BackendKeyData {
165                process_id,
166                secret_key,
167            } => {
168                self.cancel_key = Some(CancelKey {
169                    process_id,
170                    secret_key: secret_key.clone(),
171                });
172                Some(SessionItem::Message(BackendMessage::BackendKeyData {
173                    process_id,
174                    secret_key,
175                }))
176            }
177            BackendMessage::ReadyForQuery(status) => {
178                self.transaction_status = Some(status);
179                if self.startup_parameters.is_none() {
180                    self.startup_parameters = Some(self.parameters.clone());
181                }
182                Some(SessionItem::ReadyForQuery {
183                    status,
184                    parameters_changed: self.parameters_changed,
185                })
186            }
187            BackendMessage::CommandComplete(tag) => {
188                let command = self.command;
189                let notices = std::mem::take(&mut self.pending_notices);
190                self.command.0 = self.command.0.saturating_add(1);
191                Some(SessionItem::CommandComplete {
192                    tag,
193                    command,
194                    notices,
195                })
196            }
197            message => Some(SessionItem::Message(message)),
198        }
199    }
200
201    /// Returns the latest value of every reported run-time parameter.
202    #[must_use]
203    pub fn parameters(&self) -> &BTreeMap<Bytes, Bytes> {
204        &self.parameters
205    }
206
207    /// Reports whether parameters differ from the startup baseline.
208    #[must_use]
209    pub const fn parameters_changed(&self) -> bool {
210        self.parameters_changed
211    }
212
213    /// Returns the most recently received cancellation key, if any.
214    #[must_use]
215    pub const fn cancel_key(&self) -> Option<&CancelKey> {
216        self.cancel_key.as_ref()
217    }
218
219    /// Returns the latest backend transaction status, if readiness was observed.
220    #[must_use]
221    pub const fn transaction_status(&self) -> Option<TransactionStatus> {
222        self.transaction_status
223    }
224
225    /// Removes the next queued asynchronous notification.
226    pub fn pop_notification(&mut self) -> Option<Notification> {
227        self.notifications.pop_front()
228    }
229
230    /// Removes the next positionally tagged notice for prompt client forwarding.
231    pub fn pop_notice(&mut self) -> Option<TaggedNotice> {
232        self.notices.pop_front()
233    }
234
235    /// Removes the next ordered status update for forwarding to a client.
236    pub fn pop_parameter_status(&mut self) -> Option<ParameterStatus> {
237        self.parameter_statuses.pop_front()
238    }
239
240    /// Removes the next asynchronous event in original backend wire order.
241    pub fn pop_async_event(&mut self) -> Option<OrderedAsyncEvent> {
242        self.async_events.pop_front()
243    }
244
245    fn push_async(&mut self, event: AsyncEvent) {
246        let sequence = self.next_async_sequence;
247        self.next_async_sequence = self.next_async_sequence.saturating_add(1);
248        self.async_events.push_back(OrderedAsyncEvent {
249            sequence,
250            command: self.command,
251            event,
252        });
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn notices_are_attached_to_their_command_boundary() {
262        let mut demux = Demux::default();
263        assert_eq!(
264            demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
265                fields: vec![crate::codec::DiagnosticField {
266                    code: b'M',
267                    value: Bytes::from_static(b"notice"),
268                }],
269            })),
270            None
271        );
272        let completion = demux
273            .route(BackendMessage::CommandComplete(Bytes::from_static(
274                b"SELECT 1",
275            )))
276            .expect("command completion advances the session");
277        assert_eq!(
278            completion,
279            SessionItem::CommandComplete {
280                tag: Bytes::from_static(b"SELECT 1"),
281                command: CommandIndex(0),
282                notices: vec![TaggedNotice {
283                    command: CommandIndex(0),
284                    fields: DiagnosticResponse {
285                        fields: vec![crate::codec::DiagnosticField {
286                            code: b'M',
287                            value: Bytes::from_static(b"notice"),
288                        }],
289                    },
290                }],
291            }
292        );
293        assert_eq!(
294            demux.pop_notice(),
295            Some(TaggedNotice {
296                command: CommandIndex(0),
297                fields: DiagnosticResponse {
298                    fields: vec![crate::codec::DiagnosticField {
299                        code: b'M',
300                        value: Bytes::from_static(b"notice"),
301                    }],
302                },
303            })
304        );
305        assert_eq!(demux.pop_notice(), None);
306    }
307
308    #[test]
309    fn startup_parameters_establish_a_clean_baseline() {
310        let mut demux = Demux::default();
311        assert!(
312            demux
313                .route(BackendMessage::ParameterStatus {
314                    name: Bytes::from_static(b"client_encoding"),
315                    value: Bytes::from_static(b"UTF8"),
316                })
317                .is_none()
318        );
319        demux.route(BackendMessage::ReadyForQuery(TransactionStatus::Idle));
320        assert!(!demux.parameters_changed());
321
322        demux.route(BackendMessage::ParameterStatus {
323            name: Bytes::from_static(b"client_encoding"),
324            value: Bytes::from_static(b"LATIN1"),
325        });
326        assert!(demux.parameters_changed());
327    }
328
329    #[test]
330    fn parameter_statuses_remain_ordered_for_proxy_forwarding() {
331        let mut demux = Demux::default();
332        for (name, value) in [
333            (b"TimeZone".as_slice(), b"UTC".as_slice()),
334            (b"TimeZone", b"GMT"),
335        ] {
336            assert!(
337                demux
338                    .route(BackendMessage::ParameterStatus {
339                        name: Bytes::copy_from_slice(name),
340                        value: Bytes::copy_from_slice(value),
341                    })
342                    .is_none()
343            );
344        }
345
346        assert_eq!(
347            demux.pop_parameter_status(),
348            Some(ParameterStatus {
349                name: Bytes::from_static(b"TimeZone"),
350                value: Bytes::from_static(b"UTC"),
351            })
352        );
353        assert_eq!(
354            demux.pop_parameter_status(),
355            Some(ParameterStatus {
356                name: Bytes::from_static(b"TimeZone"),
357                value: Bytes::from_static(b"GMT"),
358            })
359        );
360        assert_eq!(demux.pop_parameter_status(), None);
361        assert_eq!(
362            demux.parameters().get(b"TimeZone".as_slice()),
363            Some(&Bytes::from_static(b"GMT"))
364        );
365    }
366
367    #[test]
368    fn notification_is_not_a_session_transition() {
369        let mut demux = Demux::default();
370        assert!(
371            demux
372                .route(BackendMessage::NotificationResponse {
373                    process_id: 42,
374                    channel: Bytes::from_static(b"events"),
375                    payload: Bytes::from_static(b"payload"),
376                })
377                .is_none()
378        );
379        assert_eq!(
380            demux.pop_notification(),
381            Some(Notification {
382                process_id: 42,
383                channel: Bytes::from_static(b"events"),
384                payload: Bytes::from_static(b"payload"),
385            })
386        );
387    }
388
389    #[test]
390    fn asynchronous_events_preserve_cross_kind_wire_order() {
391        let mut demux = Demux::default();
392        demux.route(BackendMessage::ParameterStatus {
393            name: Bytes::from_static(b"TimeZone"),
394            value: Bytes::from_static(b"UTC"),
395        });
396        demux.route(BackendMessage::NotificationResponse {
397            process_id: 7,
398            channel: Bytes::from_static(b"jobs"),
399            payload: Bytes::from_static(b"ready"),
400        });
401        demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
402            fields: vec![],
403        }));
404
405        let events = std::iter::from_fn(|| demux.pop_async_event()).collect::<Vec<_>>();
406        assert_eq!(events.len(), 3);
407        assert_eq!(events[0].sequence, 0);
408        assert!(matches!(events[0].event, AsyncEvent::ParameterStatus(_)));
409        assert_eq!(events[1].sequence, 1);
410        assert!(matches!(events[1].event, AsyncEvent::Notification(_)));
411        assert_eq!(events[2].sequence, 2);
412        assert!(matches!(events[2].event, AsyncEvent::Notice(_)));
413        assert!(events.iter().all(|event| event.command == CommandIndex(0)));
414    }
415}