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
21pub 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 #[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#[derive(Debug, Clone)]
62pub enum ReplicationEvent {
63 KeepAlive {
65 wal_end: Lsn,
67 reply_requested: bool,
69 server_time_micros: i64,
71 },
72
73 Begin {
75 final_lsn: Lsn,
76 xid: u32,
77 commit_time_micros: i64,
78 },
79
80 XLogData {
82 wal_start: Lsn,
84 wal_end: Lsn,
86 server_time_micros: i64,
88 data: Bytes,
90 },
91
92 Commit {
94 lsn: Lsn,
95 end_lsn: Lsn,
96 commit_time_micros: i64,
97 },
98
99 Message {
105 transactional: bool,
107 lsn: Lsn,
109 prefix: String,
111 content: Bytes,
113 },
114
115 StoppedAt {
120 reached: Lsn,
122 },
123}
124
125pub type ReplicationEventReceiver =
127 mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>;
128
129pub 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 pub async fn run_on_stream<S: AsyncRead + AsyncWrite + Unpin>(
157 &mut self,
158 stream: &mut S,
159 ) -> Result<()> {
160 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 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, ¶ms).await
180 }
181
182 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 loop {
204 let msg = read_backend_message(stream).await?;
205 match msg.tag {
206 b'W' => return Ok(()), b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
208 b'N' | b'S' | b'K' => continue, _ => continue,
210 }
211 }
212 }
213
214 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 let mut reader = MessageReader::new();
232 const DRAIN_BATCH: usize = 256;
235
236 loop {
237 let current_applied = self.progress.load_applied();
239 if current_applied != last_applied {
240 last_applied = current_applied;
241 }
242
243 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 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 drained > 0 {
277 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 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 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 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 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, };
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 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); }
422 }
423
424 return Ok(false);
425 }
426 if let Some(stop_lsn) = self.cfg.stop_at_lsn {
429 if wal_end >= stop_lsn {
430 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 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 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 Err(mpsc::error::TrySendError::Full(())) => {}
520 }
521
522 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 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' => {} b'Z' => return Ok(()), _ => {}
578 }
579 }
580 }
581
582 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(()), 3 => {
592 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 self.auth_scram(stream, data).await
600 }
601 #[cfg(feature = "md5")]
602 5 => {
603 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 async fn auth_scram<S: AsyncRead + AsyncWrite + Unpin>(
625 &mut self,
626 stream: &mut S,
627 mechanisms_data: &[u8],
628 ) -> Result<()> {
629 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 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 let server_first = read_auth_data(stream, 11).await?;
658 let server_first_str = String::from_utf8_lossy(&server_first);
659
660 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 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 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
689fn 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; }
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); 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 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 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..]; 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
803async 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 _ => {} }
823 }
824}
825
826fn 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#[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 let inner = md5_hex(format!("{password}{user}").as_bytes());
847
848 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 let hash = postgres_md5("md5_pass", "md5_user", &[0x01, 0x02, 0x03, 0x04]);
885 assert!(hash.starts_with("md5"));
886 assert_eq!(hash.len(), 35); }
888
889 #[test]
890 fn current_pg_timestamp_is_positive() {
891 let ts = current_pg_timestamp();
893 assert!(ts > 0);
894 }
895
896 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()); payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); payload.extend_from_slice(&0i64.to_be_bytes()); 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 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.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.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 #[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 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 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 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 #[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 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}