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    /// Reports whether a backend message is causally independent of session progress.
125    ///
126    /// Pipeline orchestration uses this shared classifier rather than defining a
127    /// second asynchronous-message taxonomy.
128    #[must_use]
129    pub fn is_asynchronous(message: &BackendMessage) -> bool {
130        matches!(
131            message,
132            BackendMessage::NoticeResponse(_)
133                | BackendMessage::ParameterStatus { .. }
134                | BackendMessage::NotificationResponse { .. }
135                | BackendMessage::BackendKeyData { .. }
136        )
137    }
138
139    /// Routes one decoded backend message.
140    ///
141    /// Async messages are consumed and recorded; only session-advancing messages
142    /// are returned.
143    pub fn route(&mut self, message: BackendMessage) -> Option<SessionItem> {
144        match message {
145            BackendMessage::NoticeResponse(fields) => {
146                let notice = TaggedNotice {
147                    command: self.command,
148                    fields,
149                };
150                self.pending_notices.push(notice.clone());
151                self.notices.push_back(notice.clone());
152                self.push_async(AsyncEvent::Notice(notice));
153                None
154            }
155            BackendMessage::ParameterStatus { name, value } => {
156                self.parameters.insert(name.clone(), value.clone());
157                let status = ParameterStatus { name, value };
158                self.parameter_statuses.push_back(status.clone());
159                self.push_async(AsyncEvent::ParameterStatus(status));
160                if let Some(startup_parameters) = &self.startup_parameters {
161                    self.parameters_changed = self.parameters != *startup_parameters;
162                }
163                None
164            }
165            BackendMessage::NotificationResponse {
166                process_id,
167                channel,
168                payload,
169            } => {
170                let notification = Notification {
171                    process_id,
172                    channel,
173                    payload,
174                };
175                self.notifications.push_back(notification.clone());
176                self.push_async(AsyncEvent::Notification(notification));
177                None
178            }
179            BackendMessage::BackendKeyData {
180                process_id,
181                secret_key,
182            } => {
183                self.cancel_key = Some(CancelKey {
184                    process_id,
185                    secret_key: secret_key.clone(),
186                });
187                Some(SessionItem::Message(BackendMessage::BackendKeyData {
188                    process_id,
189                    secret_key,
190                }))
191            }
192            BackendMessage::ReadyForQuery(status) => {
193                self.transaction_status = Some(status);
194                if self.startup_parameters.is_none() {
195                    self.startup_parameters = Some(self.parameters.clone());
196                }
197                Some(SessionItem::ReadyForQuery {
198                    status,
199                    parameters_changed: self.parameters_changed,
200                })
201            }
202            BackendMessage::CommandComplete(tag) => {
203                let command = self.command;
204                let notices = std::mem::take(&mut self.pending_notices);
205                self.command.0 = self.command.0.saturating_add(1);
206                Some(SessionItem::CommandComplete {
207                    tag,
208                    command,
209                    notices,
210                })
211            }
212            message => Some(SessionItem::Message(message)),
213        }
214    }
215
216    /// Returns the latest value of every reported run-time parameter.
217    #[must_use]
218    pub fn parameters(&self) -> &BTreeMap<Bytes, Bytes> {
219        &self.parameters
220    }
221
222    /// Reports whether parameters differ from the startup baseline.
223    #[must_use]
224    pub const fn parameters_changed(&self) -> bool {
225        self.parameters_changed
226    }
227
228    /// Returns the most recently received cancellation key, if any.
229    #[must_use]
230    pub const fn cancel_key(&self) -> Option<&CancelKey> {
231        self.cancel_key.as_ref()
232    }
233
234    /// Returns the latest backend transaction status, if readiness was observed.
235    #[must_use]
236    pub const fn transaction_status(&self) -> Option<TransactionStatus> {
237        self.transaction_status
238    }
239
240    /// Removes the next queued asynchronous notification.
241    pub fn pop_notification(&mut self) -> Option<Notification> {
242        self.notifications.pop_front()
243    }
244
245    /// Removes the next positionally tagged notice for prompt client forwarding.
246    pub fn pop_notice(&mut self) -> Option<TaggedNotice> {
247        self.notices.pop_front()
248    }
249
250    /// Removes the next ordered status update for forwarding to a client.
251    pub fn pop_parameter_status(&mut self) -> Option<ParameterStatus> {
252        self.parameter_statuses.pop_front()
253    }
254
255    /// Removes the next asynchronous event in original backend wire order.
256    pub fn pop_async_event(&mut self) -> Option<OrderedAsyncEvent> {
257        self.async_events.pop_front()
258    }
259
260    fn push_async(&mut self, event: AsyncEvent) {
261        let sequence = self.next_async_sequence;
262        self.next_async_sequence = self.next_async_sequence.saturating_add(1);
263        self.async_events.push_back(OrderedAsyncEvent {
264            sequence,
265            command: self.command,
266            event,
267        });
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn notices_are_attached_to_their_command_boundary() {
277        let mut demux = Demux::default();
278        assert_eq!(
279            demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
280                fields: vec![crate::codec::DiagnosticField {
281                    code: b'M',
282                    value: Bytes::from_static(b"notice"),
283                }],
284            })),
285            None
286        );
287        let completion = demux
288            .route(BackendMessage::CommandComplete(Bytes::from_static(
289                b"SELECT 1",
290            )))
291            .expect("command completion advances the session");
292        assert_eq!(
293            completion,
294            SessionItem::CommandComplete {
295                tag: Bytes::from_static(b"SELECT 1"),
296                command: CommandIndex(0),
297                notices: vec![TaggedNotice {
298                    command: CommandIndex(0),
299                    fields: DiagnosticResponse {
300                        fields: vec![crate::codec::DiagnosticField {
301                            code: b'M',
302                            value: Bytes::from_static(b"notice"),
303                        }],
304                    },
305                }],
306            }
307        );
308        assert_eq!(
309            demux.pop_notice(),
310            Some(TaggedNotice {
311                command: CommandIndex(0),
312                fields: DiagnosticResponse {
313                    fields: vec![crate::codec::DiagnosticField {
314                        code: b'M',
315                        value: Bytes::from_static(b"notice"),
316                    }],
317                },
318            })
319        );
320        assert_eq!(demux.pop_notice(), None);
321    }
322
323    #[test]
324    fn startup_parameters_establish_a_clean_baseline() {
325        let mut demux = Demux::default();
326        assert!(
327            demux
328                .route(BackendMessage::ParameterStatus {
329                    name: Bytes::from_static(b"client_encoding"),
330                    value: Bytes::from_static(b"UTF8"),
331                })
332                .is_none()
333        );
334        demux.route(BackendMessage::ReadyForQuery(TransactionStatus::Idle));
335        assert!(!demux.parameters_changed());
336
337        demux.route(BackendMessage::ParameterStatus {
338            name: Bytes::from_static(b"client_encoding"),
339            value: Bytes::from_static(b"LATIN1"),
340        });
341        assert!(demux.parameters_changed());
342    }
343
344    #[test]
345    fn parameter_statuses_remain_ordered_for_proxy_forwarding() {
346        let mut demux = Demux::default();
347        for (name, value) in [
348            (b"TimeZone".as_slice(), b"UTC".as_slice()),
349            (b"TimeZone", b"GMT"),
350        ] {
351            assert!(
352                demux
353                    .route(BackendMessage::ParameterStatus {
354                        name: Bytes::copy_from_slice(name),
355                        value: Bytes::copy_from_slice(value),
356                    })
357                    .is_none()
358            );
359        }
360
361        assert_eq!(
362            demux.pop_parameter_status(),
363            Some(ParameterStatus {
364                name: Bytes::from_static(b"TimeZone"),
365                value: Bytes::from_static(b"UTC"),
366            })
367        );
368        assert_eq!(
369            demux.pop_parameter_status(),
370            Some(ParameterStatus {
371                name: Bytes::from_static(b"TimeZone"),
372                value: Bytes::from_static(b"GMT"),
373            })
374        );
375        assert_eq!(demux.pop_parameter_status(), None);
376        assert_eq!(
377            demux.parameters().get(b"TimeZone".as_slice()),
378            Some(&Bytes::from_static(b"GMT"))
379        );
380    }
381
382    #[test]
383    fn notification_is_not_a_session_transition() {
384        let mut demux = Demux::default();
385        assert!(
386            demux
387                .route(BackendMessage::NotificationResponse {
388                    process_id: 42,
389                    channel: Bytes::from_static(b"events"),
390                    payload: Bytes::from_static(b"payload"),
391                })
392                .is_none()
393        );
394        assert_eq!(
395            demux.pop_notification(),
396            Some(Notification {
397                process_id: 42,
398                channel: Bytes::from_static(b"events"),
399                payload: Bytes::from_static(b"payload"),
400            })
401        );
402    }
403
404    #[test]
405    fn asynchronous_events_preserve_cross_kind_wire_order() {
406        let mut demux = Demux::default();
407        demux.route(BackendMessage::ParameterStatus {
408            name: Bytes::from_static(b"TimeZone"),
409            value: Bytes::from_static(b"UTC"),
410        });
411        demux.route(BackendMessage::NotificationResponse {
412            process_id: 7,
413            channel: Bytes::from_static(b"jobs"),
414            payload: Bytes::from_static(b"ready"),
415        });
416        demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
417            fields: vec![],
418        }));
419
420        let events = std::iter::from_fn(|| demux.pop_async_event()).collect::<Vec<_>>();
421        assert_eq!(events.len(), 3);
422        assert_eq!(events[0].sequence, 0);
423        assert!(matches!(events[0].event, AsyncEvent::ParameterStatus(_)));
424        assert_eq!(events[1].sequence, 1);
425        assert!(matches!(events[1].event, AsyncEvent::Notification(_)));
426        assert_eq!(events[2].sequence, 2);
427        assert!(matches!(events[2].event, AsyncEvent::Notice(_)));
428        assert!(events.iter().all(|event| event.command == CommandIndex(0)));
429    }
430}