Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions src/sources/util/http/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,12 @@ pub(crate) fn limited_body(max_body_size: usize) -> BoxedFilter<(Bytes,)> {
max_body_size,
)))
} else {
Ok(())
Ok(declared)
}
})
.untuple_one()
.and(warp::body::stream())
.and_then(move |body| async move {
collect_body_with_limit(body, max_body_size)
.and_then(move |declared: Option<u64>, body| async move {
collect_body_with_limit(body, max_body_size, declared)
.await
.map_err(warp::reject::custom)
})
Expand Down Expand Up @@ -178,19 +177,35 @@ fn decompress_snappy(
Ok(decoded.into())
}

/// Cap on the initial buffer allocation when pre-sizing from a `Content-Length` header.
/// Prevents stalling clients from reserving large amounts of memory before sending any bytes.
#[cfg(any(
feature = "sources-utils-http-prelude",
feature = "sources-opentelemetry",
test
))]
async fn collect_body_with_limit<S, B>(body: S, max_body_size: usize) -> Result<Bytes, ErrorMessage>
const MAX_INITIAL_BODY_CAPACITY: usize = 8 * 1024 * 1024; // 8 MiB

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edge case: if VECTOR_MAX_DECOMPRESSED_SIZE_BYTES/--max-decompressed-size-bytes is set to a value < 8MiB we should honor it by not allocating more than 32 + max_size + max_size / 6 (this is snappy's worst-case scenario, which is larger than gzip, zstd and zlib's worst-case scenarios)


#[cfg(any(
feature = "sources-utils-http-prelude",
feature = "sources-opentelemetry",
test
))]
async fn collect_body_with_limit<S, B>(
body: S,
max_body_size: usize,
declared_len: Option<u64>,
) -> Result<Bytes, ErrorMessage>
where
S: futures_util::Stream<Item = Result<B, warp::Error>>,
B: Buf,
{
futures_util::pin_mut!(body);

let mut bytes = BytesMut::new();
let capacity = declared_len
.and_then(|len| usize::try_from(len).ok())
.map_or(0, |len| len.min(max_body_size).min(MAX_INITIAL_BODY_CAPACITY));
let mut bytes = BytesMut::with_capacity(capacity);
Comment on lines +205 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid allocating the full declared body before reads

When an unauthenticated HTTP client sends a Content-Length near the configured limit (100 MiB by default via max_decompressed_size_bytes()) and then stalls or disconnects, this allocates that entire buffer before any body bytes are received. I checked the HTTP source call sites in src/sources/util/http/prelude.rs and src/sources/opentelemetry/http.rs; both use this helper for incoming requests, so many concurrent header-only/slow requests can now reserve large amounts of memory where the previous streaming collector only grew after chunks arrived. Consider capping the initial preallocation to a smaller threshold or growing after the first chunk instead of trusting the header up to the full body limit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. IMO, it's acceptable given the performance regression that the alternative causes.
What do you guys think about this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd agree with its suggestion to cap the initial capacity at some suitably large but smaller-than-100MiB number. That would minimize still reallocations, just not eliminate them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I set 8 MB somewhat arbitrarily some Vector constants are around these values: Kinesis Firehose: 4 MiB, syslog recv buffer: 4 MiB, Windows event log: 10 MiB), does that seem reasonable?

while let Some(chunk) = body.next().await {
let chunk = chunk.map_err(|error| {
ErrorMessage::new(
Expand Down Expand Up @@ -368,7 +383,7 @@ mod tests {
Ok::<_, warp::Error>(Bytes::from_static(b" world")),
]);

let collected = collect_body_with_limit(body, 11).await.unwrap();
let collected = collect_body_with_limit(body, 11, Some(11)).await.unwrap();
assert_eq!(collected, Bytes::from_static(b"hello world"));
}

Expand All @@ -379,7 +394,7 @@ mod tests {
Ok::<_, warp::Error>(Bytes::from_static(b" world")),
]);

let err = collect_body_with_limit(body, 5)
let err = collect_body_with_limit(body, 5, None)
.await
.expect_err("should reject");
assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
Expand Down
Loading