use crate::error::StreamBodyError;
use crate::StreamBodyResult;
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
pub(crate) const INITIAL_CAPACITY: usize = 8 * 1024;
const DEFAULT_PROGRESS_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReqwestStreamOutcome {
InProgress,
Completed,
Failed,
Aborted,
}
impl ReqwestStreamOutcome {
pub fn as_str(&self) -> &'static str {
match self {
ReqwestStreamOutcome::InProgress => "in_progress",
ReqwestStreamOutcome::Completed => "completed",
ReqwestStreamOutcome::Failed => "failed",
ReqwestStreamOutcome::Aborted => "aborted",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReqwestStreamProgress {
pub items: u64,
pub bytes: u64,
pub errors: u64,
pub elapsed: Duration,
pub outcome: ReqwestStreamOutcome,
}
pub type ReqwestStreamErrorHandler = Arc<dyn Fn(&StreamBodyError) + Send + Sync + 'static>;
pub type ReqwestStreamProgressHandler = Arc<dyn Fn(&ReqwestStreamProgress) + Send + Sync + 'static>;
#[non_exhaustive]
pub struct ReqwestStreamOptions {
pub max_obj_len: usize,
pub buf_capacity: usize,
pub on_error: Option<ReqwestStreamErrorHandler>,
pub on_progress: Option<ReqwestStreamProgressHandler>,
pub progress_interval: Option<Duration>,
pub progress_items: Option<u64>,
}
impl Default for ReqwestStreamOptions {
fn default() -> Self {
Self::new()
}
}
impl ReqwestStreamOptions {
pub fn new() -> Self {
Self {
max_obj_len: usize::MAX,
buf_capacity: INITIAL_CAPACITY,
on_error: None,
on_progress: None,
progress_interval: Some(DEFAULT_PROGRESS_INTERVAL),
progress_items: None,
}
}
pub fn max_obj_len(mut self, max_obj_len: usize) -> Self {
self.max_obj_len = max_obj_len;
self
}
pub fn buf_capacity(mut self, buf_capacity: usize) -> Self {
self.buf_capacity = buf_capacity;
self
}
pub fn on_error<F>(mut self, handler: F) -> Self
where
F: Fn(&StreamBodyError) + Send + Sync + 'static,
{
self.on_error = Some(Arc::new(handler));
self
}
pub fn on_progress<F>(mut self, handler: F) -> Self
where
F: Fn(&ReqwestStreamProgress) + Send + Sync + 'static,
{
self.on_progress = Some(Arc::new(handler));
self
}
pub fn progress_interval(mut self, interval: Duration) -> Self {
self.progress_interval = Some(interval);
self
}
pub fn progress_items(mut self, items: u64) -> Self {
self.progress_items = Some(items);
self
}
}
pub(crate) trait ProgressItem {
fn stream_error(&self) -> Option<&StreamBodyError>;
}
impl<T> ProgressItem for StreamBodyResult<T> {
fn stream_error(&self) -> Option<&StreamBodyError> {
self.as_ref().err()
}
}
struct ProgressState {
items: AtomicU64,
bytes: AtomicU64,
errors: AtomicU64,
last_emit_micros: AtomicU64,
next_item_step: AtomicU64,
polled: AtomicBool,
finalized: AtomicBool,
start: Instant,
interval_micros: Option<u64>,
item_step: Option<u64>,
on_error: Option<ReqwestStreamErrorHandler>,
on_progress: Option<ReqwestStreamProgressHandler>,
#[cfg(feature = "tracing")]
span: tracing::Span,
}
#[cfg(feature = "tracing")]
fn tracing_enabled() -> bool {
tracing::enabled!(target: "reqwest_streams", tracing::Level::ERROR)
}
#[cfg(not(feature = "tracing"))]
fn tracing_enabled() -> bool {
false
}
impl ProgressState {
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn maybe_new(
format: &'static str,
response: &reqwest::Response,
options: &ReqwestStreamOptions,
) -> Option<Arc<Self>> {
if options.on_progress.is_none() && options.on_error.is_none() && !tracing_enabled() {
return None;
}
let item_step = options.progress_items.filter(|step| *step > 0);
Some(Arc::new(Self {
items: AtomicU64::new(0),
bytes: AtomicU64::new(0),
errors: AtomicU64::new(0),
last_emit_micros: AtomicU64::new(0),
next_item_step: AtomicU64::new(item_step.unwrap_or(u64::MAX)),
polled: AtomicBool::new(false),
finalized: AtomicBool::new(false),
start: Instant::now(),
interval_micros: options
.progress_interval
.map(|interval| interval.as_micros() as u64),
item_step,
on_error: options.on_error.clone(),
on_progress: options.on_progress.clone(),
#[cfg(feature = "tracing")]
span: Self::new_span(format, response, options),
}))
}
#[cfg(feature = "tracing")]
fn new_span(
format: &'static str,
response: &reqwest::Response,
options: &ReqwestStreamOptions,
) -> tracing::Span {
let span = tracing::info_span!(
target: "reqwest_streams",
"reqwest_streams::response_stream",
format = format,
status = response.status().as_u16(),
content_length = response.content_length(),
max_obj_len = tracing::field::Empty,
buf_capacity = options.buf_capacity as u64,
items = tracing::field::Empty,
bytes = tracing::field::Empty,
errors = tracing::field::Empty,
elapsed_ms = tracing::field::Empty,
outcome = tracing::field::Empty,
);
if options.max_obj_len != usize::MAX {
span.record("max_obj_len", options.max_obj_len as u64);
}
span
}
fn record_bytes(&self, len: u64) {
let bytes = self.bytes.fetch_add(len, Ordering::Relaxed) + len;
let items = self.items.load(Ordering::Relaxed);
#[cfg(feature = "tracing")]
tracing::trace!(
target: "reqwest_streams",
parent: &self.span,
chunk_bytes = len,
items,
bytes,
"Read an HTTP body chunk"
);
if !self.finalized.load(Ordering::Relaxed) && self.should_emit(items) {
self.emit(
ReqwestStreamOutcome::InProgress,
items,
bytes,
self.errors.load(Ordering::Relaxed),
);
}
}
fn record_item(&self) {
let items = self.items.fetch_add(1, Ordering::Relaxed) + 1;
if !self.finalized.load(Ordering::Relaxed) && self.should_emit(items) {
self.emit(
ReqwestStreamOutcome::InProgress,
items,
self.bytes.load(Ordering::Relaxed),
self.errors.load(Ordering::Relaxed),
);
}
}
fn record_error(&self, err: &StreamBodyError) {
self.errors.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "tracing")]
tracing::error!(
target: "reqwest_streams",
parent: &self.span,
error = %err,
error_kind = err.kind().as_str(),
"An error occurred while streaming an HTTP body"
);
if let Some(handler) = &self.on_error {
handler(err);
}
}
fn should_emit(&self, items: u64) -> bool {
let mut emit = false;
if let Some(step) = self.item_step {
if items >= self.next_item_step.load(Ordering::Relaxed) {
self.next_item_step
.store(items - (items % step) + step, Ordering::Relaxed);
emit = true;
}
}
if let Some(interval) = self.interval_micros {
let elapsed = self.start.elapsed().as_micros() as u64;
let since_last = elapsed.saturating_sub(self.last_emit_micros.load(Ordering::Relaxed));
if emit || since_last >= interval {
self.last_emit_micros.store(elapsed, Ordering::Relaxed);
emit = true;
}
}
emit
}
fn mark_polled(&self) {
self.polled.store(true, Ordering::Relaxed);
}
fn finalize(&self, aborted: bool) {
if !self.polled.load(Ordering::Relaxed) || self.finalized.swap(true, Ordering::Relaxed) {
return;
}
let items = self.items.load(Ordering::Relaxed);
let bytes = self.bytes.load(Ordering::Relaxed);
let errors = self.errors.load(Ordering::Relaxed);
let outcome = if errors > 0 {
ReqwestStreamOutcome::Failed
} else if aborted {
ReqwestStreamOutcome::Aborted
} else {
ReqwestStreamOutcome::Completed
};
#[cfg(feature = "tracing")]
{
self.span.record("items", items);
self.span.record("bytes", bytes);
self.span.record("errors", errors);
self.span
.record("elapsed_ms", self.start.elapsed().as_millis() as u64);
self.span.record("outcome", outcome.as_str());
}
self.emit(outcome, items, bytes, errors);
}
fn emit(&self, outcome: ReqwestStreamOutcome, items: u64, bytes: u64, errors: u64) {
let progress = ReqwestStreamProgress {
items,
bytes,
errors,
elapsed: self.start.elapsed(),
outcome,
};
#[cfg(feature = "tracing")]
{
let elapsed_ms = progress.elapsed.as_millis() as u64;
match outcome {
ReqwestStreamOutcome::InProgress => tracing::debug!(
target: "reqwest_streams",
parent: &self.span,
items,
bytes,
elapsed_ms,
"Streaming an HTTP body"
),
ReqwestStreamOutcome::Failed => tracing::error!(
target: "reqwest_streams",
parent: &self.span,
items,
bytes,
errors,
elapsed_ms,
outcome = outcome.as_str(),
"Failed streaming an HTTP body"
),
_ => tracing::info!(
target: "reqwest_streams",
parent: &self.span,
items,
bytes,
errors,
elapsed_ms,
outcome = outcome.as_str(),
"Finished streaming an HTTP body"
),
}
}
if let Some(handler) = &self.on_progress {
handler(&progress);
}
}
}
#[derive(Clone)]
pub(crate) struct Progress(Option<Arc<ProgressState>>);
impl Progress {
pub(crate) fn new(
format: &'static str,
response: &reqwest::Response,
options: &ReqwestStreamOptions,
) -> Self {
Progress(ProgressState::maybe_new(format, response, options))
}
}
pub(crate) fn count_bytes<'b, S>(
stream: S,
progress: &Progress,
) -> impl Stream<Item = Result<Bytes, std::io::Error>> + Send + 'b
where
S: Stream<Item = Result<Bytes, std::io::Error>> + Send + 'b,
{
let progress = progress.clone();
stream.inspect_ok(move |chunk| {
if let Some(state) = &progress.0 {
state.record_bytes(chunk.len() as u64);
}
})
}
pub(crate) fn instrument<'b, S>(
stream: S,
progress: Progress,
) -> impl Stream<Item = S::Item> + Send + 'b
where
S: Stream + Unpin + Send + 'b,
S::Item: ProgressItem,
{
ProgressStream {
inner: stream,
progress,
}
}
struct ProgressStream<S> {
inner: S,
progress: Progress,
}
impl<S> Stream for ProgressStream<S>
where
S: Stream + Unpin,
S::Item: ProgressItem,
{
type Item = S::Item;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let Some(state) = this.progress.0.as_ref() else {
return Pin::new(&mut this.inner).poll_next(cx);
};
#[cfg(feature = "tracing")]
let _entered = state.span.enter();
state.mark_polled();
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(item)) => {
match item.stream_error() {
Some(err) => state.record_error(err),
None => state.record_item(),
}
Poll::Ready(Some(item))
}
Poll::Ready(None) => {
state.finalize(false);
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
}
impl<S> Drop for ProgressStream<S> {
fn drop(&mut self) {
if let Some(state) = &self.progress.0 {
state.finalize(true);
}
}
}