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