Skip to main content

pg_proto/
replication.rs

1//! Typed payloads carried inside walsender `CopyData` messages.
2
3use std::io;
4
5use bytes::{Buf, BufMut, Bytes, BytesMut};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8/// A typed payload sent by a walsender inside backend `CopyData`.
9pub enum BackendReplication {
10    /// A range of write-ahead log data.
11    XLogData {
12        /// WAL position of the first byte in `data`.
13        wal_start: u64,
14        /// Current end of WAL on the server.
15        wal_end: u64,
16        /// Server clock as microseconds since 2000-01-01 UTC.
17        server_time: i64,
18        /// WAL bytes.
19        data: Bytes,
20    },
21    /// A primary keepalive message.
22    PrimaryKeepalive {
23        /// Current end of WAL on the server.
24        wal_end: u64,
25        /// Server clock as microseconds since 2000-01-01 UTC.
26        server_time: i64,
27        /// Whether the server requests an immediate status reply.
28        reply_requested: bool,
29    },
30    /// An extension payload whose tag is not recognised by this crate.
31    Unknown {
32        /// Replication sub-message tag.
33        tag: u8,
34        /// Bytes following the tag.
35        body: Bytes,
36    },
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
40/// A typed payload sent by a standby inside frontend `CopyData`.
41pub enum FrontendReplication {
42    /// The standby's WAL receipt, flush, and replay positions.
43    StandbyStatus {
44        /// Last WAL position written locally.
45        written: u64,
46        /// Last WAL position flushed durably.
47        flushed: u64,
48        /// Last WAL position applied during replay.
49        applied: u64,
50        /// Client clock as microseconds since 2000-01-01 UTC.
51        client_time: i64,
52        /// Whether the standby requests an immediate keepalive reply.
53        reply_requested: bool,
54    },
55    /// Transaction horizons used to prevent premature vacuuming on the primary.
56    HotStandbyFeedback {
57        /// Client clock as microseconds since 2000-01-01 UTC.
58        client_time: i64,
59        /// Oldest transaction identifier still needed by the standby.
60        xmin: u32,
61        /// Epoch disambiguating wraparound of `xmin`.
62        xmin_epoch: u32,
63        /// Oldest catalog transaction identifier still needed by the standby.
64        catalog_xmin: u32,
65        /// Epoch disambiguating wraparound of `catalog_xmin`.
66        catalog_xmin_epoch: u32,
67    },
68    /// An extension payload whose tag is not recognised by this crate.
69    Unknown {
70        /// Replication sub-message tag.
71        tag: u8,
72        /// Bytes following the tag.
73        body: Bytes,
74    },
75}
76
77impl BackendReplication {
78    /// Decodes a backend walsender payload while preserving extension messages.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error for a truncated known message or an invalid Boolean byte.
83    pub fn decode(mut payload: Bytes) -> io::Result<Self> {
84        let tag = take_tag(&mut payload)?;
85        match tag {
86            b'w' => {
87                require(&payload, 24, "truncated XLogData")?;
88                Ok(Self::XLogData {
89                    wal_start: payload.get_u64(),
90                    wal_end: payload.get_u64(),
91                    server_time: payload.get_i64(),
92                    data: payload,
93                })
94            }
95            b'k' => {
96                require_exact(&payload, 17, "invalid primary keepalive length")?;
97                let wal_end = payload.get_u64();
98                let server_time = payload.get_i64();
99                let reply_requested = take_bool(payload.get_u8())?;
100                Ok(Self::PrimaryKeepalive {
101                    wal_end,
102                    server_time,
103                    reply_requested,
104                })
105            }
106            tag => Ok(Self::Unknown { tag, body: payload }),
107        }
108    }
109
110    #[must_use]
111    /// Encodes this value as a backend replication sub-message.
112    pub fn encode(&self) -> Bytes {
113        let mut output = BytesMut::new();
114        match self {
115            Self::XLogData {
116                wal_start,
117                wal_end,
118                server_time,
119                data,
120            } => {
121                output.put_u8(b'w');
122                output.put_u64(*wal_start);
123                output.put_u64(*wal_end);
124                output.put_i64(*server_time);
125                output.extend_from_slice(data);
126            }
127            Self::PrimaryKeepalive {
128                wal_end,
129                server_time,
130                reply_requested,
131            } => {
132                output.put_u8(b'k');
133                output.put_u64(*wal_end);
134                output.put_i64(*server_time);
135                output.put_u8(u8::from(*reply_requested));
136            }
137            Self::Unknown { tag, body } => {
138                output.put_u8(*tag);
139                output.extend_from_slice(body);
140            }
141        }
142        output.freeze()
143    }
144}
145
146impl FrontendReplication {
147    /// Decodes a standby payload while preserving extension messages.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error for a truncated known message or an invalid Boolean byte.
152    pub fn decode(mut payload: Bytes) -> io::Result<Self> {
153        let tag = take_tag(&mut payload)?;
154        match tag {
155            b'r' => {
156                require_exact(&payload, 33, "invalid standby status length")?;
157                let written = payload.get_u64();
158                let flushed = payload.get_u64();
159                let applied = payload.get_u64();
160                let client_time = payload.get_i64();
161                let reply_requested = take_bool(payload.get_u8())?;
162                Ok(Self::StandbyStatus {
163                    written,
164                    flushed,
165                    applied,
166                    client_time,
167                    reply_requested,
168                })
169            }
170            b'h' => {
171                require_exact(&payload, 24, "invalid hot standby feedback length")?;
172                Ok(Self::HotStandbyFeedback {
173                    client_time: payload.get_i64(),
174                    xmin: payload.get_u32(),
175                    xmin_epoch: payload.get_u32(),
176                    catalog_xmin: payload.get_u32(),
177                    catalog_xmin_epoch: payload.get_u32(),
178                })
179            }
180            tag => Ok(Self::Unknown { tag, body: payload }),
181        }
182    }
183
184    #[must_use]
185    /// Encodes this value as a frontend replication sub-message.
186    pub fn encode(&self) -> Bytes {
187        let mut output = BytesMut::new();
188        match self {
189            Self::StandbyStatus {
190                written,
191                flushed,
192                applied,
193                client_time,
194                reply_requested,
195            } => {
196                output.put_u8(b'r');
197                output.put_u64(*written);
198                output.put_u64(*flushed);
199                output.put_u64(*applied);
200                output.put_i64(*client_time);
201                output.put_u8(u8::from(*reply_requested));
202            }
203            Self::HotStandbyFeedback {
204                client_time,
205                xmin,
206                xmin_epoch,
207                catalog_xmin,
208                catalog_xmin_epoch,
209            } => {
210                output.put_u8(b'h');
211                output.put_i64(*client_time);
212                output.put_u32(*xmin);
213                output.put_u32(*xmin_epoch);
214                output.put_u32(*catalog_xmin);
215                output.put_u32(*catalog_xmin_epoch);
216            }
217            Self::Unknown { tag, body } => {
218                output.put_u8(*tag);
219                output.extend_from_slice(body);
220            }
221        }
222        output.freeze()
223    }
224}
225
226fn take_tag(payload: &mut Bytes) -> io::Result<u8> {
227    if payload.is_empty() {
228        Err(invalid("empty replication payload"))
229    } else {
230        Ok(payload.get_u8())
231    }
232}
233
234fn take_bool(value: u8) -> io::Result<bool> {
235    match value {
236        0 => Ok(false),
237        1 => Ok(true),
238        _ => Err(invalid("invalid replication Boolean")),
239    }
240}
241
242fn require(payload: &Bytes, minimum: usize, message: &'static str) -> io::Result<()> {
243    if payload.len() < minimum {
244        Err(invalid(message))
245    } else {
246        Ok(())
247    }
248}
249
250fn require_exact(payload: &Bytes, length: usize, message: &'static str) -> io::Result<()> {
251    if payload.len() == length {
252        Ok(())
253    } else {
254        Err(invalid(message))
255    }
256}
257
258fn invalid(message: &'static str) -> io::Error {
259    io::Error::new(io::ErrorKind::InvalidData, message)
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn physical_replication_messages_round_trip() {
268        let backend = [
269            BackendReplication::XLogData {
270                wal_start: 10,
271                wal_end: 20,
272                server_time: -30,
273                data: Bytes::from_static(b"wal"),
274            },
275            BackendReplication::PrimaryKeepalive {
276                wal_end: 40,
277                server_time: 50,
278                reply_requested: true,
279            },
280            BackendReplication::Unknown {
281                tag: b'z',
282                body: Bytes::from_static(b"extension"),
283            },
284        ];
285        for message in backend {
286            assert_eq!(
287                BackendReplication::decode(message.encode()).unwrap(),
288                message
289            );
290        }
291
292        let frontend = [
293            FrontendReplication::StandbyStatus {
294                written: 10,
295                flushed: 20,
296                applied: 30,
297                client_time: 40,
298                reply_requested: false,
299            },
300            FrontendReplication::HotStandbyFeedback {
301                client_time: 50,
302                xmin: 60,
303                xmin_epoch: 70,
304                catalog_xmin: 80,
305                catalog_xmin_epoch: 90,
306            },
307            FrontendReplication::Unknown {
308                tag: b'z',
309                body: Bytes::from_static(b"extension"),
310            },
311        ];
312        for message in frontend {
313            assert_eq!(
314                FrontendReplication::decode(message.encode()).unwrap(),
315                message
316            );
317        }
318    }
319
320    #[test]
321    fn known_messages_reject_invalid_shapes() {
322        assert!(BackendReplication::decode(Bytes::from_static(b"kshort")).is_err());
323        let mut invalid_bool = BytesMut::new();
324        invalid_bool.put_u8(b'k');
325        invalid_bool.extend_from_slice(&[0; 16]);
326        invalid_bool.put_u8(2);
327        assert!(BackendReplication::decode(invalid_bool.freeze()).is_err());
328    }
329}