use std::time::{Duration, Instant};
pub(crate) const FIRST_BACKOFF: Duration = Duration::from_secs(5);
pub(crate) const MAX_BACKOFF: Duration = Duration::from_secs(120);
pub(crate) fn backoff(failures: u32) -> Duration {
if failures == 0 {
return Duration::ZERO;
}
let factor = 1u32.checked_shl(failures.saturating_sub(1).min(16)).unwrap_or(u32::MAX);
FIRST_BACKOFF.checked_mul(factor).map_or(MAX_BACKOFF, |d| d.min(MAX_BACKOFF))
}
#[derive(Debug)]
pub(crate) struct PushHeal {
failures: u32,
next_try: Instant,
down_since: Option<Instant>,
}
impl PushHeal {
pub(crate) fn new() -> Self {
Self { failures: 0, next_try: Instant::now(), down_since: None }
}
pub(crate) fn due(&self) -> bool {
Instant::now() >= self.next_try
}
#[cfg(test)]
pub(crate) fn failures(&self) -> u32 {
self.failures
}
pub(crate) fn down(&mut self, label: &str, reason: Option<&str>) {
if self.down_since.is_none() {
self.down_since = Some(Instant::now());
eprintln!("mail4agent: WARN push socket for {label} is down ({}); reconnecting", reason.unwrap_or("no reason given"));
}
}
pub(crate) fn failed(&mut self, label: &str, err: &str) {
self.failures = self.failures.saturating_add(1);
if self.down_since.is_none() {
self.down_since = Some(Instant::now());
}
let wait = backoff(self.failures);
self.next_try = Instant::now() + wait;
eprintln!("mail4agent: WARN push socket for {label} not reopened (attempt {}, next in {}s): {err}", self.failures, wait.as_secs());
}
pub(crate) fn succeeded(&mut self, label: &str) {
if let Some(since) = self.down_since.take() {
eprintln!("mail4agent: push socket for {label} is open again after {}s ({} failed attempts)", since.elapsed().as_secs(), self.failures);
}
self.failures = 0;
self.next_try = Instant::now();
}
}
pub(crate) fn warn_streak(label: &str, streak: u32, error: Option<&str>) {
if streak == 3 || (streak > 3 && streak % 40 == 3) {
eprintln!("mail4agent: WARN {label}: {streak} ticks in a row failed: {}", error.unwrap_or("-"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_doubles_and_is_capped() {
assert_eq!(backoff(0), Duration::ZERO);
assert_eq!(backoff(1), Duration::from_secs(5));
assert_eq!(backoff(2), Duration::from_secs(10));
assert_eq!(backoff(3), Duration::from_secs(20));
assert_eq!(backoff(5), Duration::from_secs(80));
assert_eq!(backoff(6), MAX_BACKOFF);
assert_eq!(backoff(u32::MAX), MAX_BACKOFF);
}
#[test]
fn a_failure_defers_the_next_attempt_and_success_resets() {
let mut heal = PushHeal::new();
assert!(heal.due());
heal.down("t", Some("silent"));
heal.failed("t", "timed out");
assert_eq!(heal.failures(), 1);
assert!(!heal.due(), "five seconds of back-off");
heal.succeeded("t");
assert_eq!(heal.failures(), 0);
assert!(heal.due());
}
}