use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use bytes::Bytes;
use futures_core::Stream;
use futures_timer::Delay;
use futures_util::future::{Either, select};
use crate::{Body, HttpRequest, HttpResponse, HttpTransport, TransportError, TransportErrorKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct StreamTimeouts {
pub setup: Option<Duration>,
pub idle: Option<Duration>,
}
impl Default for StreamTimeouts {
fn default() -> Self {
Self {
setup: Some(Self::DEFAULT_SETUP),
idle: Some(Self::DEFAULT_IDLE),
}
}
}
impl StreamTimeouts {
pub const DEFAULT_SETUP: Duration = Duration::from_secs(120);
pub const DEFAULT_IDLE: Duration = Duration::from_secs(300);
#[must_use]
pub const fn unbounded() -> Self {
Self {
setup: None,
idle: None,
}
}
#[must_use]
pub const fn setup(mut self, timeout: Option<Duration>) -> Self {
self.setup = timeout;
self
}
#[must_use]
pub const fn idle(mut self, timeout: Option<Duration>) -> Self {
self.idle = timeout;
self
}
pub async fn send<T>(
self,
transport: &T,
request: HttpRequest,
) -> Result<HttpResponse, TransportError>
where
T: HttpTransport + ?Sized,
{
let sent = transport.send(request);
let response = match self.setup {
Some(limit) => match select(sent, Delay::new(limit)).await {
Either::Left((result, _)) => result?,
Either::Right(((), _)) => return Err(expired("stream setup", limit)),
},
None => sent.await?,
};
let Some(idle) = self.idle else {
return Ok(response);
};
let status = response.status;
let headers = response.headers.clone();
let body = IdleTimeoutBody::new(response.into_body(), idle);
Ok(HttpResponse::new(status, headers, Box::pin(body)))
}
}
struct IdleTimeoutBody {
inner: Option<Body>,
idle: Duration,
timer: Delay,
armed: bool,
}
impl IdleTimeoutBody {
fn new(inner: Body, idle: Duration) -> Self {
Self {
inner: Some(inner),
idle,
timer: Delay::new(idle),
armed: false,
}
}
}
impl Stream for IdleTimeoutBody {
type Item = Result<Bytes, TransportError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = &mut *self;
let Some(inner) = this.inner.as_mut() else {
return Poll::Ready(None);
};
match inner.as_mut().poll_next(cx) {
Poll::Ready(Some(item)) => {
this.armed = false;
Poll::Ready(Some(item))
}
Poll::Ready(None) => {
this.inner = None;
Poll::Ready(None)
}
Poll::Pending => {
if !this.armed {
this.timer.reset(this.idle);
this.armed = true;
}
if Pin::new(&mut this.timer).poll(cx).is_pending() {
return Poll::Pending;
}
this.inner = None;
Poll::Ready(Some(Err(expired(
"wait for the next body chunk",
this.idle,
))))
}
}
}
}
fn expired(what: &str, limit: Duration) -> TransportError {
TransportError::new(
TransportErrorKind::Timeout,
format!("{what} exceeded {limit:?}"),
)
.authored()
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use futures_util::StreamExt;
use super::*;
use crate::{Headers, ReplayResponse, ReplayTransport, SendFuture};
const SHORT: Duration = Duration::from_millis(20);
struct Slow {
headers_after: Duration,
gap: Option<Duration>,
}
impl HttpTransport for Slow {
fn send(&self, _request: HttpRequest) -> SendFuture<'_> {
let gap = self.gap;
let headers_after = self.headers_after;
Box::pin(async move {
Delay::new(headers_after).await;
let body: Body = match gap {
Some(gap) => Box::pin(futures_util::stream::iter(["a", "b", "c"]).then(
move |chunk| async move {
Delay::new(gap).await;
Ok(Bytes::from_static(chunk.as_bytes()))
},
)),
None => Box::pin(futures_util::stream::pending()),
};
Ok(HttpResponse::new(200, Headers::new(), body))
})
}
}
fn request() -> HttpRequest {
HttpRequest::post("https://x", "{}")
}
#[tokio::test]
async fn slow_headers_fail_the_setup_bound() {
let transport = Slow {
headers_after: Duration::from_secs(5),
gap: Some(Duration::ZERO),
};
let err = StreamTimeouts::unbounded()
.setup(Some(SHORT))
.send(&transport, request())
.await
.unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::Timeout);
let err = err.into_error();
assert!(matches!(err, allama::Error::Timeout { .. }));
assert_eq!(err.message(), "stream setup exceeded 20ms");
assert_eq!(
err.as_log().to_string(),
r#"kind=timeout message="stream setup exceeded 20ms""#
);
}
#[tokio::test]
async fn a_stalled_body_fails_the_idle_bound_then_ends() {
let transport = Slow {
headers_after: Duration::ZERO,
gap: None,
};
let response = StreamTimeouts::default()
.idle(Some(SHORT))
.send(&transport, request())
.await
.unwrap();
assert_eq!(response.status, 200);
let mut body = response.into_body();
let err = body.next().await.unwrap().unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::Timeout);
assert_eq!(
err.to_string(),
"request timed out: wait for the next body chunk exceeded 20ms"
);
assert!(body.next().await.is_none());
}
#[tokio::test]
async fn steady_chunks_reset_the_idle_bound() {
let transport = Slow {
headers_after: Duration::ZERO,
gap: Some(Duration::from_millis(30)),
};
let response = StreamTimeouts::unbounded()
.idle(Some(Duration::from_millis(60)))
.send(&transport, request())
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), "abc");
}
#[tokio::test]
async fn slow_consumers_do_not_trip_the_idle_bound() {
let transport = Slow {
headers_after: Duration::ZERO,
gap: Some(SHORT),
};
let response = StreamTimeouts::unbounded()
.idle(Some(Duration::from_millis(100)))
.send(&transport, request())
.await
.unwrap();
let mut body = response.into_body();
let mut chunks = Vec::new();
while let Some(chunk) = body.next().await {
chunks.push(chunk.unwrap());
Delay::new(Duration::from_millis(150)).await;
}
assert_eq!(chunks.concat(), b"abc");
}
#[tokio::test]
async fn a_late_first_poll_does_not_trip_the_idle_bound() {
let transport = Slow {
headers_after: Duration::ZERO,
gap: Some(Duration::from_millis(10)),
};
let response = StreamTimeouts::unbounded()
.idle(Some(Duration::from_millis(50)))
.send(&transport, request())
.await
.unwrap();
Delay::new(Duration::from_millis(100)).await;
assert_eq!(response.text().await.unwrap(), "abc");
}
#[tokio::test]
async fn a_stalled_first_chunk_fails_the_idle_bound() {
let body: Body = Box::pin(futures_util::stream::pending());
let mut body = IdleTimeoutBody::new(body, Duration::from_millis(50));
Delay::new(Duration::from_millis(100)).await;
let started = std::time::Instant::now();
let err = body.next().await.unwrap().unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::Timeout);
assert!(
started.elapsed() >= Duration::from_millis(50),
"{:?}",
started.elapsed()
);
assert!(body.next().await.is_none());
}
#[tokio::test]
async fn a_body_stalled_after_a_chunk_fails_the_idle_bound() {
let body: Body = Box::pin(
futures_util::stream::iter([Ok(Bytes::from_static(b"a"))])
.chain(futures_util::stream::pending()),
);
let mut body = IdleTimeoutBody::new(body, SHORT);
assert_eq!(body.next().await.unwrap().unwrap(), "a");
Delay::new(SHORT * 3).await;
let started = std::time::Instant::now();
let err = body.next().await.unwrap().unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::Timeout);
assert!(started.elapsed() >= SHORT, "{:?}", started.elapsed());
assert!(body.next().await.is_none());
}
#[tokio::test]
async fn unbounded_passes_responses_and_errors_through() {
let transport = ReplayTransport::new([ReplayResponse::new(201, "ok")]);
let response = StreamTimeouts::unbounded()
.send(&transport, request())
.await
.unwrap();
assert_eq!(response.status, 201);
assert_eq!(response.text().await.unwrap(), "ok");
let err = StreamTimeouts::default()
.send(&transport, request())
.await
.unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::Connect);
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use futures_util::StreamExt;
use wasm_bindgen_test::wasm_bindgen_test;
use super::*;
use crate::{Headers, SendFuture};
struct Stalled;
impl HttpTransport for Stalled {
fn send(&self, _request: HttpRequest) -> SendFuture<'_> {
Box::pin(async {
Ok(HttpResponse::new(
200,
Headers::new(),
Box::pin(futures_util::stream::pending()),
))
})
}
}
#[wasm_bindgen_test]
async fn a_stalled_body_fails_the_idle_bound() {
let response = StreamTimeouts::default()
.idle(Some(Duration::from_millis(20)))
.send(&Stalled, HttpRequest::get("https://x"))
.await
.unwrap();
let err = response.into_body().next().await.unwrap().unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::Timeout);
}
}