use crate::streams::{consumer::StreamKey, reconnect::Event};
use derive_more::Constructor;
use futures::{Stream, future::Either, stream};
use futures_util::StreamExt;
use rustrade_integration::channel::Tx;
use serde::{Deserialize, Serialize};
use std::{convert, fmt::Debug, future, future::Future, time::Duration};
use tracing::{error, info, warn};
pub trait ReconnectingStream
where
Self: Stream + Sized,
{
fn with_reconnect_backoff<St, InitError>(
self,
policy: ReconnectionBackoffPolicy,
stream_key: StreamKey,
) -> impl Stream<Item = St>
where
Self: Stream<Item = Result<St, InitError>>,
St: Stream,
InitError: Debug,
{
self.with_reconnect_backoff_reporting(policy, stream_key)
.filter_map(|result| future::ready(result.ok()))
}
fn with_reconnect_backoff_reporting<St, InitError>(
self,
policy: ReconnectionBackoffPolicy,
stream_key: StreamKey,
) -> impl Stream<Item = Result<St, ReinitFailure<InitError>>>
where
Self: Stream<Item = Result<St, InitError>>,
St: Stream,
InitError: Debug,
{
self.with_reconnect_backoff_reporting_hinted(policy, stream_key, |_| None)
}
fn with_reconnect_backoff_reporting_hinted<St, InitError, FnRetryAfter>(
self,
policy: ReconnectionBackoffPolicy,
stream_key: StreamKey,
retry_after: FnRetryAfter,
) -> impl Stream<Item = Result<St, ReinitFailure<InitError>>>
where
Self: Stream<Item = Result<St, InitError>>,
St: Stream,
InitError: Debug,
FnRetryAfter: Fn(&InitError) -> Option<Duration>,
{
self.enumerate()
.scan(
ReconnectionState::from(policy),
move |state, (attempt, result)| {
let next =
match result {
Ok(stream) => {
info!(attempt, ?stream_key, "successfully initialised Stream");
state.reset_backoff();
Either::Left(stream::once(future::ready(Ok(stream))))
}
Err(error) => {
let consecutive = state.record_failure();
let hint = retry_after(&error);
warn!(
attempt,
consecutive,
?stream_key,
?error,
?hint,
"failed to re-initialise Stream"
);
let backoff = state.generate_sleep_future(hint);
state.multiply_backoff();
let failure = ReinitFailure {
attempt: consecutive,
error,
};
Either::Right(stream::once(future::ready(Err(failure))).chain(
stream::once(backoff).filter_map(|()| future::ready(None)),
))
}
};
future::ready(Some(next))
},
)
.flatten()
}
fn with_termination_on_error<St, T, E, FnIsTerminal>(
self,
is_terminal: FnIsTerminal,
stream_key: StreamKey,
) -> impl Stream<Item = impl Stream<Item = Result<T, E>>>
where
Self: Stream<Item = St>,
St: Stream<Item = Result<T, E>>,
FnIsTerminal: Fn(&E) -> bool + Copy,
{
self.map(move |stream| terminate_on_error(stream, is_terminal, stream_key))
}
fn with_reconnection_events<St, Origin>(
self,
origin: Origin,
) -> impl Stream<Item = Event<Origin, St::Item>>
where
Self: Stream<Item = St>,
St: Stream,
Origin: Clone + 'static,
{
self.map(move |stream| with_trailing_reconnecting(stream, origin.clone()))
.flatten()
}
fn with_reconnection_events_reporting<St, InitError, Origin, FnOnFailure>(
self,
origin: Origin,
on_failure: FnOnFailure,
) -> impl Stream<Item = Event<Origin, St::Item>>
where
Self: Stream<Item = Result<St, ReinitFailure<InitError>>>,
St: Stream,
Origin: Clone,
FnOnFailure: Fn(ReinitFailure<InitError>) -> St::Item,
{
self.map(move |initialised| match initialised {
Ok(stream) => Either::Left(with_trailing_reconnecting(stream, origin.clone())),
Err(failure) => Either::Right(stream::once(future::ready(Event::Item(on_failure(
failure,
))))),
})
.flatten()
}
fn with_error_handler<FnOnErr, Origin, T, E>(
self,
op: FnOnErr,
) -> impl Stream<Item = Event<Origin, T>>
where
Self: Stream<Item = Event<Origin, Result<T, E>>>,
FnOnErr: Fn(E) + 'static,
{
self.filter_map(move |event| {
std::future::ready(match event {
Event::Reconnecting(origin) => Some(Event::Reconnecting(origin)),
Event::Item(Ok(item)) => Some(Event::Item(item)),
Event::Item(Err(error)) => {
op(error);
None
}
})
})
}
fn forward_to<Transmitter>(self, tx: Transmitter) -> impl Future<Output = ()> + Send
where
Self: Stream + Sized + Send,
Self::Item: Into<Transmitter::Item>,
Transmitter: Tx + Send + 'static,
{
tokio_stream::StreamExt::map_while(self, move |event| tx.send(event.into()).ok()).collect()
}
}
impl<T> ReconnectingStream for T where T: Stream {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReinitFailure<InitError> {
pub attempt: u32,
pub error: InitError,
}
pub(crate) fn terminate_on_error<St, T, E, FnIsTerminal>(
stream: St,
is_terminal: FnIsTerminal,
stream_key: StreamKey,
) -> impl Stream<Item = Result<T, E>>
where
St: Stream<Item = Result<T, E>>,
FnIsTerminal: Fn(&E) -> bool,
{
tokio_stream::StreamExt::map_while(stream, move |result| match result {
Ok(item) => Some(Ok(item)),
Err(error) if is_terminal(&error) => {
error!(
?stream_key,
"MarketStream encountered terminal error that requires reconnecting"
);
None
}
Err(error) => Some(Err(error)),
})
}
pub(crate) fn with_trailing_reconnecting<St, Origin>(
stream: St,
origin: Origin,
) -> impl Stream<Item = Event<Origin, St::Item>>
where
St: Stream,
{
stream
.map(Event::Item)
.chain(stream::once(future::ready(Event::Reconnecting(origin))))
}
pub async fn init_reconnecting_stream<FnInit, St, FnInitError, FnInitFut>(
init_stream: FnInit,
) -> Result<impl Stream<Item = Result<St, FnInitError>>, FnInitError>
where
FnInit: Fn() -> FnInitFut,
FnInitFut: Future<Output = Result<St, FnInitError>>,
{
let initial = init_stream().await?;
let reconnections = futures::stream::repeat_with(init_stream).then(convert::identity);
Ok(futures::stream::once(future::ready(Ok(initial))).chain(reconnections))
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct ReconnectionBackoffPolicy {
pub backoff_ms_initial: u64,
pub backoff_multiplier: u8,
pub backoff_ms_max: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
struct ReconnectionState {
policy: ReconnectionBackoffPolicy,
backoff_ms_current: u64,
consecutive_failures: u32,
}
impl From<ReconnectionBackoffPolicy> for ReconnectionState {
fn from(policy: ReconnectionBackoffPolicy) -> Self {
Self {
backoff_ms_current: policy.backoff_ms_initial,
policy,
consecutive_failures: 0,
}
}
}
impl ReconnectionState {
fn reset_backoff(&mut self) {
self.backoff_ms_current = self.policy.backoff_ms_initial;
self.consecutive_failures = 0;
}
fn record_failure(&mut self) -> u32 {
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
self.consecutive_failures
}
fn multiply_backoff(&mut self) {
let next = self.backoff_ms_current * self.policy.backoff_multiplier as u64;
let next_capped = std::cmp::min(next, self.policy.backoff_ms_max);
self.backoff_ms_current = next_capped;
}
fn generate_sleep_future(&self, at_least: Option<Duration>) -> tokio::time::Sleep {
let backoff = Duration::from_millis(self.backoff_ms_current);
tokio::time::sleep(at_least.map_or(backoff, |hint| hint.max(backoff)))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rustrade_instrument::exchange::ExchangeId;
use std::time::Duration;
use tokio::time::Instant;
const POLICY: ReconnectionBackoffPolicy = ReconnectionBackoffPolicy {
backoff_ms_initial: 100,
backoff_multiplier: 2,
backoff_ms_max: 1_000,
};
fn key() -> StreamKey {
StreamKey::new_general("test", ExchangeId::Simulated)
}
type Attempt = Result<stream::Iter<std::vec::IntoIter<u8>>, &'static str>;
fn ok(items: &[u8]) -> Attempt {
Ok(stream::iter(items.to_vec()))
}
fn attempts<St>(results: &[Result<St, ReinitFailure<&'static str>>]) -> Vec<u32> {
results
.iter()
.map(|result| {
result
.as_ref()
.map_or_else(|failure| failure.attempt, |_| 0)
})
.collect()
}
#[tokio::test(start_paused = true)]
async fn failures_count_consecutively_and_reset_on_success() {
let results = stream::iter([ok(&[1]), Err("a"), Err("b"), ok(&[2]), Err("c")])
.with_reconnect_backoff_reporting(POLICY, key())
.collect::<Vec<_>>()
.await;
assert_eq!(attempts(&results), [0, 1, 2, 0, 1]);
let Err(failure) = &results[2] else {
panic!("expected a failure")
};
assert_eq!(failure.error, "b");
}
#[tokio::test(start_paused = true)]
async fn a_failure_is_yielded_at_once_and_the_backoff_delays_the_next_attempt() {
let started = Instant::now();
let mut results = Box::pin(
stream::iter([Err("a"), Err("b"), ok(&[1])])
.with_reconnect_backoff_reporting(POLICY, key()),
);
assert!(results.next().await.unwrap().is_err());
assert_eq!(started.elapsed(), Duration::ZERO);
assert!(results.next().await.unwrap().is_err());
assert_eq!(started.elapsed(), Duration::from_millis(100));
assert!(results.next().await.unwrap().is_ok());
assert_eq!(started.elapsed(), Duration::from_millis(300));
}
#[tokio::test(start_paused = true)]
async fn a_hinted_wait_longer_than_the_backoff_holds_the_next_attempt_back() {
let hint = |error: &&str| match *error {
"wait" => Some(Duration::from_millis(500)),
"short" => Some(Duration::from_millis(50)),
_ => None,
};
let started = Instant::now();
let mut results = Box::pin(
stream::iter([Err("wait"), Err("none"), Err("short"), ok(&[1])])
.with_reconnect_backoff_reporting_hinted(POLICY, key(), hint),
);
assert!(results.next().await.unwrap().is_err());
assert_eq!(started.elapsed(), Duration::ZERO);
assert!(results.next().await.unwrap().is_err());
assert_eq!(started.elapsed(), Duration::from_millis(500));
assert!(results.next().await.unwrap().is_err());
assert_eq!(started.elapsed(), Duration::from_millis(700));
assert!(results.next().await.unwrap().is_ok());
assert_eq!(started.elapsed(), Duration::from_millis(1_100));
}
#[tokio::test(start_paused = true)]
async fn the_discarding_backoff_yields_only_initialised_streams() {
let streams = stream::iter([ok(&[1]), Err("a"), ok(&[2])])
.with_reconnect_backoff(POLICY, key())
.flatten()
.collect::<Vec<_>>()
.await;
assert_eq!(streams, [1, 2]);
}
#[tokio::test]
async fn a_failure_becomes_one_item_and_only_an_ended_stream_adds_reconnecting() {
let origin = ExchangeId::Simulated;
let failure = |attempt| ReinitFailure {
attempt,
error: "refused",
};
let events = stream::iter([
Ok(stream::iter(vec![1_u32])),
Err(failure(1)),
Err(failure(2)),
Ok(stream::iter(vec![2])),
])
.with_reconnection_events_reporting(origin, |failure| 100 + failure.attempt)
.collect::<Vec<_>>()
.await;
assert_eq!(
events,
[
Event::Item(1),
Event::Reconnecting(origin),
Event::Item(101),
Event::Item(102),
Event::Item(2),
Event::Reconnecting(origin),
]
);
}
}