use std::{fmt::Debug, future::Future, time::Duration};
use futures::StreamExt;
pub(crate) async fn staggered_first_ok<T, E: Debug>(
attempts: impl IntoIterator<Item = impl Future<Output = Result<T, E>>>,
delay: Duration,
) -> Result<T, Vec<E>> {
let mut pending = attempts.into_iter();
let mut in_flight = futures::stream::FuturesUnordered::new();
let mut errors = Vec::new();
match pending.next() {
Some(first) => in_flight.push(first),
None => return Err(errors),
}
let mut pending = pending.peekable();
loop {
let has_more = pending.peek().is_some();
tokio::select! {
biased;
completed = in_flight.next() => {
match completed {
Some(Ok(winner)) => return Ok(winner),
Some(Err(e)) => {
tracing::trace!(attempt = errors.len(), error = ?e, "Connection attempt failed");
errors.push(e);
if in_flight.is_empty() {
match pending.next() {
Some(next) => in_flight.push(next),
None => return Err(errors),
}
}
}
None => unreachable!("in_flight is never polled while empty"),
}
}
() = tokio::time::sleep(delay), if has_more => {
if let Some(next) = pending.next() {
in_flight.push(next);
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use super::*;
const DELAY: Duration = Duration::from_millis(250);
fn attempt(
started: &Arc<AtomicUsize>,
wait: Duration,
result: Result<u32, u32>,
) -> impl Future<Output = Result<u32, u32>> + use<> {
let started = started.clone();
async move {
started.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(wait).await;
result
}
}
#[tokio::test(start_paused = true)]
async fn fast_success_starts_only_one_attempt() {
let started = Arc::new(AtomicUsize::new(0));
let attempts = vec![
attempt(&started, Duration::from_millis(10), Ok(1)),
attempt(&started, Duration::ZERO, Ok(2)),
];
let winner = staggered_first_ok(attempts, DELAY).await.unwrap();
assert_eq!(winner, 1);
assert_eq!(started.load(Ordering::SeqCst), 1);
}
#[tokio::test(start_paused = true)]
async fn slow_attempt_triggers_staggered_second() {
let started = Arc::new(AtomicUsize::new(0));
let attempts = vec![
attempt(&started, Duration::from_secs(60), Ok(1)),
attempt(&started, Duration::from_millis(10), Ok(2)),
];
let start = tokio::time::Instant::now();
let winner = staggered_first_ok(attempts, DELAY).await.unwrap();
assert_eq!(winner, 2);
assert_eq!(started.load(Ordering::SeqCst), 2);
assert!(start.elapsed() >= DELAY + Duration::from_millis(10));
}
#[tokio::test(start_paused = true)]
async fn fast_failure_starts_next_attempt_immediately() {
let started = Arc::new(AtomicUsize::new(0));
let attempts = vec![
attempt(&started, Duration::from_millis(1), Err(1)),
attempt(&started, Duration::from_millis(1), Ok(2)),
];
let start = tokio::time::Instant::now();
let winner = staggered_first_ok(attempts, DELAY).await.unwrap();
assert_eq!(winner, 2);
assert!(start.elapsed() < DELAY);
}
#[tokio::test(start_paused = true)]
async fn all_failures_are_aggregated_in_completion_order() {
let started = Arc::new(AtomicUsize::new(0));
let attempts = vec![
attempt(&started, Duration::from_millis(1), Err(1)),
attempt(&started, Duration::from_millis(1), Err(2)),
attempt(&started, Duration::from_millis(1), Err(3)),
];
let errors = staggered_first_ok(attempts, DELAY).await.unwrap_err();
assert_eq!(errors, vec![1, 2, 3]);
assert_eq!(started.load(Ordering::SeqCst), 3);
}
#[tokio::test(start_paused = true)]
async fn success_cancels_the_losing_attempt() {
type BoxedAttempt = std::pin::Pin<Box<dyn Future<Output = Result<u32, u32>>>>;
struct DropFlag(Arc<AtomicUsize>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let dropped = Arc::new(AtomicUsize::new(0));
let flag = DropFlag(dropped.clone());
let loser = async move {
let _flag = flag;
tokio::time::sleep(Duration::from_secs(3600)).await;
Ok(1)
};
let winner_started = Arc::new(AtomicUsize::new(0));
let attempts: Vec<BoxedAttempt> = vec![
Box::pin(loser),
Box::pin(attempt(&winner_started, Duration::from_millis(1), Ok(2))),
];
let winner = staggered_first_ok(attempts, DELAY).await.unwrap();
assert_eq!(winner, 2);
assert_eq!(dropped.load(Ordering::SeqCst), 1);
}
#[tokio::test(start_paused = true)]
async fn no_attempts_fails_with_no_errors() {
let attempts: Vec<std::future::Ready<Result<u32, u32>>> = vec![];
let errors = staggered_first_ok(attempts, DELAY).await.unwrap_err();
assert!(errors.is_empty());
}
}