1use crate::connection::PgReplicationConnection;
7use crate::error::{ReplicationError, Result};
8use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9use tracing::{debug, error, info};
10
11#[derive(Debug, Copy, Clone)]
36pub struct RetryConfig {
37 pub max_attempts: u32,
39 pub initial_delay: Duration,
41 pub max_delay: Duration,
43 pub multiplier: f64,
45 pub max_duration: Duration,
47 pub jitter: bool,
49}
50
51impl Default for RetryConfig {
52 fn default() -> Self {
53 Self {
54 max_attempts: 5,
55 initial_delay: Duration::from_secs(1),
56 max_delay: Duration::from_secs(60),
57 multiplier: 2.0,
58 max_duration: Duration::from_secs(300),
59 jitter: true,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct ExponentialBackoff {
67 initial_delay: Duration,
68 max_delay: Duration,
69 multiplier: f64,
70 jitter: bool,
71 current_delay: Duration,
72 attempt: u32,
73}
74
75impl ExponentialBackoff {
76 pub fn new(config: &RetryConfig) -> Self {
99 Self {
100 initial_delay: config.initial_delay,
101 max_delay: config.max_delay,
102 multiplier: config.multiplier,
103 jitter: config.jitter,
104 current_delay: config.initial_delay,
105 attempt: 0,
106 }
107 }
108
109 pub fn next_delay(&mut self) -> Duration {
140 let delay = self.current_delay;
141
142 let next_delay_ms = (self.current_delay.as_millis() as f64 * self.multiplier) as u64;
144 self.current_delay = Duration::from_millis(next_delay_ms).min(self.max_delay);
145 self.attempt += 1;
146
147 if self.jitter {
149 self.add_jitter(delay)
150 } else {
151 delay
152 }
153 }
154
155 fn add_jitter(&self, delay: Duration) -> Duration {
157 let nanos = SystemTime::now()
160 .duration_since(UNIX_EPOCH)
161 .unwrap_or_default()
162 .subsec_nanos();
163 let jitter_factor = 0.3;
164
165 let base_millis = delay.as_millis() as f64;
167 let jitter_range = base_millis * jitter_factor;
168 let jitter = (nanos % 1000) as f64 / 1000.0; let jitter_adjustment = (jitter - 0.5) * 2.0 * jitter_range; let final_millis = (base_millis + jitter_adjustment).max(0.0) as u64;
172 Duration::from_millis(final_millis)
173 }
174
175 pub fn reset(&mut self) {
177 self.current_delay = self.initial_delay;
178 self.attempt = 0;
179 }
180
181 pub fn attempt(&self) -> u32 {
183 self.attempt
184 }
185}
186
187impl RetryConfig {
188 pub fn to_backoff(&self) -> ExponentialBackoff {
190 ExponentialBackoff::new(self)
191 }
192}
193
194pub struct ReplicationConnectionRetry {
196 config: RetryConfig,
197 connection_string: String,
198}
199
200impl ReplicationConnectionRetry {
201 pub fn new(config: RetryConfig, connection_string: String) -> Self {
203 Self {
204 config,
205 connection_string,
206 }
207 }
208
209 #[inline]
212 pub(crate) fn connection_string(&self) -> &str {
213 &self.connection_string
214 }
215
216 pub async fn connect_with_retry(&self) -> Result<PgReplicationConnection> {
254 let start_time = Instant::now();
255 let mut backoff = self.config.to_backoff();
256
257 info!("Attempting to connect to PostgreSQL with retry logic");
258
259 for attempt in 1..=self.config.max_attempts {
260 debug!(
261 "Attempting PostgreSQL connection (attempt {}/{})",
262 attempt, self.config.max_attempts
263 );
264
265 if start_time.elapsed() >= self.config.max_duration {
267 error!(
268 "Connection attempts exceeded maximum duration ({:?})",
269 self.config.max_duration
270 );
271 return Err(ReplicationError::connection(format!(
272 "Connection attempts exceeded maximum duration of {:?}",
273 self.config.max_duration
274 )));
275 }
276
277 match PgReplicationConnection::connect(&self.connection_string) {
278 Ok(conn) => {
279 let elapsed = start_time.elapsed();
280 info!(
281 "Successfully connected to PostgreSQL on attempt {} after {:?}",
282 attempt, elapsed
283 );
284 return Ok(conn);
285 }
286 Err(e) => {
287 let error_msg = e.to_string();
288 error!("Connection attempt {} failed: {}", attempt, error_msg);
289
290 if matches!(e, ReplicationError::Config(_)) {
291 error!("Connection string is invalid, not retrying: {error_msg}");
292 return Err(e);
293 }
294
295 if attempt >= self.config.max_attempts {
297 let elapsed = start_time.elapsed();
298 error!("All connection attempts failed after {:?}", elapsed);
299 return Err(e);
300 }
301
302 let delay = backoff.next_delay();
304 debug!("Waiting {:?} before next attempt", delay);
305 tokio::time::sleep(delay).await;
306 }
307 }
308 }
309
310 let elapsed = start_time.elapsed();
312 error!("Connection failed after all attempts and {:?}", elapsed);
313 Err(ReplicationError::connection(
314 "Connection failed after all retry attempts".to_string(),
315 ))
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[cfg(feature = "rustls-tls")]
328 #[tokio::test]
329 async fn invalid_connection_string_returns_on_first_attempt() {
330 let retry = ReplicationConnectionRetry::new(
331 RetryConfig {
332 max_attempts: 5,
333 initial_delay: Duration::from_secs(5),
334 max_delay: Duration::from_secs(60),
335 multiplier: 2.0,
336 max_duration: Duration::from_secs(300),
337 jitter: false,
338 },
339 "postgresql://u@127.0.0.1/db?replication=database&sslmode=bogus".to_string(),
340 );
341
342 let start = Instant::now();
343 let err = match retry.connect_with_retry().await {
345 Ok(_) => panic!("an unparsable conninfo must not connect"),
346 Err(e) => e,
347 };
348 let elapsed = start.elapsed();
349
350 assert!(matches!(err, ReplicationError::Config(_)), "got {err:?}");
353 assert!(err.to_string().contains("sslmode"), "{err}");
354 assert!(
355 elapsed < Duration::from_secs(1),
356 "retried an unparsable conninfo for {elapsed:?}"
357 );
358 }
359
360 #[cfg(feature = "rustls-tls")]
365 #[tokio::test]
366 async fn transient_error_still_consumes_the_backoff_ladder() {
367 let port = {
369 let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
370 l.local_addr().unwrap().port()
371 };
372 let retry = ReplicationConnectionRetry::new(
373 RetryConfig {
374 max_attempts: 3,
375 initial_delay: Duration::from_millis(50),
376 max_delay: Duration::from_secs(1),
377 multiplier: 1.0,
378 max_duration: Duration::from_secs(300),
379 jitter: false,
380 },
381 format!(
382 "postgresql://u@127.0.0.1:{port}/db\
383 ?replication=database&sslmode=disable&connect_timeout=1"
384 ),
385 );
386
387 let start = Instant::now();
388 let err = match retry.connect_with_retry().await {
389 Ok(_) => panic!("nothing should be listening on a just-released port"),
390 Err(e) => e,
391 };
392 assert!(!err.is_permanent(), "got {err:?}");
393 assert!(
394 start.elapsed() >= Duration::from_millis(100),
395 "did not back off between attempts"
396 );
397 }
398
399 #[test]
400 fn test_exponential_backoff_basic() {
401 let config = RetryConfig {
402 max_attempts: 5,
403 initial_delay: Duration::from_millis(100),
404 max_delay: Duration::from_secs(60),
405 multiplier: 2.0,
406 max_duration: Duration::from_secs(300),
407 jitter: false,
408 };
409
410 let mut backoff = ExponentialBackoff::new(&config);
411
412 let first_delay = backoff.next_delay();
414 assert_eq!(first_delay, Duration::from_millis(100));
415 assert_eq!(backoff.attempt(), 1);
416
417 let second_delay = backoff.next_delay();
419 assert_eq!(second_delay, Duration::from_millis(200));
420 assert_eq!(backoff.attempt(), 2);
421
422 let third_delay = backoff.next_delay();
424 assert_eq!(third_delay, Duration::from_millis(400));
425 assert_eq!(backoff.attempt(), 3);
426 }
427
428 #[test]
429 fn test_exponential_backoff_max_delay() {
430 let config = RetryConfig {
431 max_attempts: 10,
432 initial_delay: Duration::from_millis(100),
433 max_delay: Duration::from_millis(500), multiplier: 2.0,
435 max_duration: Duration::from_secs(300),
436 jitter: false,
437 };
438
439 let mut backoff = ExponentialBackoff::new(&config);
440
441 backoff.next_delay(); backoff.next_delay(); backoff.next_delay(); let delay = backoff.next_delay(); assert!(delay <= Duration::from_millis(500));
449 }
450
451 #[test]
452 fn test_exponential_backoff_reset() {
453 let config = RetryConfig {
454 max_attempts: 5,
455 initial_delay: Duration::from_millis(100),
456 max_delay: Duration::from_secs(60),
457 multiplier: 2.0,
458 max_duration: Duration::from_secs(300),
459 jitter: false,
460 };
461
462 let mut backoff = ExponentialBackoff::new(&config);
463
464 backoff.next_delay();
466 backoff.next_delay();
467 assert_eq!(backoff.attempt(), 2);
468
469 backoff.reset();
471 assert_eq!(backoff.attempt(), 0);
472
473 let delay = backoff.next_delay();
474 assert_eq!(delay, Duration::from_millis(100));
475 assert_eq!(backoff.attempt(), 1);
476 }
477
478 #[test]
479 fn test_exponential_backoff_jitter() {
480 let config = RetryConfig {
481 max_attempts: 5,
482 initial_delay: Duration::from_millis(100),
483 max_delay: Duration::from_secs(60),
484 multiplier: 2.0,
485 max_duration: Duration::from_secs(300),
486 jitter: true,
487 };
488
489 let mut backoff = ExponentialBackoff::new(&config);
490
491 let mut saw_variation = false;
492 let mut prev = backoff.next_delay();
493 for _ in 0..5 {
494 let delay = backoff.next_delay();
495 if delay != prev {
496 saw_variation = true;
497 break;
498 }
499 prev = delay;
500 }
501 assert!(saw_variation, "Expected jitter to vary delays");
502 }
503
504 #[test]
505 fn test_max_attempts() {
506 let config = RetryConfig {
507 max_attempts: 3,
508 initial_delay: Duration::from_millis(100),
509 max_delay: Duration::from_secs(60),
510 multiplier: 2.0,
511 max_duration: Duration::from_secs(300),
512 jitter: false,
513 };
514
515 let mut backoff = ExponentialBackoff::new(&config);
516
517 backoff.next_delay();
518 assert_eq!(backoff.attempt(), 1);
519 backoff.next_delay();
520 assert_eq!(backoff.attempt(), 2);
521 backoff.next_delay();
522 assert_eq!(backoff.attempt(), 3);
523
524 assert_eq!(backoff.attempt(), config.max_attempts);
526 }
527
528 #[test]
529 fn test_multiplier_effect() {
530 let config = RetryConfig {
531 max_attempts: 5,
532 initial_delay: Duration::from_millis(10),
533 max_delay: Duration::from_secs(60),
534 multiplier: 3.0, max_duration: Duration::from_secs(300),
536 jitter: false,
537 };
538
539 let mut backoff = ExponentialBackoff::new(&config);
540
541 assert_eq!(backoff.next_delay(), Duration::from_millis(10));
542 assert_eq!(backoff.next_delay(), Duration::from_millis(30));
543 assert_eq!(backoff.next_delay(), Duration::from_millis(90));
544 }
545
546 #[test]
547 fn test_retry_config_default() {
548 let config = RetryConfig::default();
549 assert_eq!(config.max_attempts, 5);
550 assert_eq!(config.initial_delay, Duration::from_secs(1));
551 assert_eq!(config.max_delay, Duration::from_secs(60));
552 assert_eq!(config.multiplier, 2.0);
553 }
554
555 #[test]
556 fn test_backoff_current_delay_field() {
557 let config = RetryConfig {
558 max_attempts: 5,
559 initial_delay: Duration::from_millis(100),
560 max_delay: Duration::from_secs(60),
561 multiplier: 2.0,
562 max_duration: Duration::from_secs(300),
563 jitter: false,
564 };
565
566 let mut backoff = ExponentialBackoff::new(&config);
567
568 assert_eq!(backoff.current_delay, Duration::from_millis(100));
569 backoff.next_delay();
570 assert_eq!(backoff.current_delay, Duration::from_millis(200));
571 }
572
573 #[test]
574 fn test_reset_clears_state() {
575 let config = RetryConfig {
576 max_attempts: 5,
577 initial_delay: Duration::from_millis(100),
578 max_delay: Duration::from_secs(60),
579 multiplier: 2.0,
580 max_duration: Duration::from_secs(300),
581 jitter: false,
582 };
583
584 let mut backoff = ExponentialBackoff::new(&config);
585
586 backoff.next_delay();
587 backoff.next_delay();
588
589 backoff.reset();
590
591 assert_eq!(backoff.attempt(), 0);
592 assert_eq!(backoff.current_delay, Duration::from_millis(100));
593 }
594
595 #[test]
596 fn test_to_backoff() {
597 let config = RetryConfig::default();
598 let backoff = config.to_backoff();
599 assert_eq!(backoff.attempt(), 0);
600 assert_eq!(backoff.current_delay, config.initial_delay);
601 }
602
603 #[test]
604 fn test_replication_connection_retry_new() {
605 let config = RetryConfig::default();
606 let retry = ReplicationConnectionRetry::new(
607 config,
608 "postgresql://localhost/test?replication=database".to_string(),
609 );
610 assert_eq!(
611 retry.connection_string,
612 "postgresql://localhost/test?replication=database"
613 );
614 }
615
616 #[test]
617 fn test_jitter_stays_within_bounds() {
618 let config = RetryConfig {
619 max_attempts: 50,
620 initial_delay: Duration::from_millis(1000),
621 max_delay: Duration::from_secs(60),
622 multiplier: 1.0, max_duration: Duration::from_secs(300),
624 jitter: true,
625 };
626
627 let mut backoff = ExponentialBackoff::new(&config);
628
629 for _ in 0..20 {
631 let delay = backoff.next_delay();
632 let millis = delay.as_millis() as f64;
633 assert!(
634 (700.0..=1300.0).contains(&millis),
635 "Jitter delay {millis}ms is outside ±30% of 1000ms"
636 );
637 backoff.current_delay = Duration::from_millis(1000);
639 }
640 }
641
642 #[test]
643 fn test_retry_config_clone_and_debug() {
644 let config = RetryConfig::default();
645 let cloned = config;
646 assert_eq!(cloned.max_attempts, config.max_attempts);
647 assert_eq!(cloned.initial_delay, config.initial_delay);
648 assert_eq!(cloned.max_delay, config.max_delay);
649
650 let debug_str = format!("{:?}", config);
652 assert!(debug_str.contains("RetryConfig"));
653 }
654
655 #[test]
656 fn test_exponential_backoff_clone_debug() {
657 let config = RetryConfig::default();
658 let backoff = ExponentialBackoff::new(&config);
659 let cloned = backoff.clone();
660 assert_eq!(cloned.attempt(), backoff.attempt());
661
662 let debug_str = format!("{:?}", backoff);
663 assert!(debug_str.contains("ExponentialBackoff"));
664 }
665
666 #[test]
667 fn test_max_delay_cap_with_jitter() {
668 let config = RetryConfig {
669 max_attempts: 20,
670 initial_delay: Duration::from_secs(30),
671 max_delay: Duration::from_secs(60),
672 multiplier: 2.0,
673 max_duration: Duration::from_secs(300),
674 jitter: true,
675 };
676
677 let mut backoff = ExponentialBackoff::new(&config);
678
679 for _ in 0..10 {
681 let delay = backoff.next_delay();
682 let max_allowed = Duration::from_secs(60).as_millis() as f64 * 1.3;
684 assert!(
685 (delay.as_millis() as f64) <= max_allowed + 1.0,
686 "Delay {:?} exceeds max allowed with jitter",
687 delay
688 );
689 }
690 }
691
692 #[test]
693 fn test_add_jitter_small_delay() {
694 let config = RetryConfig {
695 max_attempts: 5,
696 initial_delay: Duration::from_millis(1),
697 max_delay: Duration::from_secs(60),
698 multiplier: 1.0,
699 max_duration: Duration::from_secs(300),
700 jitter: true,
701 };
702
703 let mut backoff = ExponentialBackoff::new(&config);
704 let delay = backoff.next_delay();
706 assert!(delay.as_millis() <= 2); }
708
709 #[test]
710 fn test_backoff_max_delay_capping() {
711 let config = RetryConfig {
712 max_attempts: 10,
713 initial_delay: Duration::from_secs(50),
714 max_delay: Duration::from_secs(60),
715 multiplier: 2.0,
716 max_duration: Duration::from_secs(300),
717 jitter: false,
718 };
719
720 let mut backoff = ExponentialBackoff::new(&config);
721
722 let d1 = backoff.next_delay(); assert_eq!(d1, Duration::from_secs(50));
724 let d2 = backoff.next_delay(); assert_eq!(d2, Duration::from_secs(60));
726 let d3 = backoff.next_delay(); assert_eq!(d3, Duration::from_secs(60));
728 }
729
730 #[test]
734 fn connection_string_is_returned_verbatim() {
735 let conninfo = "postgresql://u:p@h:5432/db?replication=database&sslmode=verify-full";
736 let retry = ReplicationConnectionRetry::new(RetryConfig::default(), conninfo.to_string());
737 assert_eq!(retry.connection_string(), conninfo);
738 }
739}