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 mut params = vec![
173 ("user", self.cfg.user.as_str()),
174 ("database", self.cfg.database.as_str()),
175 ("replication", "database"),
176 ("client_encoding", "UTF8"),
177 ("application_name", "pgwire-replication"),
178 ];
179 if let Some(options) = self.cfg.options.as_deref() {
184 params.push(("options", options));
185 }
186 write_startup_message(stream, 196608, ¶ms).await
187 }
188
189 async fn start_replication<S: AsyncRead + AsyncWrite + Unpin>(
191 &self,
192 stream: &mut S,
193 ) -> Result<()> {
194 let binary_opt = if self.cfg.binary {
195 ", binary 'true'"
196 } else {
197 ""
198 };
199 let sql = format!(
200 "START_REPLICATION SLOT {} LOGICAL {} \
201 (proto_version '1', publication_names '{}', messages 'true'{})",
202 self.cfg.slot,
203 self.cfg.start_lsn,
204 self.cfg.publication.to_option_value(),
205 binary_opt,
206 );
207 write_query(stream, &sql).await?;
208
209 loop {
211 let msg = read_backend_message(stream).await?;
212 match msg.tag {
213 b'W' => return Ok(()), b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
215 b'N' | b'S' | b'K' => continue, _ => continue,
217 }
218 }
219 }
220
221 async fn stream_loop<S: AsyncRead + AsyncWrite + Unpin>(
232 &mut self,
233 stream: &mut BufReader<S>,
234 ) -> Result<()> {
235 let mut last_status_sent = Instant::now() - self.cfg.status_interval;
236 let mut last_applied = self.progress.load_applied();
237 let mut reader = MessageReader::new();
239 const DRAIN_BATCH: usize = 256;
242
243 loop {
244 let current_applied = self.progress.load_applied();
246 if current_applied != last_applied {
247 last_applied = current_applied;
248 }
249
250 if last_status_sent.elapsed() >= self.cfg.status_interval {
252 self.send_feedback(stream, last_applied, false).await?;
253 last_status_sent = Instant::now();
254 }
255
256 let mut drained = 0usize;
261 while stream.buffer().len() >= 5 && drained < DRAIN_BATCH {
262 let msg = reader.read(stream).await?;
263 drained += 1;
264 if msg.tag == b'E' {
265 return Err(PgWireError::Server(parse_error_response(&msg.payload)));
266 }
267 if msg.tag == b'd'
268 && self
269 .handle_copy_data(
270 stream,
271 msg.payload,
272 &mut last_applied,
273 &mut last_status_sent,
274 )
275 .await?
276 {
277 return Ok(());
278 }
279 }
280
281 if drained > 0 {
284 if self.stop_rx.has_changed().unwrap_or(false) && *self.stop_rx.borrow() {
286 let _ = write_copy_done(stream).await;
287 return Ok(());
288 }
289 continue;
290 }
291
292 let msg = tokio::select! {
299 biased;
300
301 _ = self.stop_rx.changed() => {
302 if *self.stop_rx.borrow() {
303 let _ = write_copy_done(stream).await;
304 return Ok(());
305 }
306 continue;
307 }
308
309 msg_result = tokio::time::timeout(
310 self.cfg.idle_wakeup_interval,
311 reader.read(stream),
312 ) => {
313 match msg_result {
314 Ok(res) => res?,
315 Err(_) => {
316 let applied = self.progress.load_applied();
317 last_applied = applied;
318 self.send_feedback(stream, applied, false).await?;
319 last_status_sent = Instant::now();
320 continue;
321 }
322 }
323 }
324 };
325
326 if msg.tag == b'E' {
327 return Err(PgWireError::Server(parse_error_response(&msg.payload)));
328 }
329 if msg.tag == b'd'
330 && self
331 .handle_copy_data(
332 stream,
333 msg.payload,
334 &mut last_applied,
335 &mut last_status_sent,
336 )
337 .await?
338 {
339 return Ok(());
340 }
341 }
342 }
343
344 async fn handle_copy_data<S: AsyncRead + AsyncWrite + Unpin>(
346 &mut self,
347 stream: &mut BufReader<S>,
348 payload: Bytes,
349 last_applied: &mut Lsn,
350 last_status_sent: &mut Instant,
351 ) -> Result<bool> {
352 let cd = parse_copy_data(payload)?;
353
354 match cd {
355 ReplicationCopyData::KeepAlive {
356 wal_end,
357 server_time_micros,
358 reply_requested,
359 } => {
360 self.metrics.set_last_wal_end(wal_end);
361
362 if reply_requested {
364 let applied = self.progress.load_applied();
365 *last_applied = applied;
366 self.send_feedback(stream, applied, true).await?;
367 *last_status_sent = Instant::now();
368 self.metrics.record_keepalive_reply();
369 }
370
371 if self
372 .send_event_backpressured(
373 stream,
374 Ok(ReplicationEvent::KeepAlive {
375 wal_end,
376 reply_requested,
377 server_time_micros,
378 }),
379 last_status_sent,
380 )
381 .await?
382 {
383 return Ok(true);
384 }
385
386 Ok(false)
387 }
388 ReplicationCopyData::XLogData {
389 wal_start,
390 wal_end,
391 server_time_micros,
392 data,
393 } => {
394 self.metrics.set_last_wal_end(wal_end);
395
396 if let Some(boundary_ev) = parse_pgoutput_boundary(&data)? {
398 let reached_lsn = match boundary_ev {
399 ReplicationEvent::Begin { final_lsn, .. } => final_lsn,
400 ReplicationEvent::Commit { end_lsn, .. } => end_lsn,
401 _ => wal_end, };
403
404 if self
405 .send_event_backpressured(stream, Ok(boundary_ev), last_status_sent)
406 .await?
407 {
408 return Ok(true);
409 }
410
411 if let Some(stop_lsn) = self.cfg.stop_at_lsn {
413 if reached_lsn >= stop_lsn {
414 if self
415 .send_event_backpressured(
416 stream,
417 Ok(ReplicationEvent::StoppedAt {
418 reached: reached_lsn,
419 }),
420 last_status_sent,
421 )
422 .await?
423 {
424 return Ok(true);
425 }
426 let _ = write_copy_done(stream).await;
427 return Ok(true); }
429 }
430
431 return Ok(false);
432 }
433 if let Some(stop_lsn) = self.cfg.stop_at_lsn {
436 if wal_end >= stop_lsn {
437 if self
439 .send_event_backpressured(
440 stream,
441 Ok(ReplicationEvent::XLogData {
442 wal_start,
443 wal_end,
444 server_time_micros,
445 data,
446 }),
447 last_status_sent,
448 )
449 .await?
450 {
451 return Ok(true);
452 }
453
454 if self
455 .send_event_backpressured(
456 stream,
457 Ok(ReplicationEvent::StoppedAt { reached: wal_end }),
458 last_status_sent,
459 )
460 .await?
461 {
462 return Ok(true);
463 }
464
465 let _ = write_copy_done(stream).await;
466 return Ok(true);
467 }
468 }
469
470 if self
471 .send_event_backpressured(
472 stream,
473 Ok(ReplicationEvent::XLogData {
474 wal_start,
475 wal_end,
476 server_time_micros,
477 data,
478 }),
479 last_status_sent,
480 )
481 .await?
482 {
483 return Ok(true);
484 }
485
486 Ok(false)
487 }
488 }
489 }
490
491 async fn send_event_backpressured<S: AsyncWrite + Unpin>(
508 &mut self,
509 stream: &mut S,
510 event: std::result::Result<ReplicationEvent, PgWireError>,
511 last_status_sent: &mut Instant,
512 ) -> Result<bool> {
513 match self.out.try_reserve() {
515 Ok(permit) => {
516 permit.send(event);
517 self.metrics.record_event_forwarded();
518 return Ok(false);
519 }
520 Err(mpsc::error::TrySendError::Closed(())) => {
521 tracing::debug!("event channel closed, client may have disconnected");
522 return Ok(false);
523 }
524 Err(mpsc::error::TrySendError::Full(())) => {}
527 }
528
529 self.metrics.record_stall_begin();
532 let stall_start = Instant::now();
533 loop {
534 let deadline = *last_status_sent + self.cfg.status_interval;
535 tokio::select! {
536 biased;
537
538 _ = self.stop_rx.changed() => {
539 if *self.stop_rx.borrow() {
540 let _ = write_copy_done(stream).await;
541 return Ok(true);
542 }
543 }
544
545 permit = self.out.reserve() => {
546 self.metrics
547 .record_stall_end(stall_start.elapsed().as_micros() as u64);
548 match permit {
549 Ok(permit) => {
550 permit.send(event);
551 self.metrics.record_event_forwarded();
552 }
553 Err(_closed) => {
554 tracing::debug!("event channel closed while backpressured");
555 }
556 }
557 return Ok(false);
558 }
559
560 _ = tokio::time::sleep_until(deadline) => {
561 let applied = self.progress.load_applied();
562 self.send_feedback(stream, applied, false).await?;
563 *last_status_sent = Instant::now();
564 }
565 }
566 }
567 }
568
569 async fn authenticate<S: AsyncRead + AsyncWrite + Unpin>(
571 &mut self,
572 stream: &mut S,
573 ) -> Result<()> {
574 loop {
575 let msg = read_backend_message(stream).await?;
576 match msg.tag {
577 b'R' => {
578 let (code, rest) = parse_auth_request(&msg.payload)?;
579 self.handle_auth_request(stream, code, rest).await?;
580 }
581 b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
582 b'S' | b'K' => {} b'Z' => return Ok(()), _ => {}
585 }
586 }
587 }
588
589 async fn handle_auth_request<S: AsyncRead + AsyncWrite + Unpin>(
591 &mut self,
592 stream: &mut S,
593 code: i32,
594 data: &[u8],
595 ) -> Result<()> {
596 match code {
597 0 => Ok(()), 3 => {
599 let mut payload = Vec::from(self.cfg.password.as_bytes());
601 payload.push(0);
602 write_password_message(stream, &payload).await
603 }
604 10 => {
605 self.auth_scram(stream, data).await
607 }
608 #[cfg(feature = "md5")]
609 5 => {
610 if data.len() != 4 {
612 return Err(PgWireError::Protocol(
613 "MD5 auth: expected 4-byte salt".into(),
614 ));
615 }
616 let mut salt = [0u8; 4];
617 salt.copy_from_slice(&data[..4]);
618
619 let hash = postgres_md5(&self.cfg.password, &self.cfg.user, &salt);
620 let mut payload = hash.into_bytes();
621 payload.push(0);
622 write_password_message(stream, &payload).await
623 }
624 _ => Err(PgWireError::Auth(format!(
625 "unsupported auth method code: {code}"
626 ))),
627 }
628 }
629
630 async fn auth_scram<S: AsyncRead + AsyncWrite + Unpin>(
632 &mut self,
633 stream: &mut S,
634 mechanisms_data: &[u8],
635 ) -> Result<()> {
636 let mechanisms = parse_sasl_mechanisms(mechanisms_data);
638
639 if !mechanisms.iter().any(|m| m == "SCRAM-SHA-256") {
640 return Err(PgWireError::Auth(format!(
641 "server doesn't offer SCRAM-SHA-256, available: {mechanisms:?}"
642 )));
643 }
644
645 #[cfg(not(feature = "scram"))]
646 return Err(PgWireError::Auth(
647 "SCRAM authentication required but 'scram' feature not enabled".into(),
648 ));
649
650 #[cfg(feature = "scram")]
651 {
652 use crate::auth::scram::ScramClient;
653
654 let scram = ScramClient::new(&self.cfg.user);
655
656 let mut init = Vec::new();
658 init.extend_from_slice(b"SCRAM-SHA-256\0");
659 init.extend_from_slice(&(scram.client_first.len() as i32).to_be_bytes());
660 init.extend_from_slice(scram.client_first.as_bytes());
661 write_password_message(stream, &init).await?;
662
663 let server_first = read_auth_data(stream, 11).await?;
665 let server_first_str = String::from_utf8_lossy(&server_first);
666
667 let (client_final, auth_message, salted_password) =
669 scram.client_final(&self.cfg.password, &server_first_str)?;
670 write_password_message(stream, client_final.as_bytes()).await?;
671
672 let server_final = read_auth_data(stream, 12).await?;
674 let server_final_str = String::from_utf8_lossy(&server_final);
675 ScramClient::verify_server_final(&server_final_str, &salted_password, &auth_message)?;
676
677 Ok(())
678 }
679 }
680
681 async fn send_feedback<S: AsyncWrite + Unpin>(
683 &self,
684 stream: &mut S,
685 applied: Lsn,
686 reply_requested: bool,
687 ) -> Result<()> {
688 let client_time = current_pg_timestamp();
689 let payload = encode_standby_status_update(applied, client_time, reply_requested);
690 write_copy_data(stream, &payload).await?;
691 self.metrics.record_feedback_sent(applied);
692 Ok(())
693 }
694}
695
696fn parse_sasl_mechanisms(data: &[u8]) -> Vec<String> {
698 let mut mechanisms = Vec::new();
699 let mut remaining = data;
700
701 while !remaining.is_empty() {
702 if let Some(pos) = remaining.iter().position(|&x| x == 0) {
703 if pos == 0 {
704 break; }
706 mechanisms.push(String::from_utf8_lossy(&remaining[..pos]).to_string());
707 remaining = &remaining[pos + 1..];
708 } else {
709 break;
710 }
711 }
712
713 mechanisms
714}
715
716fn parse_pgoutput_boundary(data: &Bytes) -> Result<Option<ReplicationEvent>> {
717 if data.is_empty() {
718 return Ok(None);
719 }
720
721 let tag = data[0];
722 let mut p = &data[1..];
723
724 fn take_i8(p: &mut &[u8]) -> Result<i8> {
725 if p.is_empty() {
726 return Err(PgWireError::Protocol("pgoutput: truncated i8".into()));
727 }
728 let v = p[0] as i8;
729 *p = &p[1..];
730 Ok(v)
731 }
732
733 fn take_i32(p: &mut &[u8]) -> Result<i32> {
734 if p.len() < 4 {
735 return Err(PgWireError::Protocol("pgoutput: truncated i32".into()));
736 }
737 let (head, tail) = p.split_at(4);
738 *p = tail;
739 Ok(i32::from_be_bytes(head.try_into().unwrap()))
740 }
741
742 fn take_i64(p: &mut &[u8]) -> Result<i64> {
743 if p.len() < 8 {
744 return Err(PgWireError::Protocol("pgoutput: truncated i64".into()));
745 }
746 let (head, tail) = p.split_at(8);
747 *p = tail;
748 Ok(i64::from_be_bytes(head.try_into().unwrap()))
749 }
750
751 match tag {
752 b'B' => {
753 let final_lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
754 let commit_time_micros = take_i64(&mut p)?;
755 let xid = take_i32(&mut p)? as u32;
756
757 Ok(Some(ReplicationEvent::Begin {
758 final_lsn,
759 commit_time_micros,
760 xid,
761 }))
762 }
763 b'C' => {
764 let _flags = take_i8(&mut p)?;
765 let lsn = Lsn::from_u64(take_i64(&mut p)? as u64); let end_lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
767 let commit_time_micros = take_i64(&mut p)?;
768
769 Ok(Some(ReplicationEvent::Commit {
770 lsn,
771 end_lsn,
772 commit_time_micros,
773 }))
774 }
775 b'M' => {
776 let flags = take_i8(&mut p)?;
779 let transactional = (flags & 1) != 0;
780 let lsn = Lsn::from_u64(take_i64(&mut p)? as u64);
781
782 let prefix_end = p.iter().position(|&b| b == 0).ok_or_else(|| {
784 PgWireError::Protocol("pgoutput Message: missing null terminator for prefix".into())
785 })?;
786 let prefix = String::from_utf8_lossy(&p[..prefix_end]).into_owned();
787 p = &p[prefix_end + 1..]; let content_len = take_i32(&mut p)? as usize;
790 if p.len() < content_len {
791 return Err(PgWireError::Protocol(format!(
792 "pgoutput Message: expected {} content bytes, got {}",
793 content_len,
794 p.len()
795 )));
796 }
797 let content = Bytes::copy_from_slice(&p[..content_len]);
798
799 Ok(Some(ReplicationEvent::Message {
800 transactional,
801 lsn,
802 prefix,
803 content,
804 }))
805 }
806 _ => Ok(None),
807 }
808}
809
810async fn read_auth_data<S: AsyncRead + AsyncWrite + Unpin>(
812 stream: &mut S,
813 expected_code: i32,
814) -> Result<Vec<u8>> {
815 loop {
816 let msg = read_backend_message(stream).await?;
817 match msg.tag {
818 b'R' => {
819 let (code, data) = parse_auth_request(&msg.payload)?;
820 if code == expected_code {
821 return Ok(data.to_vec());
822 }
823 return Err(PgWireError::Auth(format!(
824 "unexpected auth code {code}, expected {expected_code}"
825 )));
826 }
827 b'E' => return Err(PgWireError::Server(parse_error_response(&msg.payload))),
828 _ => {} }
830 }
831}
832
833fn current_pg_timestamp() -> i64 {
835 use std::time::{SystemTime, UNIX_EPOCH};
836
837 let now = SystemTime::now()
838 .duration_since(UNIX_EPOCH)
839 .unwrap_or_default();
840
841 let unix_micros = (now.as_secs() as i64) * 1_000_000 + (now.subsec_micros() as i64);
842 unix_micros - PG_EPOCH_MICROS
843}
844
845#[cfg(feature = "md5")]
847fn postgres_md5(password: &str, user: &str, salt: &[u8; 4]) -> String {
848 fn md5_hex(data: &[u8]) -> String {
849 format!("{:x}", md5::compute(data))
850 }
851
852 let inner = md5_hex(format!("{password}{user}").as_bytes());
854
855 let mut outer_input = inner.into_bytes();
857 outer_input.extend_from_slice(salt);
858
859 format!("md5{}", md5_hex(&outer_input))
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn parse_sasl_mechanisms_single() {
868 let data = b"SCRAM-SHA-256\0\0";
869 let mechs = parse_sasl_mechanisms(data);
870 assert_eq!(mechs, vec!["SCRAM-SHA-256"]);
871 }
872
873 #[test]
874 fn parse_sasl_mechanisms_multiple() {
875 let data = b"SCRAM-SHA-256\0SCRAM-SHA-256-PLUS\0\0";
876 let mechs = parse_sasl_mechanisms(data);
877 assert_eq!(mechs, vec!["SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"]);
878 }
879
880 #[test]
881 fn parse_sasl_mechanisms_empty() {
882 let mechs = parse_sasl_mechanisms(b"\0");
883 assert!(mechs.is_empty());
884 }
885
886 #[test]
887 #[cfg(feature = "md5")]
888 fn postgres_md5_known_value() {
889 let hash = postgres_md5("md5_pass", "md5_user", &[0x01, 0x02, 0x03, 0x04]);
892 assert!(hash.starts_with("md5"));
893 assert_eq!(hash.len(), 35); }
895
896 #[test]
897 fn current_pg_timestamp_is_positive() {
898 let ts = current_pg_timestamp();
900 assert!(ts > 0);
901 }
902
903 fn xlog_frame(wal_end: u64, data: &[u8]) -> Vec<u8> {
907 let mut payload = Vec::new();
908 payload.push(b'w');
909 payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); payload.extend_from_slice(&(wal_end as i64).to_be_bytes()); payload.extend_from_slice(&0i64.to_be_bytes()); payload.extend_from_slice(data);
913
914 let mut frame = Vec::new();
915 frame.push(b'd');
916 frame.extend_from_slice(&((payload.len() + 4) as i32).to_be_bytes());
917 frame.extend_from_slice(&payload);
918 frame
919 }
920
921 fn test_worker(
925 cfg: ReplicationConfig,
926 ) -> (
927 WorkerState,
928 watch::Sender<bool>,
929 mpsc::Receiver<std::result::Result<ReplicationEvent, PgWireError>>,
930 ) {
931 let progress = Arc::new(SharedProgress::new(cfg.start_lsn));
932 let (stop_tx, stop_rx) = watch::channel(false);
933 let (tx, rx) = mpsc::channel(16);
934 let metrics = Arc::new(ReplicationMetrics::default());
935 let worker = WorkerState::new(cfg, progress, stop_rx, tx, metrics);
936 (worker, stop_tx, rx)
937 }
938
939 #[tokio::test]
945 async fn startup_includes_options_when_set() {
946 use tokio::io::{AsyncReadExt, AsyncWriteExt};
947
948 let cfg = ReplicationConfig::new("h", "u", "p", "db", "slot", "pub")
949 .with_options("-c datestyle=ISO,MDY -c intervalstyle=iso_8601");
950 let (worker, _stop_tx, _rx) = test_worker(cfg);
951 let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
952
953 worker.startup(&mut worker_end).await.unwrap();
954 worker_end.shutdown().await.unwrap();
955
956 let mut sent = Vec::new();
957 server.read_to_end(&mut sent).await.unwrap();
958 let sent = String::from_utf8_lossy(&sent);
959
960 assert!(
961 sent.contains("options"),
962 "startup message missing 'options' key: {sent:?}"
963 );
964 assert!(
965 sent.contains("-c datestyle=ISO,MDY -c intervalstyle=iso_8601"),
966 "startup message missing options value: {sent:?}"
967 );
968 }
969
970 #[tokio::test]
971 async fn startup_omits_options_when_unset() {
972 use tokio::io::{AsyncReadExt, AsyncWriteExt};
973
974 let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
975 let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
976
977 worker.startup(&mut worker_end).await.unwrap();
978 worker_end.shutdown().await.unwrap();
979
980 let mut sent = Vec::new();
981 server.read_to_end(&mut sent).await.unwrap();
982 let sent = String::from_utf8_lossy(&sent);
983
984 assert!(
985 !sent.contains("options"),
986 "startup message should omit 'options' when unset: {sent:?}"
987 );
988 }
989
990 #[tokio::test]
991 async fn start_replication_returns_ok_on_copy_both_response() {
992 use std::time::Duration;
993 use tokio::io::AsyncWriteExt;
994
995 let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
996 let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
997
998 server.write_all(&[b'W', 0, 0, 0, 4]).await.unwrap();
1000
1001 let res = tokio::time::timeout(
1002 Duration::from_secs(5),
1003 worker.start_replication(&mut worker_end),
1004 )
1005 .await;
1006 assert!(matches!(res, Ok(Ok(()))), "expected Ok on 'W', got {res:?}");
1007 }
1008
1009 #[tokio::test]
1010 async fn start_replication_surfaces_server_error_response() {
1011 use std::time::Duration;
1012 use tokio::io::AsyncWriteExt;
1013
1014 let (worker, _stop_tx, _rx) = test_worker(ReplicationConfig::default());
1015 let (mut worker_end, mut server) = tokio::io::duplex(64 * 1024);
1016
1017 server.write_all(&[b'E', 0, 0, 0, 5, 0]).await.unwrap();
1019
1020 let res = tokio::time::timeout(
1021 Duration::from_secs(5),
1022 worker.start_replication(&mut worker_end),
1023 )
1024 .await;
1025 assert!(
1026 matches!(res, Ok(Err(PgWireError::Server(_)))),
1027 "expected Server error on 'E', got {res:?}"
1028 );
1029 }
1030
1031 #[tokio::test]
1036 async fn feedback_continues_under_backpressure() {
1037 use std::time::Duration;
1038 use tokio::io::AsyncWriteExt;
1039
1040 let cfg = ReplicationConfig {
1041 status_interval: Duration::from_millis(40),
1042 idle_wakeup_interval: Duration::from_secs(10),
1043 buffer_events: 2,
1044 ..Default::default()
1045 };
1046
1047 let progress = Arc::new(SharedProgress::new(Lsn(0)));
1048 let (stop_tx, stop_rx) = watch::channel(false);
1049 let (tx, _rx_never_drained) = mpsc::channel(cfg.buffer_events);
1051 let metrics = Arc::new(ReplicationMetrics::default());
1052 let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1053
1054 let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1055
1056 let worker_task = tokio::spawn(async move {
1057 let mut buf = BufReader::with_capacity(1024, worker_end);
1058 let _ = worker.stream_loop(&mut buf).await;
1059 });
1060
1061 for i in 1..=5u64 {
1064 test_end.write_all(&xlog_frame(i, b"Idummy")).await.unwrap();
1065 }
1066 test_end.flush().await.unwrap();
1067
1068 let mut reader = MessageReader::new();
1072 let mut feedback_count = 0;
1073 let deadline = Instant::now() + Duration::from_millis(500);
1074 while feedback_count < 3 {
1075 let remaining = deadline.saturating_duration_since(Instant::now());
1076 if remaining.is_zero() {
1077 break;
1078 }
1079 match tokio::time::timeout(remaining, reader.read(&mut test_end)).await {
1080 Ok(Ok(msg)) if msg.tag == b'd' && msg.payload.first() == Some(&b'r') => {
1081 feedback_count += 1;
1082 }
1083 Ok(Ok(_)) => continue,
1084 _ => break,
1085 }
1086 }
1087
1088 assert!(
1089 feedback_count >= 3,
1090 "expected repeated feedback under backpressure, got {feedback_count}"
1091 );
1092 assert!(
1093 metrics.stall_count() >= 1,
1094 "expected at least one stall to be recorded"
1095 );
1096 assert!(
1097 metrics.feedback_sent() >= 3,
1098 "feedback_sent metric should track sends, got {}",
1099 metrics.feedback_sent()
1100 );
1101
1102 let _ = stop_tx.send(true);
1103 let _ = worker_task.await;
1104 }
1105
1106 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1110 async fn feedback_under_backpressure_is_rate_limited() {
1111 use std::time::Duration;
1112 use tokio::io::AsyncWriteExt;
1113
1114 let interval = Duration::from_millis(50);
1115 let cfg = ReplicationConfig {
1116 status_interval: interval,
1117 idle_wakeup_interval: Duration::from_secs(10),
1118 buffer_events: 2,
1119 ..Default::default()
1120 };
1121
1122 let progress = Arc::new(SharedProgress::new(Lsn(0)));
1123 let (stop_tx, stop_rx) = watch::channel(false);
1124 let (tx, _rx_never_drained) = mpsc::channel(cfg.buffer_events);
1125 let metrics = Arc::new(ReplicationMetrics::default());
1126 let mut worker = WorkerState::new(cfg, progress, stop_rx, tx, Arc::clone(&metrics));
1127
1128 let (worker_end, mut test_end) = tokio::io::duplex(64 * 1024);
1129 let worker_task = tokio::spawn(async move {
1130 let mut buf = BufReader::with_capacity(1024, worker_end);
1131 let _ = worker.stream_loop(&mut buf).await;
1132 });
1133
1134 for i in 1..=5u64 {
1135 test_end.write_all(&xlog_frame(i, b"Idummy")).await.unwrap();
1136 }
1137 test_end.flush().await.unwrap();
1138
1139 let window = Duration::from_millis(350);
1144 let end = Instant::now() + window;
1145 let mut reader = MessageReader::new();
1146 let mut feedback = 0usize;
1147 loop {
1148 let remaining = end.saturating_duration_since(Instant::now());
1149 if remaining.is_zero() {
1150 break;
1151 }
1152 match tokio::time::timeout(remaining, reader.read(&mut test_end)).await {
1153 Ok(Ok(msg)) if msg.tag == b'd' && msg.payload.first() == Some(&b'r') => {
1154 feedback += 1;
1155 }
1156 Ok(Ok(_)) => {}
1157 _ => break,
1158 }
1159 }
1160
1161 assert!(
1162 feedback >= 2,
1163 "feedback should keep flowing under backpressure, got {feedback}"
1164 );
1165 assert!(
1166 feedback <= 25,
1167 "feedback must be rate-limited to ~status_interval, got {feedback}"
1168 );
1169
1170 let _ = stop_tx.send(true);
1171 let _ = worker_task.await;
1172 }
1173}