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