use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::core::Broker;
use crate::runtime::backoff::Backoff;
pub(crate) struct AckRetrier<B: Broker> {
broker: B,
input_rx: mpsc::Receiver<B::AckToken>,
backoff: Backoff,
max_attempts: u32,
cancellation: CancellationToken,
}
impl<B: Broker> AckRetrier<B> {
pub(crate) fn new(
broker: B, input_rx: mpsc::Receiver<B::AckToken>, backoff: Backoff, max_attempts: u32,
cancellation: CancellationToken,
) -> Self {
Self { broker, input_rx, backoff, max_attempts: max_attempts.max(1), cancellation }
}
pub(crate) async fn run(mut self) {
while let Some(token) = self.input_rx.recv().await {
self.retry_one(token).await;
}
}
async fn retry_one(&self, mut token: B::AckToken) {
let mut attempt: u32 = 1;
loop {
let retry_token = token.clone();
match self.broker.ack(token).await {
Ok(()) => return,
Err(error) => {
if attempt >= self.max_attempts {
tracing::error!(attempt, %error, "ack retry exhausted; giving up (message may be redelivered)");
return;
}
if self.cancellation.is_cancelled() {
tracing::warn!("shutting down; abandoning further ack retries");
return;
}
}
}
let delay = self.backoff.delay(attempt);
attempt = attempt.saturating_add(1);
token = retry_token;
tokio::select! {
_ = self.cancellation.cancelled() => return,
_ = tokio::time::sleep(delay) => {}
}
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use super::AckRetrier;
use crate::config::BackoffConfig;
use crate::runtime::backoff::Backoff;
use crate::test_support::MockBroker;
use crate::test_support::token;
fn fast_backoff() -> Backoff {
Backoff::new(BackoffConfig {
initial: Duration::from_millis(1),
max: Duration::from_millis(2),
factor: 2.0,
jitter: 0.0,
})
}
#[tokio::test]
async fn retries_until_success() {
let broker = MockBroker::default();
broker.fail_next_acks(2);
let (tx, rx) = mpsc::channel(8);
let cancellation = CancellationToken::new();
let retrier = AckRetrier::new(broker.clone(), rx, fast_backoff(), 8, cancellation.clone());
let handle = tokio::spawn(retrier.run());
tx.send(token("1-1")).await.unwrap();
timeout(Duration::from_secs(1), async {
while broker.ack_count() < 1 {
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("ack should eventually succeed");
assert_eq!(broker.ack_attempts(), 3);
cancellation.cancel();
drop(tx);
timeout(Duration::from_secs(1), handle).await.unwrap().unwrap();
}
#[tokio::test]
async fn gives_up_after_max_attempts() {
let broker = MockBroker::default();
broker.fail_next_acks(100);
let (tx, rx) = mpsc::channel(8);
let cancellation = CancellationToken::new();
let retrier = AckRetrier::new(broker.clone(), rx, fast_backoff(), 3, cancellation.clone());
let handle = tokio::spawn(retrier.run());
tx.send(token("1-1")).await.unwrap();
timeout(Duration::from_secs(1), async {
while broker.ack_attempts() < 3 {
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("retrier should attempt up to the cap");
tokio::time::sleep(Duration::from_millis(20)).await;
assert_eq!(broker.ack_attempts(), 3);
cancellation.cancel();
drop(tx);
timeout(Duration::from_secs(1), handle).await.unwrap().unwrap();
}
}