Skip to main content

pgwire_replication/client/
worker.rs

1use bytes::Bytes;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4use tokio::io::{AsyncRead, AsyncWrite, BufReader};
5use tokio::sync::{mpsc, watch};
6use tokio::time::Instant;
7
8use super::metrics::ReplicationMetrics;
9use crate::config::ReplicationConfig;
10use crate::error::{PgWireError, Result};
11use crate::lsn::Lsn;
12use crate::protocol::framing::{
13    read_backend_message, write_copy_data, write_copy_done, write_password_message, write_query,
14    write_startup_message, MessageReader,
15};
16use crate::protocol::messages::{parse_auth_request, parse_error_response};
17use crate::protocol::replication::{
18    encode_standby_status_update, parse_copy_data, ReplicationCopyData, PG_EPOCH_MICROS,
19};
20
21/// Shared replication progress updated by the consumer and read by the worker.
22///
23/// Stored as an AtomicU64 so progress updates are cheap and monotonic
24/// without async backpressure.
25pub struct SharedProgress {
26    applied: AtomicU64,
27}
28
29impl SharedProgress {
30    pub fn new(start: Lsn) -> Self {
31        Self {
32            applied: AtomicU64::new(start.as_u64()),
33        }
34    }
35
36    #[inline]
37    pub fn load_applied(&self) -> Lsn {
38        Lsn::from_u64(self.applied.load(Ordering::Acquire))
39    }
40
41    /// Monotonic update: if `lsn` is lower than the currently stored applied LSN,
42    /// this is a no-op.
43    #[inline]
44    pub fn update_applied(&self, lsn: Lsn) {
45        let new = lsn.as_u64();
46        let mut cur = self.applied.load(Ordering::Relaxed);
47
48        while new > cur {
49            match self
50                .applied
51                .compare_exchange_weak(cur, new, Ordering::Release, Ordering::Relaxed)
52            {
53                Ok(_) => break,
54                Err(observed) => cur = observed,
55            }
56        }
57    }
58}
59
60/// Events emitted by the replication worker.
61#[derive(Debug, Clone)]
62pub enum ReplicationEvent {
63    /// Server heartbeat message.
64    KeepAlive {
65        /// Current server WAL end position
66        wal_end: Lsn,
67        /// Whether server requested a reply (already handled internally)
68        reply_requested: bool,
69        /// Server timestamp (microseconds since 2000-01-01)
70        server_time_micros: i64,
71    },
72
73    /// Start of a transaction (pgoutput Begin message).
74    Begin {
75        final_lsn: Lsn,
76        xid: u32,
77        commit_time_micros: i64,
78    },
79
80    /// WAL data containing transaction changes.
81    XLogData {
82        /// WAL position where this data starts
83        wal_start: Lsn,
84        /// WAL end position (may be 0 for mid-transaction messages)
85        wal_end: Lsn,
86        /// Server timestamp (microseconds since 2000-01-01)
87        server_time_micros: i64,
88        /// pgoutput-encoded change data
89        data: Bytes,
90    },
91
92    /// End of a transaction (pgoutput Commit message).
93    Commit {
94        lsn: Lsn,
95        end_lsn: Lsn,
96        commit_time_micros: i64,
97    },
98
99    /// Logical decoding message emitted via `pg_logical_emit_message()`.
100    ///
101    /// Transactional messages are delivered only after the enclosing
102    /// transaction commits. Non-transactional messages are delivered
103    /// immediately.
104    Message {
105        /// Whether the message was emitted inside a transaction.
106        transactional: bool,
107        /// LSN of the message in the WAL.
108        lsn: Lsn,
109        /// Application-defined message prefix (e.g. `"myapp.checkpoint"`).
110        prefix: String,
111        /// Raw message content bytes.
112        content: Bytes,
113    },
114
115    /// Emitted when `stop_at_lsn` has been reached.
116    ///
117    /// After this event, no more events will be emitted and the
118    /// replication stream will be closed.
119    StoppedAt {
120        /// The LSN that triggered the stop condition
121        reached: Lsn,
122    },
123}
124
125/// Channel receiver type for replication events.
126pub type ReplicationEventReceiver =
127    mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>;
128
129/// Internal worker state.
130pub struct WorkerState {
131    cfg: ReplicationConfig,
132    progress: Arc<SharedProgress>,
133    stop_rx: watch::Receiver<bool>,
134    out: mpsc::Sender<std::result::Result<ReplicationEvent, PgWireError>>,
135    metrics: Arc<ReplicationMetrics>,
136}
137
138impl WorkerState {
139    pub fn new(
140        cfg: ReplicationConfig,
141        progress: Arc<SharedProgress>,
142        stop_rx: watch::Receiver<bool>,
143        out: mpsc::Sender<std::result::Result<ReplicationEvent, PgWireError>>,
144        metrics: Arc<ReplicationMetrics>,
145    ) -> Self {
146        Self {
147            cfg,
148            progress,
149            stop_rx,
150            out,
151            metrics,
152        }
153    }
154
155    /// Run the replication protocol on the given stream.
156    pub async fn run_on_stream<S: AsyncRead + AsyncWrite + Unpin>(
157        &mut self,
158        stream: &mut S,
159    ) -> Result<()> {
160        // Wrap in a 128KB read buffer to batch multiple WAL messages into fewer
161        // recv() syscalls. BufReader delegates AsyncWrite to the inner stream,
162        // so writes (standby status replies, etc.) are unaffected.
163        let mut stream = BufReader::with_capacity(128 * 1024, stream);
164        self.startup(&mut stream).await?;
165        self.authenticate(&mut stream).await?;
166        self.start_replication(&mut stream).await?;
167        self.stream_loop(&mut stream).await
168    }
169
170    /// Send startup message with replication parameters.
171    async fn startup<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<()> {
172        let mut params = vec![
173            ("user", self.cfg.user.as_str()),
174            ("database", self.cfg.database.as_str()),
175            ("replication", "database"),
176            ("client_encoding", "UTF8"),
177            ("application_name", "pgwire-replication"),
178        ];
179        // PostgreSQL honors `options` on replication connections the same as on
180        // ordinary ones, so this lets callers pin session GUCs (e.g. `DateStyle`,
181        // `IntervalStyle`) the way they already can via a normal libpq `options`
182        // parameter — only sent when the caller actually set one.
183        if let Some(options) = self.cfg.options.as_deref() {
184            params.push(("options", options));
185        }
186        write_startup_message(stream, 196608, &params).await
187    }
188
189    /// Start the logical replication stream.
190    async fn start_replication<S: AsyncRead + AsyncWrite + Unpin>(
191        &self,
192        stream: &mut S,
193    ) -> Result<()> {
194        let binary_opt = if self.cfg.binary {
195            ", binary 'true'"
196        } else {
197            ""
198        };
199        let sql = format!(
200            "START_REPLICATION SLOT {} LOGICAL {} \
201            (proto_version '1', publication_names '{}', messages 'true'{})",
202            self.cfg.slot,
203            self.cfg.start_lsn,
204            self.cfg.publication.to_option_value(),
205            binary_opt,
206        );
207        write_query(stream, &sql).await?;
208
209        // Wait for CopyBothResponse
210        loop {
211            let msg = read_backend_message(stream).await?;
212            match msg.tag {
213                b'W' => return Ok(()), // CopyBothResponse - ready to stream
214                b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
215                b'N' | b'S' | b'K' => continue, // Notice, ParameterStatus, BackendKeyData
216                _ => continue,
217            }
218        }
219    }
220
221    /// Main replication streaming loop.
222    ///
223    /// Uses a two-phase approach for throughput:
224    /// 1. **Drain phase**: while the BufReader has buffered data, read messages
225    ///    in a tight loop without `select!` or timeout overhead.
226    /// 2. **Wait phase**: when the buffer is empty, fall back to `select!` with
227    ///    timeout + stop signal to handle idle keepalives and graceful shutdown.
228    ///
229    /// Reads use [`MessageReader`], which preserves partial-read state across
230    /// dropped futures so the wait-phase `select!` is cancellation-safe.
231    async fn stream_loop<S: AsyncRead + AsyncWrite + Unpin>(
232        &mut self,
233        stream: &mut BufReader<S>,
234    ) -> Result<()> {
235        let mut last_status_sent = Instant::now() - self.cfg.status_interval;
236        let mut last_applied = self.progress.load_applied();
237        // Cancellation-safe message reader, partial reads survive dropped futures.
238        let mut reader = MessageReader::new();
239        // How many messages to process in the tight loop before checking
240        // stop signal and sending periodic status feedback.
241        const DRAIN_BATCH: usize = 256;
242
243        loop {
244            // Update applied LSN from client
245            let current_applied = self.progress.load_applied();
246            if current_applied != last_applied {
247                last_applied = current_applied;
248            }
249
250            // Send periodic status feedback
251            if last_status_sent.elapsed() >= self.cfg.status_interval {
252                self.send_feedback(stream, last_applied, false).await?;
253                last_status_sent = Instant::now();
254            }
255
256            // ── Drain phase: tight loop while BufReader has buffered data ──
257            // The BufReader has a 128KB internal buffer. When the kernel delivers
258            // a large TCP segment, many WAL messages are available without syscalls.
259            // Read them in a tight loop to avoid select!/timeout overhead per message.
260            let mut drained = 0usize;
261            while stream.buffer().len() >= 5 && drained < DRAIN_BATCH {
262                let msg = reader.read(stream).await?;
263                drained += 1;
264                if msg.tag == b'E' {
265                    return Err(PgWireError::Server(parse_error_response(&msg.payload)));
266                }
267                if msg.tag == b'd'
268                    && self
269                        .handle_copy_data(
270                            stream,
271                            msg.payload,
272                            &mut last_applied,
273                            &mut last_status_sent,
274                        )
275                        .await?
276                {
277                    return Ok(());
278                }
279            }
280
281            // If we drained messages, loop back to check stop/status before
282            // potentially blocking on the next read.
283            if drained > 0 {
284                // Check stop signal without blocking
285                if self.stop_rx.has_changed().unwrap_or(false) && *self.stop_rx.borrow() {
286                    let _ = write_copy_done(stream).await;
287                    return Ok(());
288                }
289                continue;
290            }
291
292            // ── Wait phase: buffer empty, need to wait for socket data ──
293            //
294            // Both `stop_rx.changed()` and the timeout can drop the read future
295            // mid-message. `MessageReader::read` is cancellation-safe — partial
296            // header/payload state lives on `reader` and is preserved across the
297            // drop, so the next iteration resumes the read without losing bytes.
298            let msg = tokio::select! {
299                biased;
300
301                _ = self.stop_rx.changed() => {
302                    if *self.stop_rx.borrow() {
303                        let _ = write_copy_done(stream).await;
304                        return Ok(());
305                    }
306                    continue;
307                }
308
309                msg_result = tokio::time::timeout(
310                    self.cfg.idle_wakeup_interval,
311                    reader.read(stream),
312                ) => {
313                    match msg_result {
314                        Ok(res) => res?,
315                        Err(_) => {
316                            let applied = self.progress.load_applied();
317                            last_applied = applied;
318                            self.send_feedback(stream, applied, false).await?;
319                            last_status_sent = Instant::now();
320                            continue;
321                        }
322                    }
323                }
324            };
325
326            if msg.tag == b'E' {
327                return Err(PgWireError::Server(parse_error_response(&msg.payload)));
328            }
329            if msg.tag == b'd'
330                && self
331                    .handle_copy_data(
332                        stream,
333                        msg.payload,
334                        &mut last_applied,
335                        &mut last_status_sent,
336                    )
337                    .await?
338            {
339                return Ok(());
340            }
341        }
342    }
343
344    /// Handle a CopyData message. Returns true if we should stop.
345    async fn handle_copy_data<S: AsyncRead + AsyncWrite + Unpin>(
346        &mut self,
347        stream: &mut BufReader<S>,
348        payload: Bytes,
349        last_applied: &mut Lsn,
350        last_status_sent: &mut Instant,
351    ) -> Result<bool> {
352        let cd = parse_copy_data(payload)?;
353
354        match cd {
355            ReplicationCopyData::KeepAlive {
356                wal_end,
357                server_time_micros,
358                reply_requested,
359            } => {
360                self.metrics.set_last_wal_end(wal_end);
361
362                // Respond immediately if server requests it
363                if reply_requested {
364                    let applied = self.progress.load_applied();
365                    *last_applied = applied;
366                    self.send_feedback(stream, applied, true).await?;
367                    *last_status_sent = Instant::now();
368                    self.metrics.record_keepalive_reply();
369                }
370
371                if self
372                    .send_event_backpressured(
373                        stream,
374                        Ok(ReplicationEvent::KeepAlive {
375                            wal_end,
376                            reply_requested,
377                            server_time_micros,
378                        }),
379                        last_status_sent,
380                    )
381                    .await?
382                {
383                    return Ok(true);
384                }
385
386                Ok(false)
387            }
388            ReplicationCopyData::XLogData {
389                wal_start,
390                wal_end,
391                server_time_micros,
392                data,
393            } => {
394                self.metrics.set_last_wal_end(wal_end);
395
396                // If the payload is a pgoutput Begin/Commit message, emit only the boundary event.
397                if let Some(boundary_ev) = parse_pgoutput_boundary(&data)? {
398                    let reached_lsn = match boundary_ev {
399                        ReplicationEvent::Begin { final_lsn, .. } => final_lsn,
400                        ReplicationEvent::Commit { end_lsn, .. } => end_lsn,
401                        _ => wal_end, // should never happen if parser only returns Begin/Commit
402                    };
403
404                    if self
405                        .send_event_backpressured(stream, Ok(boundary_ev), last_status_sent)
406                        .await?
407                    {
408                        return Ok(true);
409                    }
410
411                    // Stop condition (prefer boundary LSN semantics when available)
412                    if let Some(stop_lsn) = self.cfg.stop_at_lsn {
413                        if reached_lsn >= stop_lsn {
414                            if self
415                                .send_event_backpressured(
416                                    stream,
417                                    Ok(ReplicationEvent::StoppedAt {
418                                        reached: reached_lsn,
419                                    }),
420                                    last_status_sent,
421                                )
422                                .await?
423                            {
424                                return Ok(true);
425                            }
426                            let _ = write_copy_done(stream).await;
427                            return Ok(true); // should stop.
428                        }
429                    }
430
431                    return Ok(false);
432                }
433                // Otherwise, emit raw payload
434                // Check stop condition
435                if let Some(stop_lsn) = self.cfg.stop_at_lsn {
436                    if wal_end >= stop_lsn {
437                        // Send final event, then stop signal
438                        if self
439                            .send_event_backpressured(
440                                stream,
441                                Ok(ReplicationEvent::XLogData {
442                                    wal_start,
443                                    wal_end,
444                                    server_time_micros,
445                                    data,
446                                }),
447                                last_status_sent,
448                            )
449                            .await?
450                        {
451                            return Ok(true);
452                        }
453
454                        if self
455                            .send_event_backpressured(
456                                stream,
457                                Ok(ReplicationEvent::StoppedAt { reached: wal_end }),
458                                last_status_sent,
459                            )
460                            .await?
461                        {
462                            return Ok(true);
463                        }
464
465                        let _ = write_copy_done(stream).await;
466                        return Ok(true);
467                    }
468                }
469
470                if self
471                    .send_event_backpressured(
472                        stream,
473                        Ok(ReplicationEvent::XLogData {
474                            wal_start,
475                            wal_end,
476                            server_time_micros,
477                            data,
478                        }),
479                        last_status_sent,
480                    )
481                    .await?
482                {
483                    return Ok(true);
484                }
485
486                Ok(false)
487            }
488        }
489    }
490
491    /// Forward an event to the consumer, keeping standby feedback alive under
492    /// backpressure.
493    ///
494    /// Fast path: if the channel has capacity, send immediately — no await, no
495    /// stall. When the channel is full we must **not** block silently on
496    /// `send().await`: that would park the worker and starve standby-status
497    /// feedback until `wal_sender_timeout` fires and the server resets the
498    /// connection. Instead we wait for capacity while continuing to emit
499    /// feedback every `status_interval`, so the server keeps seeing us alive.
500    /// This does not falsely acknowledge data — feedback reports the unchanged
501    /// applied LSN — and real backpressure still propagates via TCP flow
502    /// control (we stop reading the socket while parked here).
503    ///
504    /// Returns `Ok(true)` if a stop was requested while waiting (the caller
505    /// should unwind), or `Ok(false)` once the event is delivered or the
506    /// channel is closed.
507    async fn send_event_backpressured<S: AsyncWrite + Unpin>(
508        &mut self,
509        stream: &mut S,
510        event: std::result::Result<ReplicationEvent, PgWireError>,
511        last_status_sent: &mut Instant,
512    ) -> Result<bool> {
513        // Fast path: capacity available.
514        match self.out.try_reserve() {
515            Ok(permit) => {
516                permit.send(event);
517                self.metrics.record_event_forwarded();
518                return Ok(false);
519            }
520            Err(mpsc::error::TrySendError::Closed(())) => {
521                tracing::debug!("event channel closed, client may have disconnected");
522                return Ok(false);
523            }
524            // Channel full: fall through to the backpressure wait loop. `event`
525            // is untouched (try_reserve carries no value), so we still own it.
526            Err(mpsc::error::TrySendError::Full(())) => {}
527        }
528
529        // Counted here, at the start, so an ongoing stall is visible even while
530        // the worker is still parked below awaiting capacity.
531        self.metrics.record_stall_begin();
532        let stall_start = Instant::now();
533        loop {
534            let deadline = *last_status_sent + self.cfg.status_interval;
535            tokio::select! {
536                biased;
537
538                _ = self.stop_rx.changed() => {
539                    if *self.stop_rx.borrow() {
540                        let _ = write_copy_done(stream).await;
541                        return Ok(true);
542                    }
543                }
544
545                permit = self.out.reserve() => {
546                    self.metrics
547                        .record_stall_end(stall_start.elapsed().as_micros() as u64);
548                    match permit {
549                        Ok(permit) => {
550                            permit.send(event);
551                            self.metrics.record_event_forwarded();
552                        }
553                        Err(_closed) => {
554                            tracing::debug!("event channel closed while backpressured");
555                        }
556                    }
557                    return Ok(false);
558                }
559
560                _ = tokio::time::sleep_until(deadline) => {
561                    let applied = self.progress.load_applied();
562                    self.send_feedback(stream, applied, false).await?;
563                    *last_status_sent = Instant::now();
564                }
565            }
566        }
567    }
568
569    /// Handle PostgreSQL authentication exchange.
570    async fn authenticate<S: AsyncRead + AsyncWrite + Unpin>(
571        &mut self,
572        stream: &mut S,
573    ) -> Result<()> {
574        loop {
575            let msg = read_backend_message(stream).await?;
576            match msg.tag {
577                b'R' => {
578                    let (code, rest) = parse_auth_request(&msg.payload)?;
579                    self.handle_auth_request(stream, code, rest).await?;
580                }
581                b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
582                b'S' | b'K' => {}      // ParameterStatus, BackendKeyData - ignore
583                b'Z' => return Ok(()), // ReadyForQuery - auth complete
584                _ => {}
585            }
586        }
587    }
588
589    /// Handle a specific authentication request.
590    async fn handle_auth_request<S: AsyncRead + AsyncWrite + Unpin>(
591        &mut self,
592        stream: &mut S,
593        code: i32,
594        data: &[u8],
595    ) -> Result<()> {
596        match code {
597            0 => Ok(()), // AuthenticationOk
598            3 => {
599                // Cleartext password
600                let mut payload = Vec::from(self.cfg.password.as_bytes());
601                payload.push(0);
602                write_password_message(stream, &payload).await
603            }
604            10 => {
605                // SASL (SCRAM-SHA-256)
606                self.auth_scram(stream, data).await
607            }
608            #[cfg(feature = "md5")]
609            5 => {
610                // MD5 password
611                if data.len() != 4 {
612                    return Err(PgWireError::Protocol(
613                        "MD5 auth: expected 4-byte salt".into(),
614                    ));
615                }
616                let mut salt = [0u8; 4];
617                salt.copy_from_slice(&data[..4]);
618
619                let hash = postgres_md5(&self.cfg.password, &self.cfg.user, &salt);
620                let mut payload = hash.into_bytes();
621                payload.push(0);
622                write_password_message(stream, &payload).await
623            }
624            _ => Err(PgWireError::Auth(format!(
625                "unsupported auth method code: {code}"
626            ))),
627        }
628    }
629
630    /// Perform SCRAM-SHA-256 authentication.
631    async fn auth_scram<S: AsyncRead + AsyncWrite + Unpin>(
632        &mut self,
633        stream: &mut S,
634        mechanisms_data: &[u8],
635    ) -> Result<()> {
636        // Parse offered mechanisms
637        let mechanisms = parse_sasl_mechanisms(mechanisms_data);
638
639        if !mechanisms.iter().any(|m| m == "SCRAM-SHA-256") {
640            return Err(PgWireError::Auth(format!(
641                "server doesn't offer SCRAM-SHA-256, available: {mechanisms:?}"
642            )));
643        }
644
645        #[cfg(not(feature = "scram"))]
646        return Err(PgWireError::Auth(
647            "SCRAM authentication required but 'scram' feature not enabled".into(),
648        ));
649
650        #[cfg(feature = "scram")]
651        {
652            use crate::auth::scram::ScramClient;
653
654            let scram = ScramClient::new(&self.cfg.user);
655
656            // Send SASLInitialResponse
657            let mut init = Vec::new();
658            init.extend_from_slice(b"SCRAM-SHA-256\0");
659            init.extend_from_slice(&(scram.client_first.len() as i32).to_be_bytes());
660            init.extend_from_slice(scram.client_first.as_bytes());
661            write_password_message(stream, &init).await?;
662
663            // Receive AuthenticationSASLContinue (code 11)
664            let server_first = read_auth_data(stream, 11).await?;
665            let server_first_str = String::from_utf8_lossy(&server_first);
666
667            // Compute and send client-final
668            let (client_final, auth_message, salted_password) =
669                scram.client_final(&self.cfg.password, &server_first_str)?;
670            write_password_message(stream, client_final.as_bytes()).await?;
671
672            // Receive and verify AuthenticationSASLFinal (code 12)
673            let server_final = read_auth_data(stream, 12).await?;
674            let server_final_str = String::from_utf8_lossy(&server_final);
675            ScramClient::verify_server_final(&server_final_str, &salted_password, &auth_message)?;
676
677            Ok(())
678        }
679    }
680
681    /// Send standby status update to server.
682    async fn send_feedback<S: AsyncWrite + Unpin>(
683        &self,
684        stream: &mut S,
685        applied: Lsn,
686        reply_requested: bool,
687    ) -> Result<()> {
688        let client_time = current_pg_timestamp();
689        let payload = encode_standby_status_update(applied, client_time, reply_requested);
690        write_copy_data(stream, &payload).await?;
691        self.metrics.record_feedback_sent(applied);
692        Ok(())
693    }
694}
695
696/// Parse SASL mechanism list from auth data.
697fn parse_sasl_mechanisms(data: &[u8]) -> Vec<String> {
698    let mut mechanisms = Vec::new();
699    let mut remaining = data;
700
701    while !remaining.is_empty() {
702        if let Some(pos) = remaining.iter().position(|&x| x == 0) {
703            if pos == 0 {
704                break; // Empty string terminates list
705            }
706            mechanisms.push(String::from_utf8_lossy(&remaining[..pos]).to_string());
707            remaining = &remaining[pos + 1..];
708        } else {
709            break;
710        }
711    }
712
713    mechanisms
714}
715
716fn parse_pgoutput_boundary(data: &Bytes) -> Result<Option<ReplicationEvent>> {
717    if data.is_empty() {
718        return Ok(None);
719    }
720
721    let tag = data[0];
722    let mut p = &data[1..];
723
724    fn take_i8(p: &mut &[u8]) -> Result<i8> {
725        if p.is_empty() {
726            return Err(PgWireError::Protocol("pgoutput: truncated i8".into()));
727        }
728        let v = p[0] as i8;
729        *p = &p[1..];
730        Ok(v)
731    }
732
733    fn take_i32(p: &mut &[u8]) -> Result<i32> {
734        if p.len() < 4 {
735            return Err(PgWireError::Protocol("pgoutput: truncated i32".into()));
736        }
737        let (head, tail) = p.split_at(4);
738        *p = tail;
739        Ok(i32::from_be_bytes(head.try_into().unwrap()))
740    }
741
742    fn take_i64(p: &mut &[u8]) -> Result<i64> {
743        if p.len() < 8 {
744            return Err(PgWireError::Protocol("pgoutput: truncated i64".into()));
745        }
746        let (head, tail) = p.split_at(8);
747        *p = tail;
748        Ok(i64::from_be_bytes(head.try_into().unwrap()))
749    }
750
751    match tag {
752        b'B' => {
753            let final_lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
754            let commit_time_micros = take_i64(&mut p)?;
755            let xid = take_i32(&mut p)? as u32;
756
757            Ok(Some(ReplicationEvent::Begin {
758                final_lsn,
759                commit_time_micros,
760                xid,
761            }))
762        }
763        b'C' => {
764            let _flags = take_i8(&mut p)?;
765            let lsn = Lsn::from_u64(take_i64(&mut p)? as u64); // should be safe
766            let end_lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
767            let commit_time_micros = take_i64(&mut p)?;
768
769            Ok(Some(ReplicationEvent::Commit {
770                lsn,
771                end_lsn,
772                commit_time_micros,
773            }))
774        }
775        b'M' => {
776            // Logical decoding message (pg_logical_emit_message)
777            // Wire: flags(1) + lsn(8) + prefix(null-terminated) + content_len(4) + content(n)
778            let flags = take_i8(&mut p)?;
779            let transactional = (flags & 1) != 0;
780            let lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
781
782            // Read null-terminated prefix string
783            let prefix_end = p.iter().position(|&b| b == 0).ok_or_else(|| {
784                PgWireError::Protocol("pgoutput Message: missing null terminator for prefix".into())
785            })?;
786            let prefix = String::from_utf8_lossy(&p[..prefix_end]).into_owned();
787            p = &p[prefix_end + 1..]; // advance past null byte
788
789            let content_len = take_i32(&mut p)? as usize;
790            if p.len() < content_len {
791                return Err(PgWireError::Protocol(format!(
792                    "pgoutput Message: expected {} content bytes, got {}",
793                    content_len,
794                    p.len()
795                )));
796            }
797            let content = Bytes::copy_from_slice(&p[..content_len]);
798
799            Ok(Some(ReplicationEvent::Message {
800                transactional,
801                lsn,
802                prefix,
803                content,
804            }))
805        }
806        _ => Ok(None),
807    }
808}
809
810/// Read authentication response data for a specific auth code.
811async fn read_auth_data<S: AsyncRead + AsyncWrite + Unpin>(
812    stream: &mut S,
813    expected_code: i32,
814) -> Result<Vec<u8>> {
815    loop {
816        let msg = read_backend_message(stream).await?;
817        match msg.tag {
818            b'R' => {
819                let (code, data) = parse_auth_request(&msg.payload)?;
820                if code == expected_code {
821                    return Ok(data.to_vec());
822                }
823                return Err(PgWireError::Auth(format!(
824                    "unexpected auth code {code}, expected {expected_code}"
825                )));
826            }
827            b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
828            _ => {} // Skip other messages
829        }
830    }
831}
832
833/// Get current time as PostgreSQL timestamp (microseconds since 2000-01-01).
834fn current_pg_timestamp() -> i64 {
835    use std::time::{SystemTime, UNIX_EPOCH};
836
837    let now = SystemTime::now()
838        .duration_since(UNIX_EPOCH)
839        .unwrap_or_default();
840
841    let unix_micros = (now.as_secs() as i64) * 1_000_000 + (now.subsec_micros() as i64);
842    unix_micros - PG_EPOCH_MICROS
843}
844
845/// Compute PostgreSQL MD5 password hash.
846#[cfg(feature = "md5")]
847fn postgres_md5(password: &str, user: &str, salt: &[u8; 4]) -> String {
848    fn md5_hex(data: &[u8]) -> String {
849        format!("{:x}", md5::compute(data))
850    }
851
852    // First hash: md5(password + username)
853    let inner = md5_hex(format!("{password}{user}").as_bytes());
854
855    // Second hash: md5(inner_hash + salt)
856    let mut outer_input = inner.into_bytes();
857    outer_input.extend_from_slice(salt);
858
859    format!("md5{}", md5_hex(&outer_input))
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn parse_sasl_mechanisms_single() {
868        let data = b"SCRAM-SHA-256\0\0";
869        let mechs = parse_sasl_mechanisms(data);
870        assert_eq!(mechs, vec!["SCRAM-SHA-256"]);
871    }
872
873    #[test]
874    fn parse_sasl_mechanisms_multiple() {
875        let data = b"SCRAM-SHA-256\0SCRAM-SHA-256-PLUS\0\0";
876        let mechs = parse_sasl_mechanisms(data);
877        assert_eq!(mechs, vec!["SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"]);
878    }
879
880    #[test]
881    fn parse_sasl_mechanisms_empty() {
882        let mechs = parse_sasl_mechanisms(b"\0");
883        assert!(mechs.is_empty());
884    }
885
886    #[test]
887    #[cfg(feature = "md5")]
888    fn postgres_md5_known_value() {
889        // Test vector: user="md5_user", password="md5_pass", salt=[0x01, 0x02, 0x03, 0x04]
890        // Can verify with: SELECT 'md5' || md5(md5('md5_passmd5_user') || E'\\x01020304');
891        let hash = postgres_md5("md5_pass", "md5_user", &[0x01, 0x02, 0x03, 0x04]);
892        assert!(hash.starts_with("md5"));
893        assert_eq!(hash.len(), 35); // "md5" + 32 hex chars
894    }
895
896    #[test]
897    fn current_pg_timestamp_is_positive() {
898        // Any time after 2000-01-01 should be positive
899        let ts = current_pg_timestamp();
900        assert!(ts > 0);
901    }
902
903    /// Build a CopyData('d') frame wrapping an XLogData('w') replication message
904    /// carrying a non-boundary pgoutput payload (so the worker forwards it as a
905    /// raw `XLogData` event).
906    fn xlog_frame(wal_end: u64, data: &[u8]) -> Vec<u8> {
907        let mut payload = Vec::new();
908        payload.push(b'w');
909        payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); // wal_start
910        payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); // wal_end
911        payload.extend_from_slice(&0i64.to_be_bytes()); // server_time
912        payload.extend_from_slice(data);
913
914        let mut frame = Vec::new();
915        frame.push(b'd');
916        frame.extend_from_slice(&((payload.len() + 4) as i32).to_be_bytes());
917        frame.extend_from_slice(&payload);
918        frame
919    }
920
921    /// Build a `WorkerState` wired to in-memory channels for unit testing.
922    /// Returns the worker plus the stop sender and event receiver (held so the
923    /// channels stay open for the duration of the test).
924    fn test_worker(
925        cfg: ReplicationConfig,
926    ) -> (
927        WorkerState,
928        watch::Sender<bool>,
929        mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>,
930    ) {
931        let progress = Arc::new(SharedProgress::new(cfg.start_lsn));
932        let (stop_tx, stop_rx) = watch::channel(false);
933        let (tx, rx) = mpsc::channel(16);
934        let metrics = Arc::new(ReplicationMetrics::default());
935        let worker = WorkerState::new(cfg, progress, stop_rx, tx, metrics);
936        (worker, stop_tx, rx)
937    }
938
939    /// The `options` startup parameter is how `libpq`/`PGOPTIONS` lets a caller
940    /// pin session GUCs (e.g. `DateStyle`, `IntervalStyle`) on a connection.
941    /// PostgreSQL honors it on replication connections too, so it must be
942    /// forwarded into the startup message when the caller sets it, and left
943    /// out entirely otherwise (server defaults apply).
944    #[tokio::test]
945    async fn startup_includes_options_when_set() {
946        use tokio::io::{AsyncReadExt, AsyncWriteExt};
947
948        let cfg = ReplicationConfig::new("h", "u", "p", "db", "slot", "pub")
949            .with_options("-c datestyle=ISO,MDY -c intervalstyle=iso_8601");
950        let (worker, _stop_tx, _rx) = test_worker(cfg);
951        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
952
953        worker.startup(&mut worker_end).await.unwrap();
954        worker_end.shutdown().await.unwrap();
955
956        let mut sent = Vec::new();
957        server.read_to_end(&mut sent).await.unwrap();
958        let sent = String::from_utf8_lossy(&sent);
959
960        assert!(
961            sent.contains("options"),
962            "startup message missing 'options' key: {sent:?}"
963        );
964        assert!(
965            sent.contains("-c datestyle=ISO,MDY -c intervalstyle=iso_8601"),
966            "startup message missing options value: {sent:?}"
967        );
968    }
969
970    #[tokio::test]
971    async fn startup_omits_options_when_unset() {
972        use tokio::io::{AsyncReadExt, AsyncWriteExt};
973
974        let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
975        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
976
977        worker.startup(&mut worker_end).await.unwrap();
978        worker_end.shutdown().await.unwrap();
979
980        let mut sent = Vec::new();
981        server.read_to_end(&mut sent).await.unwrap();
982        let sent = String::from_utf8_lossy(&sent);
983
984        assert!(
985            !sent.contains("options"),
986            "startup message should omit 'options' when unset: {sent:?}"
987        );
988    }
989
990    #[tokio::test]
991    async fn start_replication_returns_ok_on_copy_both_response() {
992        use std::time::Duration;
993        use tokio::io::AsyncWriteExt;
994
995        let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
996        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
997
998        // Server accepts the stream: CopyBothResponse ('W', empty payload).
999        server.write_all(&[b'W', 0, 0, 0, 4]).await.unwrap();
1000
1001        let res = tokio::time::timeout(
1002            Duration::from_secs(5),
1003            worker.start_replication(&mut worker_end),
1004        )
1005        .await;
1006        assert!(matches!(res, Ok(Ok(()))), "expected Ok on 'W', got {res:?}");
1007    }
1008
1009    #[tokio::test]
1010    async fn start_replication_surfaces_server_error_response() {
1011        use std::time::Duration;
1012        use tokio::io::AsyncWriteExt;
1013
1014        let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
1015        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1016
1017        // Server rejects the stream: ErrorResponse ('E') with an empty field list.
1018        server.write_all(&[b'E', 0, 0, 0, 5, 0]).await.unwrap();
1019
1020        let res = tokio::time::timeout(
1021            Duration::from_secs(5),
1022            worker.start_replication(&mut worker_end),
1023        )
1024        .await;
1025        assert!(
1026            matches!(res, Ok(Err(PgWireError::Server(_)))),
1027            "expected Server error on 'E', got {res:?}"
1028        );
1029    }
1030
1031    /// Regression test for the keepalive/feedback starvation bug
1032    /// (spiceai/spiceai#11616): when the event channel fills and the consumer
1033    /// never drains, the worker must keep emitting standby-status feedback so
1034    /// the server does not hit `wal_sender_timeout` and reset the connection.
1035    #[tokio::test]
1036    async fn feedback_continues_under_backpressure() {
1037        use std::time::Duration;
1038        use tokio::io::AsyncWriteExt;
1039
1040        let cfg = ReplicationConfig {
1041            status_interval: Duration::from_millis(40),
1042            idle_wakeup_interval: Duration::from_secs(10),
1043            buffer_events: 2,
1044            ..Default::default()
1045        };
1046
1047        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1048        let (stop_tx, stop_rx) = watch::channel(false);
1049        // Receiver is held but never drained, so the channel stays full.
1050        let (tx, _rx_never_drained) = mpsc::channel(cfg.buffer_events);
1051        let metrics = Arc::new(ReplicationMetrics::default());
1052        let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1053
1054        let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1055
1056        let worker_task = tokio::spawn(async move {
1057            let mut buf = BufReader::with_capacity(1024, worker_end);
1058            let _ = worker.stream_loop(&mut buf).await;
1059        });
1060
1061        // Feed more messages than the 2-slot channel can hold; the worker
1062        // forwards two, then parks awaiting capacity on the third.
1063        for i in 1..=5u64 {
1064            test_end.write_all(&xlog_frame(i, b"Idummy")).await.unwrap();
1065        }
1066        test_end.flush().await.unwrap();
1067
1068        // A feedback message is a CopyData('d') whose payload is a
1069        // StandbyStatusUpdate('r'). Assert several arrive while the channel is
1070        // full — i.e. feedback survives backpressure, not just the initial one.
1071        let mut reader = MessageReader::new();
1072        let mut feedback_count = 0;
1073        let deadline = Instant::now() + Duration::from_millis(500);
1074        while feedback_count < 3 {
1075            let remaining = deadline.saturating_duration_since(Instant::now());
1076            if remaining.is_zero() {
1077                break;
1078            }
1079            match tokio::time::timeout(remaining, reader.read(&mut test_end)).await {
1080                Ok(Ok(msg)) if msg.tag == b'd' && msg.payload.first() == Some(&b'r') => {
1081                    feedback_count += 1;
1082                }
1083                Ok(Ok(_)) => continue,
1084                _ => break,
1085            }
1086        }
1087
1088        assert!(
1089            feedback_count >= 3,
1090            "expected repeated feedback under backpressure, got {feedback_count}"
1091        );
1092        assert!(
1093            metrics.stall_count() >= 1,
1094            "expected at least one stall to be recorded"
1095        );
1096        assert!(
1097            metrics.feedback_sent() >= 3,
1098            "feedback_sent metric should track sends, got {}",
1099            metrics.feedback_sent()
1100        );
1101
1102        let _ = stop_tx.send(true);
1103        let _ = worker_task.await;
1104    }
1105
1106    /// Feedback under backpressure must be *rate-limited* to `status_interval`,
1107    /// not emitted on every loop iteration. Guards the stall-loop deadline math:
1108    /// a wrong sign (deadline in the past) would busy-loop and flood the server.
1109    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1110    async fn feedback_under_backpressure_is_rate_limited() {
1111        use std::time::Duration;
1112        use tokio::io::AsyncWriteExt;
1113
1114        let interval = Duration::from_millis(50);
1115        let cfg = ReplicationConfig {
1116            status_interval: interval,
1117            idle_wakeup_interval: Duration::from_secs(10),
1118            buffer_events: 2,
1119            ..Default::default()
1120        };
1121
1122        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1123        let (stop_tx, stop_rx) = watch::channel(false);
1124        let (tx, _rx_never_drained) = mpsc::channel(cfg.buffer_events);
1125        let metrics = Arc::new(ReplicationMetrics::default());
1126        let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1127
1128        let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1129        let worker_task = tokio::spawn(async move {
1130            let mut buf = BufReader::with_capacity(1024, worker_end);
1131            let _ = worker.stream_loop(&mut buf).await;
1132        });
1133
1134        for i in 1..=5u64 {
1135            test_end.write_all(&xlog_frame(i, b"Idummy")).await.unwrap();
1136        }
1137        test_end.flush().await.unwrap();
1138
1139        // Over a fixed window, correct behavior emits roughly window/interval
1140        // feedbacks (~7 here). A deadline that fires every iteration would emit
1141        // far more, so a generous upper bound catches the flooding regression
1142        // while the lower bound confirms feedback keeps flowing.
1143        let window = Duration::from_millis(350);
1144        let end = Instant::now() + window;
1145        let mut reader = MessageReader::new();
1146        let mut feedback = 0usize;
1147        loop {
1148            let remaining = end.saturating_duration_since(Instant::now());
1149            if remaining.is_zero() {
1150                break;
1151            }
1152            match tokio::time::timeout(remaining, reader.read(&mut test_end)).await {
1153                Ok(Ok(msg)) if msg.tag == b'd' && msg.payload.first() == Some(&b'r') => {
1154                    feedback += 1;
1155                }
1156                Ok(Ok(_)) => {}
1157                _ => break,
1158            }
1159        }
1160
1161        assert!(
1162            feedback >= 2,
1163            "feedback should keep flowing under backpressure, got {feedback}"
1164        );
1165        assert!(
1166            feedback <= 25,
1167            "feedback must be rate-limited to ~status_interval, got {feedback}"
1168        );
1169
1170        let _ = stop_tx.send(true);
1171        let _ = worker_task.await;
1172    }
1173}