Skip to main content

pgwire_replication/client/
worker.rs

1use bytes::Bytes;
2use std::sync::Arc;
3use tokio::io::{AsyncRead, AsyncWrite, BufReader};
4use tokio::sync::{mpsc, watch};
5use tokio::time::Instant;
6
7use super::metrics::ReplicationMetrics;
8use super::progress::ProgressCell;
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/// The applied LSN lives in a lock-free [`ProgressCell`] so progress updates
24/// are cheap and monotonic without async backpressure. A raise also wakes an
25/// idle worker, which sends it promptly instead of at its next wakeup. Only the
26/// first raise after the worker last looked bumps the watch generation; later
27/// ones coalesce into it and cost one more atomic load.
28pub struct SharedProgress {
29    cell: ProgressCell,
30    changed: watch::Sender<u64>,
31}
32
33/// Clock origin for pending starts (nanoseconds after it, plus one), shared
34/// by every session so a start carried from an earlier one stays comparable.
35/// tokio's clock: the system clock, paused under test. Set an hour back so a
36/// clock read on another (paused) runtime never falls before it.
37fn clock_origin() -> Instant {
38    static ORIGIN: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
39    *ORIGIN.get_or_init(|| {
40        let now = Instant::now();
41        now.checked_sub(std::time::Duration::from_secs(3600))
42            .unwrap_or(now)
43    })
44}
45
46impl SharedProgress {
47    #[cfg(test)]
48    pub fn new(start: Lsn) -> Self {
49        Self::with_timing(start, false)
50    }
51
52    /// `timed`: keep when the oldest unreported raise happened, for a
53    /// feedback observer.
54    pub fn with_timing(start: Lsn, timed: bool) -> Self {
55        Self {
56            cell: ProgressCell::new(start.as_u64(), timed),
57            changed: watch::Sender::new(0),
58        }
59    }
60
61    fn clock(&self) -> u64 {
62        clock_origin().elapsed().as_nanos() as u64 + 1
63    }
64
65    fn instant(&self, clock: u64) -> Instant {
66        clock_origin() + std::time::Duration::from_nanos(clock - 1)
67    }
68
69    /// When the oldest raise not yet reported to the server happened.
70    pub fn pending_since(&self) -> Option<std::time::Instant> {
71        match self.cell.pending_since() {
72            0 => None,
73            clock => Some(self.instant(clock).into_std()),
74        }
75    }
76
77    /// Carry over the pending start of an earlier session (the older wins).
78    pub fn carry_pending(&self, since: std::time::Instant) {
79        let since = Instant::from_std(since);
80        let clock = since.saturating_duration_since(clock_origin()).as_nanos() as u64 + 1;
81        self.cell.carry_pending(clock);
82    }
83
84    #[inline]
85    pub fn load_applied(&self) -> Lsn {
86        Lsn::from_u64(self.cell.load())
87    }
88
89    /// Monotonic update: if `lsn` is lower than the currently stored applied LSN,
90    /// this is a no-op.
91    #[inline]
92    pub fn update_applied(&self, lsn: Lsn) {
93        if self.cell.raise(lsn.as_u64(), || self.clock()) {
94            self.changed.send_modify(|g| *g = g.wrapping_add(1));
95        }
96    }
97
98    /// Wakes when the applied LSN is raised.
99    pub fn subscribe(&self) -> watch::Receiver<u64> {
100        self.changed.subscribe()
101    }
102
103    /// The worker is about to read the applied LSN: the next raise wakes it
104    /// again. Returns the applied LSN read after clearing the signal.
105    fn take_signal(&self) -> Lsn {
106        Lsn::from_u64(self.cell.take_signal())
107    }
108}
109
110/// Events emitted by the replication worker.
111#[derive(Debug, Clone)]
112pub enum ReplicationEvent {
113    /// Server heartbeat message.
114    KeepAlive {
115        /// Current server WAL end position
116        wal_end: Lsn,
117        /// Whether server requested a reply (already handled internally)
118        reply_requested: bool,
119        /// Server timestamp (microseconds since 2000-01-01)
120        server_time_micros: i64,
121    },
122
123    /// Start of a transaction (pgoutput Begin message).
124    Begin {
125        final_lsn: Lsn,
126        xid: u32,
127        commit_time_micros: i64,
128    },
129
130    /// WAL data containing transaction changes.
131    XLogData {
132        /// WAL position where this data starts
133        wal_start: Lsn,
134        /// WAL end position (may be 0 for mid-transaction messages)
135        wal_end: Lsn,
136        /// Server timestamp (microseconds since 2000-01-01)
137        server_time_micros: i64,
138        /// pgoutput-encoded change data
139        data: Bytes,
140    },
141
142    /// End of a transaction (pgoutput Commit message).
143    Commit {
144        lsn: Lsn,
145        end_lsn: Lsn,
146        commit_time_micros: i64,
147    },
148
149    /// Logical decoding message emitted via `pg_logical_emit_message()`.
150    ///
151    /// Transactional messages are delivered only after the enclosing
152    /// transaction commits. Non-transactional messages are delivered
153    /// immediately.
154    Message {
155        /// Whether the message was emitted inside a transaction.
156        transactional: bool,
157        /// LSN of the message in the WAL.
158        lsn: Lsn,
159        /// Application-defined message prefix (e.g. `"myapp.checkpoint"`).
160        prefix: String,
161        /// Raw message content bytes.
162        content: Bytes,
163    },
164
165    /// Emitted when `stop_at_lsn` has been reached.
166    ///
167    /// After this event, no more events will be emitted and the
168    /// replication stream will be closed.
169    StoppedAt {
170        /// The LSN that triggered the stop condition
171        reached: Lsn,
172    },
173}
174
175/// Channel receiver type for replication events.
176pub type ReplicationEventReceiver =
177    mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>;
178
179/// One row of a simple-query result: each column's text-format value, `None`
180/// for SQL NULL.
181pub type QueryRow = Vec<Option<Bytes>>;
182
183/// A request to a gated session, which waits after authentication instead of
184/// starting replication at once.
185pub(crate) enum GateCommand {
186    /// Run a simple query (SQL or a replication command such as
187    /// IDENTIFY_SYSTEM) on the authenticated session.
188    Query {
189        sql: String,
190        reply: tokio::sync::oneshot::Sender<Result<Vec<QueryRow>>>,
191    },
192    /// Send START_REPLICATION.
193    Start,
194}
195
196/// Internal worker state.
197pub struct WorkerState {
198    cfg: ReplicationConfig,
199    progress: Arc<SharedProgress>,
200    stop_rx: watch::Receiver<bool>,
201    out: mpsc::Sender<std::result::Result<ReplicationEvent, PgWireError>>,
202    metrics: Arc<ReplicationMetrics>,
203    /// Set to `true` once the server accepted START_REPLICATION.
204    started: Option<watch::Sender<bool>>,
205    /// When set, the session waits for these commands between authentication
206    /// and START_REPLICATION.
207    gate: Option<mpsc::Receiver<GateCommand>>,
208    /// The applied LSN in the last status update written to the server, if any.
209    last_sent: Option<Lsn>,
210}
211
212impl WorkerState {
213    pub fn new(
214        cfg: ReplicationConfig,
215        progress: Arc<SharedProgress>,
216        stop_rx: watch::Receiver<bool>,
217        out: mpsc::Sender<std::result::Result<ReplicationEvent, PgWireError>>,
218        metrics: Arc<ReplicationMetrics>,
219    ) -> Self {
220        Self {
221            cfg,
222            progress,
223            stop_rx,
224            out,
225            metrics,
226            started: None,
227            gate: None,
228            last_sent: None,
229        }
230    }
231
232    /// Notify `started` once the server accepts START_REPLICATION.
233    pub(crate) fn notify_started(&mut self, started: watch::Sender<bool>) {
234        self.started = Some(started);
235    }
236
237    /// Wait for `gate` commands after authentication, before START_REPLICATION:
238    /// queries run on the authenticated session, `Start` starts replication,
239    /// and a closed gate ends the session without starting.
240    pub(crate) fn gate_before_start(&mut self, gate: mpsc::Receiver<GateCommand>) {
241        self.gate = Some(gate);
242    }
243
244    /// Serve gate commands until `Start`. `false`: the gate closed (the client
245    /// was dropped) and the session ended without starting.
246    async fn serve_gate<S: AsyncRead + AsyncWrite + Unpin>(
247        gate: &mut mpsc::Receiver<GateCommand>,
248        stream: &mut S,
249        max_message_size: usize,
250    ) -> Result<bool> {
251        loop {
252            match gate.recv().await {
253                Some(GateCommand::Query { sql, reply }) => {
254                    let result = simple_query(stream, &sql, max_message_size).await;
255                    // A server error leaves the session usable (it ends with
256                    // ReadyForQuery); anything else leaves the stream in an
257                    // unknown state, so the session ends.
258                    let broken = matches!(&result, Err(e) if !e.is_server());
259                    let ended = result.as_ref().err().cloned();
260                    let _ = reply.send(result);
261                    if broken {
262                        return Err(ended.expect("checked error"));
263                    }
264                }
265                Some(GateCommand::Start) => return Ok(true),
266                None => {
267                    write_terminate(stream).await?;
268                    return Ok(false);
269                }
270            }
271        }
272    }
273
274    /// Run the replication protocol on the given stream.
275    pub async fn run_on_stream<S: AsyncRead + AsyncWrite + Unpin>(
276        &mut self,
277        stream: &mut S,
278    ) -> Result<()> {
279        // Wrap in a 128KB read buffer to batch multiple WAL messages into fewer
280        // recv() syscalls. BufReader delegates AsyncWrite to the inner stream,
281        // so writes (standby status replies, etc.) are unaffected.
282        let mut stream = BufReader::with_capacity(128 * 1024, stream);
283        self.startup(&mut stream).await?;
284        self.authenticate(&mut stream).await?;
285        if let Some(mut gate) = self.gate.take() {
286            if !Self::serve_gate(&mut gate, &mut stream, self.cfg.max_message_size).await? {
287                return Ok(());
288            }
289        }
290        self.start_replication(&mut stream).await?;
291        if let Some(started) = &self.started {
292            started.send_replace(true);
293        }
294        self.stream_loop(&mut stream).await
295    }
296
297    /// Send startup message with replication parameters.
298    async fn startup<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<()> {
299        let mut params = vec![
300            ("user", self.cfg.user.as_str()),
301            ("database", self.cfg.database.as_str()),
302            ("replication", "database"),
303            ("client_encoding", "UTF8"),
304            (
305                "application_name",
306                self.cfg
307                    .application_name
308                    .as_deref()
309                    .unwrap_or("pgwire-replication"),
310            ),
311        ];
312        // PostgreSQL honors `options` on replication connections the same as on
313        // ordinary ones, so this lets callers pin session GUCs (e.g. `DateStyle`,
314        // `IntervalStyle`) the way they already can via a normal libpq `options`
315        // parameter — only sent when the caller actually set one.
316        if let Some(options) = self.cfg.options.as_deref() {
317            params.push(("options", options));
318        }
319        write_startup_message(stream, 196608, &params).await
320    }
321
322    /// Start the logical replication stream.
323    async fn start_replication<S: AsyncRead + AsyncWrite + Unpin>(
324        &self,
325        stream: &mut S,
326    ) -> Result<()> {
327        let binary_opt = if self.cfg.binary {
328            ", binary 'true'"
329        } else {
330            ""
331        };
332        let sql = format!(
333            "START_REPLICATION SLOT {} LOGICAL {} \
334            (proto_version '1', publication_names '{}', messages 'true'{})",
335            self.cfg.slot,
336            self.cfg.start_lsn,
337            self.cfg.publication.to_option_value(),
338            binary_opt,
339        );
340        write_query(stream, &sql).await?;
341
342        // Wait for CopyBothResponse
343        loop {
344            let msg = read_backend_message(stream).await?;
345            match msg.tag {
346                b'W' => return Ok(()), // CopyBothResponse - ready to stream
347                b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
348                b'N' | b'S' | b'K' => continue, // Notice, ParameterStatus, BackendKeyData
349                _ => continue,
350            }
351        }
352    }
353
354    /// Main replication streaming loop.
355    ///
356    /// Uses a two-phase approach for throughput:
357    /// 1. **Drain phase**: while the BufReader has buffered data, read messages
358    ///    in a tight loop without `select!` or timeout overhead.
359    /// 2. **Wait phase**: when the buffer is empty, fall back to `select!` with
360    ///    timeout + stop signal to handle idle keepalives and graceful shutdown.
361    ///
362    /// Reads use [`MessageReader`], which preserves partial-read state across
363    /// dropped futures so the wait-phase `select!` is cancellation-safe.
364    async fn stream_loop<S: AsyncRead + AsyncWrite + Unpin>(
365        &mut self,
366        stream: &mut BufReader<S>,
367    ) -> Result<()> {
368        let mut last_status_sent = Instant::now() - self.cfg.status_interval;
369        let mut last_applied = self.progress.load_applied();
370        // Wakes the wait phase when the consumer raises the applied LSN.
371        let mut applied_rx = self.progress.subscribe();
372        // Cancellation-safe message reader, partial reads survive dropped futures.
373        let mut reader = MessageReader::new().with_max_message_size(self.cfg.max_message_size);
374        // How many messages to process in the tight loop before checking
375        // stop signal and sending periodic status feedback.
376        const DRAIN_BATCH: usize = 256;
377
378        loop {
379            // Update applied LSN from client
380            let current_applied = self.progress.load_applied();
381            if current_applied != last_applied {
382                last_applied = current_applied;
383            }
384
385            // Send periodic status feedback
386            if last_status_sent.elapsed() >= self.cfg.status_interval {
387                self.send_feedback(stream, last_applied, false).await?;
388                last_status_sent = Instant::now();
389            }
390
391            // ── Drain phase: tight loop while BufReader has buffered data ──
392            // The BufReader has a 128KB internal buffer. When the kernel delivers
393            // a large TCP segment, many WAL messages are available without syscalls.
394            // Read them in a tight loop to avoid select!/timeout overhead per message.
395            let mut drained = 0usize;
396            while stream.buffer().len() >= 5 && drained < DRAIN_BATCH {
397                let msg = reader.read(stream).await?;
398                drained += 1;
399                if msg.tag == b'E' {
400                    return Err(PgWireError::Server(parse_error_response(&msg.payload)));
401                }
402                if msg.tag == b'd'
403                    && self
404                        .handle_copy_data(
405                            stream,
406                            msg.payload,
407                            &mut last_applied,
408                            &mut last_status_sent,
409                        )
410                        .await?
411                {
412                    return Ok(());
413                }
414            }
415
416            // If we drained messages, loop back to check stop/status before
417            // potentially blocking on the next read.
418            if drained > 0 {
419                // Check stop signal without blocking
420                if self.stop_rx.has_changed().unwrap_or(false) && *self.stop_rx.borrow() {
421                    let _ = write_copy_done(stream).await;
422                    return Ok(());
423                }
424                continue;
425            }
426
427            // ── Wait phase: buffer empty, need to wait for socket data ──
428            //
429            // Both `stop_rx.changed()` and the timeout can drop the read future
430            // mid-message. `MessageReader::read` is cancellation-safe — partial
431            // header/payload state lives on `reader` and is preserved across the
432            // drop, so the next iteration resumes the read without losing bytes.
433            //
434            // An applied LSN newer than the last one sent is pending: it is sent
435            // once `status_interval` has passed since the last status update.
436            // Otherwise a raise wakes the loop. Marking the watch seen and
437            // clearing the signal before reading the LSN means a raise after
438            // this point still wakes it.
439            applied_rx.mark_unchanged();
440            let applied = self.progress.take_signal();
441            let pending = self.last_sent.is_none_or(|sent| applied > sent);
442            let feedback_due = last_status_sent + self.cfg.status_interval;
443            let msg = tokio::select! {
444                biased;
445
446                _ = self.stop_rx.changed() => {
447                    if *self.stop_rx.borrow() {
448                        let _ = write_copy_done(stream).await;
449                        return Ok(());
450                    }
451                    continue;
452                }
453
454                _ = tokio::time::sleep_until(feedback_due), if pending => {
455                    let applied = self.progress.load_applied();
456                    last_applied = applied;
457                    self.send_feedback(stream, applied, false).await?;
458                    last_status_sent = Instant::now();
459                    continue;
460                }
461
462                // The sender lives in `self.progress`, so this never closes.
463                _ = applied_rx.changed(), if !pending => continue,
464
465                msg_result = tokio::time::timeout(
466                    self.cfg.idle_wakeup_interval,
467                    reader.read(stream),
468                ) => {
469                    match msg_result {
470                        Ok(res) => res?,
471                        Err(_) => {
472                            let applied = self.progress.load_applied();
473                            last_applied = applied;
474                            self.send_feedback(stream, applied, false).await?;
475                            last_status_sent = Instant::now();
476                            continue;
477                        }
478                    }
479                }
480            };
481
482            if msg.tag == b'E' {
483                return Err(PgWireError::Server(parse_error_response(&msg.payload)));
484            }
485            if msg.tag == b'd'
486                && self
487                    .handle_copy_data(
488                        stream,
489                        msg.payload,
490                        &mut last_applied,
491                        &mut last_status_sent,
492                    )
493                    .await?
494            {
495                return Ok(());
496            }
497        }
498    }
499
500    /// Handle a CopyData message. Returns true if we should stop.
501    async fn handle_copy_data<S: AsyncRead + AsyncWrite + Unpin>(
502        &mut self,
503        stream: &mut BufReader<S>,
504        payload: Bytes,
505        last_applied: &mut Lsn,
506        last_status_sent: &mut Instant,
507    ) -> Result<bool> {
508        let cd = parse_copy_data(payload)?;
509
510        match cd {
511            ReplicationCopyData::KeepAlive {
512                wal_end,
513                server_time_micros,
514                reply_requested,
515            } => {
516                self.metrics.set_last_wal_end(wal_end);
517
518                // Respond immediately if server requests it
519                if reply_requested {
520                    let applied = self.progress.load_applied();
521                    *last_applied = applied;
522                    self.send_feedback(stream, applied, true).await?;
523                    *last_status_sent = Instant::now();
524                    self.metrics.record_keepalive_reply();
525                }
526
527                if self
528                    .send_event_backpressured(
529                        stream,
530                        Ok(ReplicationEvent::KeepAlive {
531                            wal_end,
532                            reply_requested,
533                            server_time_micros,
534                        }),
535                        last_status_sent,
536                    )
537                    .await?
538                {
539                    return Ok(true);
540                }
541
542                Ok(false)
543            }
544            ReplicationCopyData::XLogData {
545                wal_start,
546                wal_end,
547                server_time_micros,
548                data,
549            } => {
550                self.metrics.set_last_wal_end(wal_end);
551
552                // If the payload is a pgoutput Begin/Commit message, emit only the boundary event.
553                if let Some(boundary_ev) = parse_pgoutput_boundary(&data)? {
554                    let reached_lsn = match boundary_ev {
555                        ReplicationEvent::Begin { final_lsn, .. } => final_lsn,
556                        ReplicationEvent::Commit { end_lsn, .. } => end_lsn,
557                        _ => wal_end, // should never happen if parser only returns Begin/Commit
558                    };
559
560                    if self
561                        .send_event_backpressured(stream, Ok(boundary_ev), last_status_sent)
562                        .await?
563                    {
564                        return Ok(true);
565                    }
566
567                    // Stop condition (prefer boundary LSN semantics when available)
568                    if let Some(stop_lsn) = self.cfg.stop_at_lsn {
569                        if reached_lsn >= stop_lsn {
570                            if self
571                                .send_event_backpressured(
572                                    stream,
573                                    Ok(ReplicationEvent::StoppedAt {
574                                        reached: reached_lsn,
575                                    }),
576                                    last_status_sent,
577                                )
578                                .await?
579                            {
580                                return Ok(true);
581                            }
582                            let _ = write_copy_done(stream).await;
583                            return Ok(true); // should stop.
584                        }
585                    }
586
587                    return Ok(false);
588                }
589                // Otherwise, emit raw payload
590                // Check stop condition
591                if let Some(stop_lsn) = self.cfg.stop_at_lsn {
592                    if wal_end >= stop_lsn {
593                        // Send final event, then stop signal
594                        if self
595                            .send_event_backpressured(
596                                stream,
597                                Ok(ReplicationEvent::XLogData {
598                                    wal_start,
599                                    wal_end,
600                                    server_time_micros,
601                                    data,
602                                }),
603                                last_status_sent,
604                            )
605                            .await?
606                        {
607                            return Ok(true);
608                        }
609
610                        if self
611                            .send_event_backpressured(
612                                stream,
613                                Ok(ReplicationEvent::StoppedAt { reached: wal_end }),
614                                last_status_sent,
615                            )
616                            .await?
617                        {
618                            return Ok(true);
619                        }
620
621                        let _ = write_copy_done(stream).await;
622                        return Ok(true);
623                    }
624                }
625
626                if self
627                    .send_event_backpressured(
628                        stream,
629                        Ok(ReplicationEvent::XLogData {
630                            wal_start,
631                            wal_end,
632                            server_time_micros,
633                            data,
634                        }),
635                        last_status_sent,
636                    )
637                    .await?
638                {
639                    return Ok(true);
640                }
641
642                Ok(false)
643            }
644        }
645    }
646
647    /// Forward an event to the consumer, keeping standby feedback alive under
648    /// backpressure.
649    ///
650    /// Fast path: if the channel has capacity, send immediately — no await, no
651    /// stall. When the channel is full we must **not** block silently on
652    /// `send().await`: that would park the worker and starve standby-status
653    /// feedback until `wal_sender_timeout` fires and the server resets the
654    /// connection. Instead we wait for capacity while continuing to emit
655    /// feedback every `status_interval`, so the server keeps seeing us alive.
656    /// This does not falsely acknowledge data — feedback reports the unchanged
657    /// applied LSN — and real backpressure still propagates via TCP flow
658    /// control (we stop reading the socket while parked here).
659    ///
660    /// Returns `Ok(true)` if a stop was requested while waiting (the caller
661    /// should unwind), or `Ok(false)` once the event is delivered or the
662    /// channel is closed.
663    async fn send_event_backpressured<S: AsyncWrite + Unpin>(
664        &mut self,
665        stream: &mut S,
666        event: std::result::Result<ReplicationEvent, PgWireError>,
667        last_status_sent: &mut Instant,
668    ) -> Result<bool> {
669        // Fast path: capacity available.
670        match self.out.try_reserve() {
671            Ok(permit) => {
672                permit.send(event);
673                self.metrics.record_event_forwarded();
674                return Ok(false);
675            }
676            Err(mpsc::error::TrySendError::Closed(())) => {
677                tracing::debug!("event channel closed, client may have disconnected");
678                return Ok(false);
679            }
680            // Channel full: fall through to the backpressure wait loop. `event`
681            // is untouched (try_reserve carries no value), so we still own it.
682            Err(mpsc::error::TrySendError::Full(())) => {}
683        }
684
685        // Counted here, at the start, so an ongoing stall is visible even while
686        // the worker is still parked below awaiting capacity.
687        self.metrics.record_stall_begin();
688        let stall_start = Instant::now();
689        loop {
690            let deadline = *last_status_sent + self.cfg.status_interval;
691            tokio::select! {
692                biased;
693
694                _ = self.stop_rx.changed() => {
695                    if *self.stop_rx.borrow() {
696                        let _ = write_copy_done(stream).await;
697                        return Ok(true);
698                    }
699                    continue;
700                }
701
702                permit = self.out.reserve() => {
703                    self.metrics
704                        .record_stall_end(stall_start.elapsed().as_micros() as u64);
705                    match permit {
706                        Ok(permit) => {
707                            permit.send(event);
708                            self.metrics.record_event_forwarded();
709                        }
710                        Err(_closed) => {
711                            tracing::debug!("event channel closed while backpressured");
712                        }
713                    }
714                    return Ok(false);
715                }
716
717                _ = tokio::time::sleep_until(deadline) => {}
718            }
719            // The status interval elapsed while parked.
720            let applied = self.progress.load_applied();
721            self.send_feedback(stream, applied, false).await?;
722            *last_status_sent = Instant::now();
723        }
724    }
725
726    /// Handle PostgreSQL authentication exchange.
727    async fn authenticate<S: AsyncRead + AsyncWrite + Unpin>(
728        &mut self,
729        stream: &mut S,
730    ) -> Result<()> {
731        loop {
732            let msg = read_backend_message(stream).await?;
733            match msg.tag {
734                b'R' => {
735                    let (code, rest) = parse_auth_request(&msg.payload)?;
736                    self.handle_auth_request(stream, code, rest).await?;
737                }
738                b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
739                b'S' | b'K' => {}      // ParameterStatus, BackendKeyData - ignore
740                b'Z' => return Ok(()), // ReadyForQuery - auth complete
741                _ => {}
742            }
743        }
744    }
745
746    /// Handle a specific authentication request.
747    async fn handle_auth_request<S: AsyncRead + AsyncWrite + Unpin>(
748        &mut self,
749        stream: &mut S,
750        code: i32,
751        data: &[u8],
752    ) -> Result<()> {
753        match code {
754            0 => Ok(()), // AuthenticationOk
755            3 => {
756                // Cleartext password
757                let mut payload = Vec::from(self.cfg.password.as_bytes());
758                payload.push(0);
759                write_password_message(stream, &payload).await
760            }
761            10 => {
762                // SASL (SCRAM-SHA-256)
763                self.auth_scram(stream, data).await
764            }
765            #[cfg(feature = "md5")]
766            5 => {
767                // MD5 password
768                if data.len() != 4 {
769                    return Err(PgWireError::Protocol(
770                        "MD5 auth: expected 4-byte salt".into(),
771                    ));
772                }
773                let mut salt = [0u8; 4];
774                salt.copy_from_slice(&data[..4]);
775
776                let hash = postgres_md5(&self.cfg.password, &self.cfg.user, &salt);
777                let mut payload = hash.into_bytes();
778                payload.push(0);
779                write_password_message(stream, &payload).await
780            }
781            _ => Err(PgWireError::Auth(format!(
782                "unsupported auth method code: {code}"
783            ))),
784        }
785    }
786
787    /// Perform SCRAM-SHA-256 authentication.
788    async fn auth_scram<S: AsyncRead + AsyncWrite + Unpin>(
789        &mut self,
790        stream: &mut S,
791        mechanisms_data: &[u8],
792    ) -> Result<()> {
793        // Parse offered mechanisms
794        let mechanisms = parse_sasl_mechanisms(mechanisms_data);
795
796        if !mechanisms.iter().any(|m| m == "SCRAM-SHA-256") {
797            return Err(PgWireError::Auth(format!(
798                "server doesn't offer SCRAM-SHA-256, available: {mechanisms:?}"
799            )));
800        }
801
802        #[cfg(not(feature = "scram"))]
803        return Err(PgWireError::Auth(
804            "SCRAM authentication required but 'scram' feature not enabled".into(),
805        ));
806
807        #[cfg(feature = "scram")]
808        {
809            use crate::auth::scram::ScramClient;
810
811            let scram = ScramClient::new(&self.cfg.user);
812
813            // Send SASLInitialResponse
814            let mut init = Vec::new();
815            init.extend_from_slice(b"SCRAM-SHA-256\0");
816            init.extend_from_slice(&(scram.client_first.len() as i32).to_be_bytes());
817            init.extend_from_slice(scram.client_first.as_bytes());
818            write_password_message(stream, &init).await?;
819
820            // Receive AuthenticationSASLContinue (code 11)
821            let server_first = read_auth_data(stream, 11).await?;
822            let server_first_str = String::from_utf8_lossy(&server_first);
823
824            // Compute and send client-final
825            let (client_final, auth_message, salted_password) =
826                scram.client_final(&self.cfg.password, &server_first_str)?;
827            write_password_message(stream, client_final.as_bytes()).await?;
828
829            // Receive and verify AuthenticationSASLFinal (code 12)
830            let server_final = read_auth_data(stream, 12).await?;
831            let server_final_str = String::from_utf8_lossy(&server_final);
832            ScramClient::verify_server_final(&server_final_str, &salted_password, &auth_message)?;
833
834            Ok(())
835        }
836    }
837
838    /// Send standby status update to server.
839    ///
840    /// The position is recorded as sent only once the write succeeded, so a
841    /// failed send leaves it pending.
842    async fn send_feedback<S: AsyncWrite + Unpin>(
843        &mut self,
844        stream: &mut S,
845        applied: Lsn,
846        reply_requested: bool,
847    ) -> Result<()> {
848        // Timed only when this write advances the reported position and
849        // someone observes it.
850        let loaded_at = (self.cfg.feedback_observer.is_some()
851            && self.last_sent.is_none_or(|sent| applied > sent))
852        .then(|| self.progress.clock());
853        let client_time = current_pg_timestamp();
854        let payload = encode_standby_status_update(applied, client_time, reply_requested);
855        write_copy_data(stream, &payload).await?;
856        self.last_sent = Some(applied);
857        self.metrics.record_feedback_sent(applied);
858        if let (Some(at), Some(observer)) = (loaded_at, &self.cfg.feedback_observer) {
859            let since = self.progress.cell.take_pending(applied.as_u64(), at);
860            if since != 0 {
861                observer.observe(crate::FeedbackSent {
862                    pending_for: self.progress.instant(since).elapsed(),
863                });
864            }
865        }
866        Ok(())
867    }
868}
869
870/// Run `sql` with the simple query protocol and collect its rows (text
871/// format). A server ErrorResponse is returned once the server is ready for
872/// the next query.
873async fn simple_query<S: AsyncRead + AsyncWrite + Unpin>(
874    stream: &mut S,
875    sql: &str,
876    max_message_size: usize,
877) -> Result<Vec<QueryRow>> {
878    write_query(stream, sql).await?;
879    // Rows can be as large as the query makes them: bound them like the
880    // replication stream. A refused message is terminal (the session ends).
881    let mut reader = MessageReader::new().with_max_message_size(max_message_size);
882    let mut rows = Vec::new();
883    let mut error = None;
884    loop {
885        let msg = reader.read(stream).await?;
886        match msg.tag {
887            b'D' => rows.push(parse_data_row(&msg.payload)?),
888            b'E' => error = Some(PgWireError::Server(parse_error_response(&msg.payload))),
889            b'Z' => return error.map_or(Ok(rows), Err),
890            // RowDescription, CommandComplete, EmptyQueryResponse, notices,
891            // parameter status.
892            _ => continue,
893        }
894    }
895}
896
897/// Parse a DataRow ('D') payload.
898fn parse_data_row(payload: &Bytes) -> Result<QueryRow> {
899    let malformed = || PgWireError::Protocol("malformed DataRow".into());
900    let mut p = payload.clone();
901    if p.len() < 2 {
902        return Err(malformed());
903    }
904    let columns = i16::from_be_bytes([p[0], p[1]]);
905    p = p.slice(2..);
906    let mut row = Vec::with_capacity(columns.max(0) as usize);
907    for _ in 0..columns {
908        if p.len() < 4 {
909            return Err(malformed());
910        }
911        let len = i32::from_be_bytes([p[0], p[1], p[2], p[3]]);
912        p = p.slice(4..);
913        if len < 0 {
914            row.push(None);
915            continue;
916        }
917        let len = len as usize;
918        if p.len() < len {
919            return Err(malformed());
920        }
921        row.push(Some(p.slice(..len)));
922        p = p.slice(len..);
923    }
924    Ok(row)
925}
926
927/// Send Terminate ('X') to end the session.
928async fn write_terminate<S: AsyncWrite + Unpin>(stream: &mut S) -> Result<()> {
929    use tokio::io::AsyncWriteExt;
930    stream.write_all(&[b'X', 0, 0, 0, 4]).await?;
931    stream.flush().await?;
932    Ok(())
933}
934
935/// Parse SASL mechanism list from auth data.
936fn parse_sasl_mechanisms(data: &[u8]) -> Vec<String> {
937    let mut mechanisms = Vec::new();
938    let mut remaining = data;
939
940    while !remaining.is_empty() {
941        if let Some(pos) = remaining.iter().position(|&x| x == 0) {
942            if pos == 0 {
943                break; // Empty string terminates list
944            }
945            mechanisms.push(String::from_utf8_lossy(&remaining[..pos]).to_string());
946            remaining = &remaining[pos + 1..];
947        } else {
948            break;
949        }
950    }
951
952    mechanisms
953}
954
955fn parse_pgoutput_boundary(data: &Bytes) -> Result<Option<ReplicationEvent>> {
956    if data.is_empty() {
957        return Ok(None);
958    }
959
960    let tag = data[0];
961    let mut p = &data[1..];
962
963    fn take_i8(p: &mut &[u8]) -> Result<i8> {
964        if p.is_empty() {
965            return Err(PgWireError::Protocol("pgoutput: truncated i8".into()));
966        }
967        let v = p[0] as i8;
968        *p = &p[1..];
969        Ok(v)
970    }
971
972    fn take_i32(p: &mut &[u8]) -> Result<i32> {
973        if p.len() < 4 {
974            return Err(PgWireError::Protocol("pgoutput: truncated i32".into()));
975        }
976        let (head, tail) = p.split_at(4);
977        *p = tail;
978        Ok(i32::from_be_bytes(head.try_into().unwrap()))
979    }
980
981    fn take_i64(p: &mut &[u8]) -> Result<i64> {
982        if p.len() < 8 {
983            return Err(PgWireError::Protocol("pgoutput: truncated i64".into()));
984        }
985        let (head, tail) = p.split_at(8);
986        *p = tail;
987        Ok(i64::from_be_bytes(head.try_into().unwrap()))
988    }
989
990    match tag {
991        b'B' => {
992            let final_lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
993            let commit_time_micros = take_i64(&mut p)?;
994            let xid = take_i32(&mut p)? as u32;
995
996            Ok(Some(ReplicationEvent::Begin {
997                final_lsn,
998                commit_time_micros,
999                xid,
1000            }))
1001        }
1002        b'C' => {
1003            let _flags = take_i8(&mut p)?;
1004            let lsn = Lsn::from_u64(take_i64(&mut p)? as u64); // should be safe
1005            let end_lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
1006            let commit_time_micros = take_i64(&mut p)?;
1007
1008            Ok(Some(ReplicationEvent::Commit {
1009                lsn,
1010                end_lsn,
1011                commit_time_micros,
1012            }))
1013        }
1014        b'M' => {
1015            // Logical decoding message (pg_logical_emit_message)
1016            // Wire: flags(1) + lsn(8) + prefix(null-terminated) + content_len(4) + content(n)
1017            let flags = take_i8(&mut p)?;
1018            let transactional = (flags & 1) != 0;
1019            let lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
1020
1021            // Read null-terminated prefix string
1022            let prefix_end = p.iter().position(|&b| b == 0).ok_or_else(|| {
1023                PgWireError::Protocol("pgoutput Message: missing null terminator for prefix".into())
1024            })?;
1025            let prefix = String::from_utf8_lossy(&p[..prefix_end]).into_owned();
1026            p = &p[prefix_end + 1..]; // advance past null byte
1027
1028            let content_len = take_i32(&mut p)? as usize;
1029            if p.len() < content_len {
1030                return Err(PgWireError::Protocol(format!(
1031                    "pgoutput Message: expected {} content bytes, got {}",
1032                    content_len,
1033                    p.len()
1034                )));
1035            }
1036            let content = Bytes::copy_from_slice(&p[..content_len]);
1037
1038            Ok(Some(ReplicationEvent::Message {
1039                transactional,
1040                lsn,
1041                prefix,
1042                content,
1043            }))
1044        }
1045        _ => Ok(None),
1046    }
1047}
1048
1049/// Read authentication response data for a specific auth code.
1050async fn read_auth_data<S: AsyncRead + AsyncWrite + Unpin>(
1051    stream: &mut S,
1052    expected_code: i32,
1053) -> Result<Vec<u8>> {
1054    loop {
1055        let msg = read_backend_message(stream).await?;
1056        match msg.tag {
1057            b'R' => {
1058                let (code, data) = parse_auth_request(&msg.payload)?;
1059                if code == expected_code {
1060                    return Ok(data.to_vec());
1061                }
1062                return Err(PgWireError::Auth(format!(
1063                    "unexpected auth code {code}, expected {expected_code}"
1064                )));
1065            }
1066            b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
1067            _ => {} // Skip other messages
1068        }
1069    }
1070}
1071
1072/// Get current time as PostgreSQL timestamp (microseconds since 2000-01-01).
1073fn current_pg_timestamp() -> i64 {
1074    use std::time::{SystemTime, UNIX_EPOCH};
1075
1076    let now = SystemTime::now()
1077        .duration_since(UNIX_EPOCH)
1078        .unwrap_or_default();
1079
1080    let unix_micros = (now.as_secs() as i64) * 1_000_000 + (now.subsec_micros() as i64);
1081    unix_micros - PG_EPOCH_MICROS
1082}
1083
1084/// Compute PostgreSQL MD5 password hash.
1085#[cfg(feature = "md5")]
1086fn postgres_md5(password: &str, user: &str, salt: &[u8; 4]) -> String {
1087    fn md5_hex(data: &[u8]) -> String {
1088        format!("{:x}", md5::compute(data))
1089    }
1090
1091    // First hash: md5(password + username)
1092    let inner = md5_hex(format!("{password}{user}").as_bytes());
1093
1094    // Second hash: md5(inner_hash + salt)
1095    let mut outer_input = inner.into_bytes();
1096    outer_input.extend_from_slice(salt);
1097
1098    format!("md5{}", md5_hex(&outer_input))
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use super::*;
1104
1105    #[test]
1106    fn parse_sasl_mechanisms_single() {
1107        let data = b"SCRAM-SHA-256\0\0";
1108        let mechs = parse_sasl_mechanisms(data);
1109        assert_eq!(mechs, vec!["SCRAM-SHA-256"]);
1110    }
1111
1112    #[test]
1113    fn parse_sasl_mechanisms_multiple() {
1114        let data = b"SCRAM-SHA-256\0SCRAM-SHA-256-PLUS\0\0";
1115        let mechs = parse_sasl_mechanisms(data);
1116        assert_eq!(mechs, vec!["SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"]);
1117    }
1118
1119    #[test]
1120    fn parse_sasl_mechanisms_empty() {
1121        let mechs = parse_sasl_mechanisms(b"\0");
1122        assert!(mechs.is_empty());
1123    }
1124
1125    #[test]
1126    #[cfg(feature = "md5")]
1127    fn postgres_md5_known_value() {
1128        // Test vector: user="md5_user", password="md5_pass", salt=[0x01, 0x02, 0x03, 0x04]
1129        // Can verify with: SELECT 'md5' || md5(md5('md5_passmd5_user') || E'\\x01020304');
1130        let hash = postgres_md5("md5_pass", "md5_user", &[0x01, 0x02, 0x03, 0x04]);
1131        assert!(hash.starts_with("md5"));
1132        assert_eq!(hash.len(), 35); // "md5" + 32 hex chars
1133    }
1134
1135    #[test]
1136    fn current_pg_timestamp_is_positive() {
1137        // Any time after 2000-01-01 should be positive
1138        let ts = current_pg_timestamp();
1139        assert!(ts > 0);
1140    }
1141
1142    /// Build a CopyData('d') frame wrapping an XLogData('w') replication message
1143    /// carrying a non-boundary pgoutput payload (so the worker forwards it as a
1144    /// raw `XLogData` event).
1145    fn xlog_frame(wal_end: u64, data: &[u8]) -> Vec<u8> {
1146        let mut payload = Vec::new();
1147        payload.push(b'w');
1148        payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); // wal_start
1149        payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); // wal_end
1150        payload.extend_from_slice(&0i64.to_be_bytes()); // server_time
1151        payload.extend_from_slice(data);
1152
1153        let mut frame = Vec::new();
1154        frame.push(b'd');
1155        frame.extend_from_slice(&((payload.len() + 4) as i32).to_be_bytes());
1156        frame.extend_from_slice(&payload);
1157        frame
1158    }
1159
1160    /// Build a `WorkerState` wired to in-memory channels for unit testing.
1161    /// Returns the worker plus the stop sender and event receiver (held so the
1162    /// channels stay open for the duration of the test).
1163    fn test_worker(
1164        cfg: ReplicationConfig,
1165    ) -> (
1166        WorkerState,
1167        watch::Sender<bool>,
1168        mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>,
1169    ) {
1170        let progress = Arc::new(SharedProgress::new(cfg.ack_lsn.unwrap_or(cfg.start_lsn)));
1171        let (stop_tx, stop_rx) = watch::channel(false);
1172        let (tx, rx) = mpsc::channel(16);
1173        let metrics = Arc::new(ReplicationMetrics::default());
1174        let worker = WorkerState::new(cfg, progress, stop_rx, tx, metrics);
1175        (worker, stop_tx, rx)
1176    }
1177
1178    /// The `options` startup parameter is how `libpq`/`PGOPTIONS` lets a caller
1179    /// pin session GUCs (e.g. `DateStyle`, `IntervalStyle`) on a connection.
1180    /// PostgreSQL honors it on replication connections too, so it must be
1181    /// forwarded into the startup message when the caller sets it, and left
1182    /// out entirely otherwise (server defaults apply).
1183    #[tokio::test]
1184    async fn startup_includes_options_when_set() {
1185        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1186
1187        let cfg = ReplicationConfig::new("h", "u", "p", "db", "slot", "pub")
1188            .with_options("-c datestyle=ISO,MDY -c intervalstyle=iso_8601");
1189        let (worker, _stop_tx, _rx) = test_worker(cfg);
1190        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1191
1192        worker.startup(&mut worker_end).await.unwrap();
1193        worker_end.shutdown().await.unwrap();
1194
1195        let mut sent = Vec::new();
1196        server.read_to_end(&mut sent).await.unwrap();
1197        let sent = String::from_utf8_lossy(&sent);
1198
1199        assert!(
1200            sent.contains("options"),
1201            "startup message missing 'options' key: {sent:?}"
1202        );
1203        assert!(
1204            sent.contains("-c datestyle=ISO,MDY -c intervalstyle=iso_8601"),
1205            "startup message missing options value: {sent:?}"
1206        );
1207    }
1208
1209    #[tokio::test]
1210    async fn startup_sends_the_application_name() {
1211        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1212
1213        for (cfg, expected) in [
1214            (ReplicationConfig::default(), "pgwire-replication"),
1215            (
1216                ReplicationConfig::default().with_application_name("df-7f3a"),
1217                "df-7f3a",
1218            ),
1219        ] {
1220            let (worker, _stop_tx, _rx) = test_worker(cfg);
1221            let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1222            worker.startup(&mut worker_end).await.unwrap();
1223            worker_end.shutdown().await.unwrap();
1224            let mut sent = Vec::new();
1225            server.read_to_end(&mut sent).await.unwrap();
1226            let pair = format!("application_name\0{expected}\0");
1227            assert!(
1228                sent.windows(pair.len()).any(|w| w == pair.as_bytes()),
1229                "startup message missing {pair:?}"
1230            );
1231        }
1232    }
1233
1234    #[tokio::test]
1235    async fn startup_omits_options_when_unset() {
1236        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1237
1238        let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
1239        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1240
1241        worker.startup(&mut worker_end).await.unwrap();
1242        worker_end.shutdown().await.unwrap();
1243
1244        let mut sent = Vec::new();
1245        server.read_to_end(&mut sent).await.unwrap();
1246        let sent = String::from_utf8_lossy(&sent);
1247
1248        assert!(
1249            !sent.contains("options"),
1250            "startup message should omit 'options' when unset: {sent:?}"
1251        );
1252    }
1253
1254    /// The worker must build its reader from `cfg.max_message_size`. Without
1255    /// that wiring an oversized frame is accepted and the limit is inert, so
1256    /// this fails if `stream_loop` goes back to a default `MessageReader`.
1257    #[tokio::test]
1258    async fn stream_loop_refuses_a_frame_over_the_configured_limit() {
1259        use tokio::io::AsyncWriteExt;
1260
1261        let cfg = ReplicationConfig {
1262            max_message_size: 64,
1263            ..Default::default()
1264        };
1265
1266        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1267        let (_stop_tx, stop_rx) = watch::channel(false);
1268        let (tx, mut rx) = mpsc::channel(16);
1269        let metrics = Arc::new(ReplicationMetrics::default());
1270        let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1271
1272        let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1273        let worker_task = tokio::spawn(async move {
1274            let mut buf = BufReader::with_capacity(1024, worker_end);
1275            worker.stream_loop(&mut buf).await
1276        });
1277
1278        // One frame past the 64-byte limit, then a frame the worker would
1279        // happily forward if it resynchronized past the refused one.
1280        test_end
1281            .write_all(&xlog_frame(1, &[b'x'; 200]))
1282            .await
1283            .unwrap();
1284        test_end.write_all(&xlog_frame(2, b"Ismall")).await.unwrap();
1285        test_end.flush().await.unwrap();
1286
1287        let err = tokio::time::timeout(std::time::Duration::from_secs(5), worker_task)
1288            .await
1289            .expect("stream_loop should end on the oversized frame")
1290            .unwrap()
1291            .expect_err("stream_loop should fail, not return Ok");
1292
1293        assert!(
1294            matches!(
1295                err,
1296                PgWireError::MessageTooLarge {
1297                    limit_bytes: 64,
1298                    ..
1299                }
1300            ),
1301            "expected MessageTooLarge, got {err:?}"
1302        );
1303        assert!(!err.is_transient());
1304
1305        // Neither the refused frame nor the one behind it reaches the consumer.
1306        assert!(
1307            rx.try_recv().is_err(),
1308            "no event should be emitted for a refused frame"
1309        );
1310        // And nothing was acknowledged.
1311        assert_eq!(metrics.last_applied_lsn(), Lsn(0));
1312    }
1313
1314    /// A backend frame: tag, length, payload.
1315    fn frame(tag: u8, payload: &[u8]) -> Vec<u8> {
1316        let mut f = vec![tag];
1317        f.extend_from_slice(&((payload.len() + 4) as i32).to_be_bytes());
1318        f.extend_from_slice(payload);
1319        f
1320    }
1321
1322    fn data_row(cols: &[Option<&[u8]>]) -> Vec<u8> {
1323        let mut p = (cols.len() as i16).to_be_bytes().to_vec();
1324        for c in cols {
1325            match c {
1326                Some(v) => {
1327                    p.extend_from_slice(&(v.len() as i32).to_be_bytes());
1328                    p.extend_from_slice(v);
1329                }
1330                None => p.extend_from_slice(&(-1i32).to_be_bytes()),
1331            }
1332        }
1333        frame(b'D', &p)
1334    }
1335
1336    /// Read one frontend message (tag + payload) from the server side.
1337    async fn read_frontend(server: &mut tokio::io::DuplexStream) -> (u8, Vec<u8>) {
1338        use tokio::io::AsyncReadExt;
1339        let tag = server.read_u8().await.unwrap();
1340        let len = server.read_i32().await.unwrap() as usize;
1341        let mut payload = vec![0; len - 4];
1342        server.read_exact(&mut payload).await.unwrap();
1343        (tag, payload)
1344    }
1345
1346    async fn gated_query(
1347        gate: &mpsc::Sender<GateCommand>,
1348        sql: &str,
1349    ) -> tokio::sync::oneshot::Receiver<Result<Vec<QueryRow>>> {
1350        let (reply, rx) = tokio::sync::oneshot::channel();
1351        gate.send(GateCommand::Query {
1352            sql: sql.into(),
1353            reply,
1354        })
1355        .await
1356        .unwrap();
1357        rx
1358    }
1359
1360    #[tokio::test]
1361    async fn a_gated_session_answers_queries_then_starts() {
1362        use tokio::io::AsyncWriteExt;
1363
1364        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1365        let (gate_tx, mut gate_rx) = mpsc::channel(1);
1366        let serve = tokio::spawn(async move {
1367            WorkerState::serve_gate(
1368                &mut gate_rx,
1369                &mut worker_end,
1370                crate::protocol::framing::MAX_MESSAGE_SIZE,
1371            )
1372            .await
1373        });
1374
1375        let reply = gated_query(&gate_tx, "IDENTIFY_SYSTEM").await;
1376        let (tag, payload) = read_frontend(&mut server).await;
1377        assert_eq!(tag, b'Q');
1378        assert_eq!(payload, b"IDENTIFY_SYSTEM\0");
1379        let mut answer = frame(b'T', &[0, 0]);
1380        answer.extend(data_row(&[
1381            Some(b"7"),
1382            Some(b"2"),
1383            Some(b"0/16B3748"),
1384            None,
1385        ]));
1386        answer.extend(frame(b'C', b"IDENTIFY_SYSTEM\0"));
1387        answer.extend(frame(b'Z', b"I"));
1388        server.write_all(&answer).await.unwrap();
1389        let rows = reply.await.unwrap().unwrap();
1390        assert_eq!(rows.len(), 1);
1391        assert_eq!(rows[0][0].as_deref(), Some(&b"7"[..]));
1392        assert_eq!(rows[0][3], None, "NULL stays NULL");
1393
1394        gate_tx.send(GateCommand::Start).await.unwrap();
1395        assert!(
1396            matches!(serve.await.unwrap(), Ok(true)),
1397            "Start ends the gate"
1398        );
1399    }
1400
1401    #[tokio::test]
1402    async fn a_server_error_keeps_the_gated_session_usable() {
1403        use tokio::io::AsyncWriteExt;
1404
1405        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1406        let (gate_tx, mut gate_rx) = mpsc::channel(1);
1407        let serve = tokio::spawn(async move {
1408            WorkerState::serve_gate(
1409                &mut gate_rx,
1410                &mut worker_end,
1411                crate::protocol::framing::MAX_MESSAGE_SIZE,
1412            )
1413            .await
1414        });
1415
1416        let reply = gated_query(&gate_tx, "TIMELINE_HISTORY 1").await;
1417        read_frontend(&mut server).await;
1418        let mut answer = frame(b'E', b"Mno history\0\0");
1419        answer.extend(frame(b'Z', b"I"));
1420        server.write_all(&answer).await.unwrap();
1421        assert!(matches!(reply.await.unwrap(), Err(PgWireError::Server(_))));
1422
1423        let reply = gated_query(&gate_tx, "SELECT 1").await;
1424        read_frontend(&mut server).await;
1425        let mut answer = data_row(&[Some(b"1")]);
1426        answer.extend(frame(b'Z', b"I"));
1427        server.write_all(&answer).await.unwrap();
1428        assert_eq!(reply.await.unwrap().unwrap().len(), 1);
1429
1430        drop(gate_tx);
1431        assert!(matches!(serve.await.unwrap(), Ok(false)));
1432    }
1433
1434    #[tokio::test]
1435    async fn a_closed_gate_terminates_without_starting() {
1436        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1437        let (gate_tx, mut gate_rx) = mpsc::channel::<GateCommand>(1);
1438        drop(gate_tx);
1439        let started = WorkerState::serve_gate(
1440            &mut gate_rx,
1441            &mut worker_end,
1442            crate::protocol::framing::MAX_MESSAGE_SIZE,
1443        )
1444        .await
1445        .unwrap();
1446        assert!(!started);
1447        assert_eq!(read_frontend(&mut server).await.0, b'X', "Terminate sent");
1448    }
1449
1450    #[tokio::test]
1451    async fn a_broken_session_ends_the_gate_with_its_error() {
1452        let (mut worker_end, server) = tokio::io::duplex(64 * 1024);
1453        let (gate_tx, mut gate_rx) = mpsc::channel(1);
1454        let serve = tokio::spawn(async move {
1455            WorkerState::serve_gate(
1456                &mut gate_rx,
1457                &mut worker_end,
1458                crate::protocol::framing::MAX_MESSAGE_SIZE,
1459            )
1460            .await
1461        });
1462        let reply = gated_query(&gate_tx, "IDENTIFY_SYSTEM").await;
1463        drop(server);
1464        assert!(reply.await.unwrap().is_err());
1465        assert!(serve.await.unwrap().is_err(), "the session ends");
1466    }
1467
1468    #[tokio::test]
1469    async fn a_gated_query_row_over_the_limit_ends_the_session() {
1470        use tokio::io::AsyncWriteExt;
1471
1472        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1473        let (gate_tx, mut gate_rx) = mpsc::channel(1);
1474        let serve = tokio::spawn(async move {
1475            WorkerState::serve_gate(&mut gate_rx, &mut worker_end, 64).await
1476        });
1477
1478        let reply = gated_query(&gate_tx, "SELECT big").await;
1479        read_frontend(&mut server).await;
1480        let mut answer = data_row(&[Some(&[b'x'; 200])]);
1481        answer.extend(frame(b'Z', b"I"));
1482        server.write_all(&answer).await.unwrap();
1483        assert!(matches!(
1484            reply.await.unwrap(),
1485            Err(PgWireError::MessageTooLarge {
1486                limit_bytes: 64,
1487                ..
1488            })
1489        ));
1490        assert!(
1491            matches!(
1492                serve.await.unwrap(),
1493                Err(PgWireError::MessageTooLarge { .. })
1494            ),
1495            "the session ends"
1496        );
1497    }
1498
1499    #[test]
1500    fn a_malformed_data_row_is_a_protocol_error() {
1501        let truncated = Bytes::from_static(&[0, 1, 0, 0, 0, 5, b'a']);
1502        assert!(matches!(
1503            parse_data_row(&truncated),
1504            Err(PgWireError::Protocol(_))
1505        ));
1506    }
1507
1508    #[tokio::test]
1509    async fn start_replication_returns_ok_on_copy_both_response() {
1510        use std::time::Duration;
1511        use tokio::io::AsyncWriteExt;
1512
1513        let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
1514        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1515
1516        // Server accepts the stream: CopyBothResponse ('W', empty payload).
1517        server.write_all(&[b'W', 0, 0, 0, 4]).await.unwrap();
1518
1519        let res = tokio::time::timeout(
1520            Duration::from_secs(5),
1521            worker.start_replication(&mut worker_end),
1522        )
1523        .await;
1524        assert!(matches!(res, Ok(Ok(()))), "expected Ok on 'W', got {res:?}");
1525    }
1526
1527    #[tokio::test]
1528    async fn start_replication_surfaces_server_error_response() {
1529        use std::time::Duration;
1530        use tokio::io::AsyncWriteExt;
1531
1532        let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
1533        let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1534
1535        // Server rejects the stream: ErrorResponse ('E') with an empty field list.
1536        server.write_all(&[b'E', 0, 0, 0, 5, 0]).await.unwrap();
1537
1538        let res = tokio::time::timeout(
1539            Duration::from_secs(5),
1540            worker.start_replication(&mut worker_end),
1541        )
1542        .await;
1543        assert!(
1544            matches!(res, Ok(Err(PgWireError::Server(_)))),
1545            "expected Server error on 'E', got {res:?}"
1546        );
1547    }
1548
1549    /// Regression test for the keepalive/feedback starvation bug
1550    /// (spiceai/spiceai#11616): when the event channel fills and the consumer
1551    /// never drains, the worker must keep emitting standby-status feedback so
1552    /// the server does not hit `wal_sender_timeout` and reset the connection.
1553    #[tokio::test]
1554    async fn feedback_continues_under_backpressure() {
1555        use std::time::Duration;
1556        use tokio::io::AsyncWriteExt;
1557
1558        let cfg = ReplicationConfig {
1559            status_interval: Duration::from_millis(40),
1560            idle_wakeup_interval: Duration::from_secs(10),
1561            buffer_events: 2,
1562            ..Default::default()
1563        };
1564
1565        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1566        let (stop_tx, stop_rx) = watch::channel(false);
1567        // Receiver is held but never drained, so the channel stays full.
1568        let (tx, _rx_never_drained) = mpsc::channel(cfg.buffer_events);
1569        let metrics = Arc::new(ReplicationMetrics::default());
1570        let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1571
1572        let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1573
1574        let worker_task = tokio::spawn(async move {
1575            let mut buf = BufReader::with_capacity(1024, worker_end);
1576            let _ = worker.stream_loop(&mut buf).await;
1577        });
1578
1579        // Feed more messages than the 2-slot channel can hold; the worker
1580        // forwards two, then parks awaiting capacity on the third.
1581        for i in 1..=5u64 {
1582            test_end.write_all(&xlog_frame(i, b"Idummy")).await.unwrap();
1583        }
1584        test_end.flush().await.unwrap();
1585
1586        // A feedback message is a CopyData('d') whose payload is a
1587        // StandbyStatusUpdate('r'). Assert several arrive while the channel is
1588        // full — i.e. feedback survives backpressure, not just the initial one.
1589        let mut reader = MessageReader::new();
1590        let mut feedback_count = 0;
1591        let deadline = Instant::now() + Duration::from_millis(500);
1592        while feedback_count < 3 {
1593            let remaining = deadline.saturating_duration_since(Instant::now());
1594            if remaining.is_zero() {
1595                break;
1596            }
1597            match tokio::time::timeout(remaining, reader.read(&mut test_end)).await {
1598                Ok(Ok(msg)) if msg.tag == b'd' && msg.payload.first() == Some(&b'r') => {
1599                    feedback_count += 1;
1600                }
1601                Ok(Ok(_)) => continue,
1602                _ => break,
1603            }
1604        }
1605
1606        assert!(
1607            feedback_count >= 3,
1608            "expected repeated feedback under backpressure, got {feedback_count}"
1609        );
1610        assert!(
1611            metrics.stall_count() >= 1,
1612            "expected at least one stall to be recorded"
1613        );
1614        assert!(
1615            metrics.feedback_sent() >= 3,
1616            "feedback_sent metric should track sends, got {}",
1617            metrics.feedback_sent()
1618        );
1619
1620        let _ = stop_tx.send(true);
1621        let _ = worker_task.await;
1622    }
1623
1624    /// Feedback under backpressure must be *rate-limited* to `status_interval`,
1625    /// not emitted on every loop iteration. Guards the stall-loop deadline math:
1626    /// a wrong sign (deadline in the past) would busy-loop and flood the server.
1627    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1628    async fn feedback_under_backpressure_is_rate_limited() {
1629        use std::time::Duration;
1630        use tokio::io::AsyncWriteExt;
1631
1632        let interval = Duration::from_millis(50);
1633        let cfg = ReplicationConfig {
1634            status_interval: interval,
1635            idle_wakeup_interval: Duration::from_secs(10),
1636            buffer_events: 2,
1637            ..Default::default()
1638        };
1639
1640        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1641        let (stop_tx, stop_rx) = watch::channel(false);
1642        let (tx, _rx_never_drained) = mpsc::channel(cfg.buffer_events);
1643        let metrics = Arc::new(ReplicationMetrics::default());
1644        let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1645
1646        let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1647        let worker_task = tokio::spawn(async move {
1648            let mut buf = BufReader::with_capacity(1024, worker_end);
1649            let _ = worker.stream_loop(&mut buf).await;
1650        });
1651
1652        for i in 1..=5u64 {
1653            test_end.write_all(&xlog_frame(i, b"Idummy")).await.unwrap();
1654        }
1655        test_end.flush().await.unwrap();
1656
1657        // Over a fixed window, correct behavior emits roughly window/interval
1658        // feedbacks (~7 here). A deadline that fires every iteration would emit
1659        // far more, so a generous upper bound catches the flooding regression
1660        // while the lower bound confirms feedback keeps flowing.
1661        let window = Duration::from_millis(350);
1662        let end = Instant::now() + window;
1663        let mut reader = MessageReader::new();
1664        let mut feedback = 0usize;
1665        loop {
1666            let remaining = end.saturating_duration_since(Instant::now());
1667            if remaining.is_zero() {
1668                break;
1669            }
1670            match tokio::time::timeout(remaining, reader.read(&mut test_end)).await {
1671                Ok(Ok(msg)) if msg.tag == b'd' && msg.payload.first() == Some(&b'r') => {
1672                    feedback += 1;
1673                }
1674                Ok(Ok(_)) => {}
1675                _ => break,
1676            }
1677        }
1678
1679        assert!(
1680            feedback >= 2,
1681            "feedback should keep flowing under backpressure, got {feedback}"
1682        );
1683        assert!(
1684            feedback <= 25,
1685            "feedback must be rate-limited to ~status_interval, got {feedback}"
1686        );
1687
1688        let _ = stop_tx.send(true);
1689        let _ = worker_task.await;
1690    }
1691
1692    // ── Change-driven standby status updates ──────────────────────────────
1693
1694    /// A KeepAlive ('k') CopyData frame.
1695    fn keepalive_frame(wal_end: u64, reply_requested: bool) -> Vec<u8> {
1696        let mut payload = vec![b'k'];
1697        payload.extend_from_slice(&(wal_end as i64).to_be_bytes());
1698        payload.extend_from_slice(&0i64.to_be_bytes());
1699        payload.push(u8::from(reply_requested));
1700        let mut frame = vec![b'd'];
1701        frame.extend_from_slice(&((payload.len() + 4) as i32).to_be_bytes());
1702        frame.extend_from_slice(&payload);
1703        frame
1704    }
1705
1706    /// A status update read by the scripted server.
1707    #[derive(Debug, PartialEq)]
1708    struct Status {
1709        applied: u64,
1710        reply: bool,
1711    }
1712
1713    /// The scripted server end of a worker's stream.
1714    struct Server {
1715        end: tokio::io::DuplexStream,
1716        reader: MessageReader,
1717    }
1718
1719    impl Server {
1720        /// The next standby status update, skipping anything else.
1721        async fn status(&mut self) -> Status {
1722            loop {
1723                let msg = self.reader.read(&mut self.end).await.expect("stream open");
1724                if msg.tag == b'd' && msg.payload.first() == Some(&b'r') {
1725                    let p = &msg.payload;
1726                    return Status {
1727                        applied: u64::from_be_bytes(p[1..9].try_into().unwrap()),
1728                        reply: p[33] == 1,
1729                    };
1730                }
1731            }
1732        }
1733
1734        /// No status update arrives within `within`.
1735        async fn quiet(&mut self, within: std::time::Duration) -> bool {
1736            tokio::time::timeout(within, self.status()).await.is_err()
1737        }
1738
1739        async fn send(&mut self, frame: &[u8]) {
1740            use tokio::io::AsyncWriteExt;
1741            self.end.write_all(frame).await.unwrap();
1742            self.end.flush().await.unwrap();
1743        }
1744    }
1745
1746    struct Running {
1747        /// When the worker was started (tokio's clock).
1748        started: Instant,
1749        server: Server,
1750        task: tokio::task::JoinHandle<Result<()>>,
1751        stop: watch::Sender<bool>,
1752        _events: mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>,
1753    }
1754
1755    fn feedback_cfg() -> ReplicationConfig {
1756        ReplicationConfig {
1757            status_interval: std::time::Duration::from_secs(1),
1758            idle_wakeup_interval: std::time::Duration::from_secs(10),
1759            ..Default::default()
1760        }
1761    }
1762
1763    /// Run `stream_loop` against a scripted server over a duplex of `capacity`
1764    /// bytes.
1765    fn run_stream(progress: &Arc<SharedProgress>, capacity: usize) -> Running {
1766        run_stream_with(feedback_cfg(), progress, capacity)
1767    }
1768
1769    fn run_stream_with(
1770        cfg: ReplicationConfig,
1771        progress: &Arc<SharedProgress>,
1772        capacity: usize,
1773    ) -> Running {
1774        let (stop, stop_rx) = watch::channel(false);
1775        let (tx, events) = mpsc::channel(16);
1776        let mut worker = WorkerState::new(
1777            cfg,
1778            Arc::clone(progress),
1779            stop_rx,
1780            tx,
1781            Arc::new(ReplicationMetrics::default()),
1782        );
1783        let (worker_end, end) = tokio::io::duplex(capacity);
1784        let task = tokio::spawn(async move {
1785            let mut buf = BufReader::with_capacity(1024, worker_end);
1786            worker.stream_loop(&mut buf).await
1787        });
1788        Running {
1789            started: Instant::now(),
1790            server: Server {
1791                end,
1792                reader: MessageReader::new(),
1793            },
1794            task,
1795            stop,
1796            _events: events,
1797        }
1798    }
1799
1800    const SECOND: std::time::Duration = std::time::Duration::from_secs(1);
1801
1802    #[tokio::test(start_paused = true)]
1803    async fn an_update_is_sent_within_the_status_interval() {
1804        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1805        let mut r = run_stream(&progress, 64 * 1024);
1806        assert_eq!(
1807            r.server.status().await,
1808            Status {
1809                applied: 0,
1810                reply: false
1811            }
1812        );
1813        // Idle with nothing new: no status before the idle wakeup.
1814        assert!(r.server.quiet(3 * SECOND).await);
1815
1816        // The interval since the last status has passed: sent at once.
1817        let t = Instant::now();
1818        progress.update_applied(Lsn(100));
1819        assert_eq!(
1820            r.server.status().await,
1821            Status {
1822                applied: 100,
1823                reply: false
1824            }
1825        );
1826        assert!(t.elapsed() < SECOND);
1827
1828        // The next one waits for the interval, no longer.
1829        progress.update_applied(Lsn(200));
1830        let t = Instant::now();
1831        assert_eq!(
1832            r.server.status().await,
1833            Status {
1834                applied: 200,
1835                reply: false
1836            }
1837        );
1838        assert!(t.elapsed() <= SECOND, "took {:?}", t.elapsed());
1839        r.task.abort();
1840    }
1841
1842    #[tokio::test(start_paused = true)]
1843    async fn coalesced_updates_send_the_newest_once() {
1844        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1845        let mut r = run_stream(&progress, 64 * 1024);
1846        assert_eq!(r.server.status().await.applied, 0);
1847        let t = Instant::now();
1848        for lsn in 1..=100 {
1849            progress.update_applied(Lsn(lsn));
1850        }
1851        assert_eq!(
1852            r.server.status().await,
1853            Status {
1854                applied: 100,
1855                reply: false
1856            }
1857        );
1858        assert!(t.elapsed() <= SECOND);
1859        // Nothing else until the idle wakeup.
1860        assert!(r.server.quiet(8 * SECOND).await);
1861        r.task.abort();
1862    }
1863
1864    #[tokio::test(start_paused = true)]
1865    async fn an_update_during_a_send_stays_pending() {
1866        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1867        // Too small for one status message: the first send blocks mid-write.
1868        let mut r = run_stream(&progress, 8);
1869        for _ in 0..10 {
1870            tokio::task::yield_now().await;
1871        }
1872        progress.update_applied(Lsn(300));
1873        // The blocked send carries the position loaded before the update...
1874        assert_eq!(r.server.status().await.applied, 0);
1875        // ...and the update follows within the interval.
1876        let t = Instant::now();
1877        assert_eq!(r.server.status().await.applied, 300);
1878        assert!(t.elapsed() <= SECOND);
1879        r.task.abort();
1880    }
1881
1882    #[tokio::test(start_paused = true)]
1883    async fn a_new_session_starts_with_the_newest_position() {
1884        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1885        progress.update_applied(Lsn(500));
1886        let mut r = run_stream(&progress, 64 * 1024);
1887        assert_eq!(r.server.status().await.applied, 500);
1888        r.task.abort();
1889    }
1890
1891    #[tokio::test(start_paused = true)]
1892    async fn a_failed_send_leaves_the_position_for_the_next_session() {
1893        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1894        let mut r = run_stream(&progress, 64 * 1024);
1895        assert_eq!(r.server.status().await.applied, 0);
1896        progress.update_applied(Lsn(700));
1897        // The server goes away before the pending status is written.
1898        drop(r.server);
1899        let ended = tokio::time::timeout(2 * SECOND, r.task).await.unwrap();
1900        assert!(ended.unwrap().is_err(), "the failed send ends the session");
1901        let mut next = run_stream(&progress, 64 * 1024);
1902        assert_eq!(next.server.status().await.applied, 700);
1903        next.task.abort();
1904    }
1905
1906    #[tokio::test(start_paused = true)]
1907    async fn a_keepalive_requesting_a_reply_is_answered_at_once() {
1908        let progress = Arc::new(SharedProgress::new(Lsn(40)));
1909        let mut r = run_stream(&progress, 64 * 1024);
1910        assert_eq!(r.server.status().await.applied, 40);
1911        // No local change, inside the status interval.
1912        let t = Instant::now();
1913        r.server.send(&keepalive_frame(90, true)).await;
1914        assert_eq!(
1915            r.server.status().await,
1916            Status {
1917                applied: 40,
1918                reply: true
1919            }
1920        );
1921        assert!(t.elapsed() < SECOND);
1922        r.task.abort();
1923    }
1924
1925    #[tokio::test(start_paused = true)]
1926    async fn a_completely_idle_connection_keeps_sending_status() {
1927        let progress = Arc::new(SharedProgress::new(Lsn(10)));
1928        let mut r = run_stream(&progress, 64 * 1024);
1929        let t = Instant::now();
1930        let mut statuses = 0;
1931        while t.elapsed() < std::time::Duration::from_secs(65) {
1932            assert_eq!(r.server.status().await.applied, 10);
1933            statuses += 1;
1934        }
1935        // One at start, then one per idle wakeup (10 s): not none, which
1936        // would let wal_sender_timeout disconnect, and not one per interval.
1937        assert!((6..=8).contains(&statuses), "{statuses} statuses in 65 s");
1938        assert!(!r.task.is_finished());
1939        r.task.abort();
1940    }
1941
1942    #[tokio::test(start_paused = true)]
1943    async fn stopping_with_the_timer_armed_ends_at_once() {
1944        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1945        let mut r = run_stream(&progress, 64 * 1024);
1946        assert_eq!(r.server.status().await.applied, 0);
1947        progress.update_applied(Lsn(900)); // pending until the interval passes
1948        let t = Instant::now();
1949        r.stop.send(true).unwrap();
1950        let ended = tokio::time::timeout(SECOND, r.task)
1951            .await
1952            .expect("the worker ends without waiting for the timer");
1953        assert!(ended.unwrap().is_ok());
1954        assert!(t.elapsed() < SECOND);
1955    }
1956
1957    /// Raises from another thread race the worker clearing its signal and
1958    /// reading the position: whatever the interleaving, the newest position of
1959    /// every burst is sent within the status interval once the burst ends.
1960    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1961    async fn racing_raises_never_strand_the_newest_position() {
1962        use std::time::Duration;
1963        let interval = Duration::from_millis(50);
1964        let cfg = ReplicationConfig {
1965            status_interval: interval,
1966            idle_wakeup_interval: Duration::from_secs(60),
1967            ..Default::default()
1968        };
1969        let progress = Arc::new(SharedProgress::new(Lsn(0)));
1970        let mut r = run_stream_with(cfg, &progress, 64 * 1024);
1971        let mut newest = 0u64;
1972        for burst in 0..50u64 {
1973            let p = Arc::clone(&progress);
1974            let from = newest;
1975            let len = 1 + (burst * 7919) % 5000;
1976            newest = std::thread::spawn(move || {
1977                for lsn in from + 1..=from + len {
1978                    p.update_applied(Lsn(lsn));
1979                }
1980                from + len
1981            })
1982            .join()
1983            .unwrap();
1984            let ended = Instant::now();
1985            loop {
1986                let status = tokio::time::timeout(4 * interval, r.server.status())
1987                    .await
1988                    .unwrap_or_else(|_| {
1989                        panic!("burst {burst}: {newest} not sent within {:?}", 4 * interval)
1990                    });
1991                if status.applied == newest {
1992                    break;
1993                }
1994                assert!(status.applied < newest);
1995            }
1996            assert!(ended.elapsed() <= 4 * interval);
1997        }
1998        r.task.abort();
1999    }
2000
2001    // ── Feedback observer ─────────────────────────────────────────────────
2002
2003    type Seen = Arc<std::sync::Mutex<Vec<std::time::Duration>>>;
2004
2005    fn observed_cfg() -> (ReplicationConfig, Seen) {
2006        let seen: Seen = Arc::default();
2007        let sink = Arc::clone(&seen);
2008        let cfg = feedback_cfg().with_feedback_observer(crate::FeedbackObserver::new(move |s| {
2009            sink.lock().unwrap().push(s.pending_for)
2010        }));
2011        (cfg, seen)
2012    }
2013
2014    fn seen(s: &Seen) -> Vec<std::time::Duration> {
2015        s.lock().unwrap().clone()
2016    }
2017
2018    #[tokio::test(start_paused = true)]
2019    async fn the_observer_reports_how_long_a_position_waited() {
2020        let (cfg, s) = observed_cfg();
2021        let progress = Arc::new(SharedProgress::with_timing(Lsn(0), true));
2022        let mut r = run_stream_with(cfg, &progress, 64 * 1024);
2023        assert_eq!(r.server.status().await.applied, 0);
2024        assert!(
2025            seen(&s).is_empty(),
2026            "nothing was pending at the first status"
2027        );
2028        assert!(r.server.quiet(3 * SECOND).await);
2029
2030        // Due at once: no wait.
2031        progress.update_applied(Lsn(100));
2032        assert_eq!(r.server.status().await.applied, 100);
2033        // Waits for the interval.
2034        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2035        progress.update_applied(Lsn(200));
2036        assert_eq!(r.server.status().await.applied, 200);
2037        assert_eq!(
2038            seen(&s),
2039            vec![
2040                std::time::Duration::ZERO,
2041                std::time::Duration::from_millis(900)
2042            ]
2043        );
2044        r.task.abort();
2045    }
2046
2047    #[tokio::test(start_paused = true)]
2048    async fn coalesced_raises_report_the_oldest_wait() {
2049        let (cfg, s) = observed_cfg();
2050        let progress = Arc::new(SharedProgress::with_timing(Lsn(0), true));
2051        let mut r = run_stream_with(cfg, &progress, 64 * 1024);
2052        assert_eq!(r.server.status().await.applied, 0);
2053        for (at_ms, lsn) in [(100, 1), (500, 2), (900, 3)] {
2054            tokio::time::sleep_until(r.started + std::time::Duration::from_millis(at_ms)).await;
2055            progress.update_applied(Lsn(lsn));
2056        }
2057        assert_eq!(r.server.status().await.applied, 3);
2058        assert_eq!(seen(&s), vec![std::time::Duration::from_millis(900)]);
2059        r.task.abort();
2060    }
2061
2062    #[tokio::test(start_paused = true)]
2063    async fn no_observation_when_the_position_does_not_advance() {
2064        let (cfg, s) = observed_cfg();
2065        let progress = Arc::new(SharedProgress::with_timing(Lsn(40), true));
2066        let mut r = run_stream_with(cfg, &progress, 64 * 1024);
2067        assert_eq!(r.server.status().await.applied, 40);
2068        r.server.send(&keepalive_frame(90, true)).await;
2069        assert!(r.server.status().await.reply);
2070        progress.update_applied(Lsn(40)); // not a raise
2071        for _ in 0..3 {
2072            r.server.status().await; // idle wakeups
2073        }
2074        assert!(seen(&s).is_empty());
2075        assert_eq!(progress.pending_since(), None);
2076        r.task.abort();
2077    }
2078
2079    #[tokio::test(start_paused = true)]
2080    async fn a_failed_write_carries_its_wait_into_the_next_session() {
2081        let (cfg, s) = observed_cfg();
2082        let progress = Arc::new(SharedProgress::with_timing(Lsn(0), true));
2083        let mut r = run_stream_with(cfg.clone(), &progress, 64 * 1024);
2084        assert_eq!(r.server.status().await.applied, 0);
2085        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2086        progress.update_applied(Lsn(700));
2087        drop(r.server);
2088        assert!(r.task.await.unwrap().is_err(), "the write fails");
2089        let since = progress.pending_since().expect("still pending");
2090
2091        // The replacement session starts from the acknowledged position and
2092        // carries the pending start over.
2093        tokio::time::sleep(2 * SECOND).await;
2094        let next = Arc::new(SharedProgress::with_timing(Lsn(700), true));
2095        next.carry_pending(since);
2096        let mut r = run_stream_with(cfg, &next, 64 * 1024);
2097        assert_eq!(r.server.status().await.applied, 700);
2098        let waited = seen(&s);
2099        assert_eq!(waited.len(), 1);
2100        assert!(waited[0] >= 2 * SECOND, "{waited:?}");
2101        r.task.abort();
2102    }
2103
2104    #[tokio::test(start_paused = true)]
2105    async fn nothing_is_timed_without_an_observer() {
2106        let progress = Arc::new(SharedProgress::with_timing(Lsn(0), false));
2107        progress.update_applied(Lsn(5));
2108        assert_eq!(progress.pending_since(), None);
2109    }
2110
2111    #[tokio::test(start_paused = true)]
2112    async fn a_raise_during_a_write_keeps_a_pending_start() {
2113        let (cfg, s) = observed_cfg();
2114        let progress = Arc::new(SharedProgress::with_timing(Lsn(0), true));
2115        progress.update_applied(Lsn(1));
2116        // Too small for one status message: the first write blocks.
2117        let mut r = run_stream_with(cfg, &progress, 8);
2118        for _ in 0..10 {
2119            tokio::task::yield_now().await;
2120        }
2121        progress.update_applied(Lsn(2));
2122        assert_eq!(r.server.status().await.applied, 1);
2123        assert_eq!(r.server.status().await.applied, 2);
2124        assert_eq!(seen(&s).len(), 2, "both waits are reported");
2125        r.task.abort();
2126    }
2127
2128    /// A write that repeats the reported position (an idle wakeup) completing
2129    /// while a newer raise is pending does not report that raise; the write
2130    /// that carries it does, once.
2131    #[tokio::test(start_paused = true)]
2132    async fn a_repeated_position_does_not_report_a_pending_raise() {
2133        let (cfg, s) = observed_cfg();
2134        let progress = Arc::new(SharedProgress::with_timing(Lsn(40), true));
2135        // Too small for one status message: writes block until read.
2136        let mut r = run_stream_with(cfg, &progress, 8);
2137        assert_eq!(r.server.status().await.applied, 40);
2138        // The idle wakeup (10 s) writes 40 again and blocks unread; raise
2139        // while it is in flight.
2140        tokio::time::sleep_until(r.started + std::time::Duration::from_millis(10_500)).await;
2141        progress.update_applied(Lsn(50));
2142        assert_eq!(r.server.status().await.applied, 40);
2143        assert!(
2144            seen(&s).is_empty(),
2145            "the repeated position reported nothing"
2146        );
2147        assert_eq!(r.server.status().await.applied, 50);
2148        assert_eq!(seen(&s).len(), 1);
2149        r.task.abort();
2150    }
2151}