217 lines
7.2 KiB
Rust
217 lines
7.2 KiB
Rust
// Credits go to https://github.com/ihrwein/backoff/pull/50
|
|
use std::{pin::Pin, task::Poll, time::Duration};
|
|
|
|
use backoff::{backoff::Backoff, future::Sleeper};
|
|
use futures_util::{Stream, TryStream};
|
|
use pin_project::pin_project;
|
|
|
|
// /// Applies a [`Backoff`] policy to a [`Stream`]
|
|
// ///
|
|
// /// After any [`Err`] is emitted, the stream is paused for [`Backoff::next_backoff`]. The
|
|
// /// [`Backoff`] is [`reset`](`Backoff::reset`) on any [`Ok`] value.
|
|
// ///
|
|
// /// If [`Backoff::next_backoff`] returns [`None`] then the backing stream is given up on, and
|
|
// closed. pub fn backoff<S: TryStream, B: Backoff>(
|
|
// stream: S,
|
|
// backoff: B,
|
|
// ) -> StreamBackoff<S, B, impl Sleeper> {
|
|
// StreamBackoff::new(stream, backoff, TokioSleeper)
|
|
// }
|
|
|
|
pub(crate) struct TokioSleeper;
|
|
impl Sleeper for TokioSleeper {
|
|
type Sleep = ::tokio::time::Sleep;
|
|
fn sleep(&self, dur: Duration) -> Self::Sleep {
|
|
::tokio::time::sleep(dur)
|
|
}
|
|
}
|
|
|
|
/// See [`backoff`]
|
|
#[pin_project]
|
|
pub struct StreamBackoff<S, B, Sl: Sleeper> {
|
|
#[pin]
|
|
stream: S,
|
|
backoff: B,
|
|
sleeper: Sl,
|
|
#[pin]
|
|
state: State<Sl>,
|
|
}
|
|
|
|
#[pin_project(project = StateProj)]
|
|
enum State<Sl: Sleeper> {
|
|
BackingOff {
|
|
#[pin]
|
|
backoff_sleep: Sl::Sleep,
|
|
},
|
|
GivenUp,
|
|
Awake,
|
|
}
|
|
|
|
impl<S: TryStream, B: Backoff, Sl: Sleeper> StreamBackoff<S, B, Sl> {
|
|
pub fn new(stream: S, backoff: B, sleeper: Sl) -> Self {
|
|
Self {
|
|
stream,
|
|
backoff,
|
|
sleeper,
|
|
state: State::Awake,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S: TryStream, B: Backoff, Sl: Sleeper> Stream for StreamBackoff<S, B, Sl>
|
|
where
|
|
Sl::Sleep: Future,
|
|
{
|
|
type Item = Result<S::Ok, S::Error>;
|
|
|
|
fn poll_next(
|
|
self: Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
) -> Poll<Option<Self::Item>> {
|
|
let mut this = self.project();
|
|
match this.state.as_mut().project() {
|
|
StateProj::BackingOff { mut backoff_sleep } => match backoff_sleep.as_mut().poll(cx) {
|
|
Poll::Ready(()) => {
|
|
// tracing::debug!(deadline = ?backoff_sleep.deadline(), "Backoff complete,
|
|
// waking up");
|
|
this.state.set(State::Awake);
|
|
}
|
|
Poll::Pending => {
|
|
// let deadline = backoff_sleep.deadline();
|
|
// tracing::trace!(
|
|
// ?deadline,
|
|
// remaining_duration = ?deadline.saturating_duration_since(Instant::now()),
|
|
// "Still waiting for backoff sleep to complete"
|
|
// );
|
|
return Poll::Pending;
|
|
}
|
|
},
|
|
StateProj::GivenUp => {
|
|
// tracing::debug!("Backoff has given up, stream is closed");
|
|
return Poll::Ready(None);
|
|
}
|
|
StateProj::Awake => {}
|
|
}
|
|
|
|
let next_item = this.stream.try_poll_next(cx);
|
|
match &next_item {
|
|
Poll::Ready(Some(Err(_))) => {
|
|
if let Some(backoff_duration) = this.backoff.next_backoff() {
|
|
let backoff_sleep = this.sleeper.sleep(backoff_duration);
|
|
// tracing::debug!(
|
|
// deadline = ?backoff_sleep.deadline(),
|
|
// duration = ?backoff_duration,
|
|
// "Error received, backing off"
|
|
// );
|
|
this.state.set(State::BackingOff { backoff_sleep });
|
|
} else {
|
|
// tracing::debug!("Error received, giving up");
|
|
this.state.set(State::GivenUp);
|
|
}
|
|
}
|
|
Poll::Ready(_) => {
|
|
// tracing::trace!("Non-error received, resetting backoff");
|
|
this.backoff.reset();
|
|
}
|
|
Poll::Pending => {}
|
|
}
|
|
next_item
|
|
}
|
|
}
|
|
|
|
// Tokio clock is required to be able to freeze time during marble tests
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use futures_util::{StreamExt, pin_mut, poll, stream};
|
|
use std::{task::Poll, time::Duration};
|
|
use tokio::{self, sync::mpsc};
|
|
|
|
#[tokio::test]
|
|
async fn stream_should_back_off() {
|
|
tokio::time::pause();
|
|
let tick = Duration::from_secs(1);
|
|
let rx = stream::iter([Ok(0), Ok(1), Err(2), Ok(3), Ok(4)]);
|
|
let rx = StreamBackoff::new(rx, backoff::backoff::Constant::new(tick), TokioSleeper);
|
|
pin_mut!(rx);
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(0))));
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(1))));
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Err(2))));
|
|
assert_eq!(poll!(rx.next()), Poll::Pending);
|
|
tokio::time::advance(tick * 2).await;
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(3))));
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(4))));
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(None));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backoff_time_should_update() {
|
|
tokio::time::pause();
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
let rx = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
|
|
let rx = StreamBackoff::new(rx, LinearBackoff::new(Duration::from_secs(2)), TokioSleeper);
|
|
pin_mut!(rx);
|
|
tx.send(Ok(0)).unwrap();
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(0))));
|
|
tx.send(Ok(1)).unwrap();
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(1))));
|
|
tx.send(Err(2)).unwrap();
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Err(2))));
|
|
assert_eq!(poll!(rx.next()), Poll::Pending);
|
|
tokio::time::advance(Duration::from_secs(3)).await;
|
|
assert_eq!(poll!(rx.next()), Poll::Pending);
|
|
tx.send(Err(3)).unwrap();
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Err(3))));
|
|
tx.send(Ok(4)).unwrap();
|
|
assert_eq!(poll!(rx.next()), Poll::Pending);
|
|
tokio::time::advance(Duration::from_secs(3)).await;
|
|
assert_eq!(poll!(rx.next()), Poll::Pending);
|
|
tokio::time::advance(Duration::from_secs(2)).await;
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(Some(Ok(4))));
|
|
assert_eq!(poll!(rx.next()), Poll::Pending);
|
|
drop(tx);
|
|
assert_eq!(poll!(rx.next()), Poll::Ready(None));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backoff_should_close_when_requested() {
|
|
assert_eq!(
|
|
StreamBackoff::new(
|
|
stream::iter([Ok(0), Ok(1), Err(2), Ok(3)]),
|
|
backoff::backoff::Stop {},
|
|
TokioSleeper
|
|
)
|
|
.collect::<Vec<_>>()
|
|
.await,
|
|
vec![Ok(0), Ok(1), Err(2)]
|
|
);
|
|
}
|
|
|
|
/// Dynamic backoff policy that is still deterministic and testable
|
|
struct LinearBackoff {
|
|
interval: Duration,
|
|
current_duration: Duration,
|
|
}
|
|
|
|
impl LinearBackoff {
|
|
fn new(interval: Duration) -> Self {
|
|
Self {
|
|
interval,
|
|
current_duration: Duration::ZERO,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Backoff for LinearBackoff {
|
|
fn next_backoff(&mut self) -> Option<Duration> {
|
|
self.current_duration += self.interval;
|
|
Some(self.current_duration)
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.current_duration = Duration::ZERO;
|
|
}
|
|
}
|
|
}
|