Skip to content

Fix aws_s3_stream EntityTooSmall error for small files - #893

Open
triddell wants to merge 8 commits into
warpstreamlabs:mainfrom
triddell:fix/aws-s3-stream-small-files
Open

Fix aws_s3_stream EntityTooSmall error for small files#893
triddell wants to merge 8 commits into
warpstreamlabs:mainfrom
triddell:fix/aws-s3-stream-small-files

Conversation

@triddell

Copy link
Copy Markdown
Contributor

Description

Fixes the EntityTooSmall error that occurs when aws_s3_stream tries to upload files smaller than 5 MiB using multipart upload.

S3 requires multipart upload parts (except the final part) to be >= 5 MiB. This fix automatically detects when files are too small for multipart and falls back to S3's simple PutObject API instead.

Changes

  • Enhanced Close() logic: Check if any parts have been uploaded before completing multipart upload
  • Smart fallback: Use PutObject for files < 5 MiB
  • Boundary handling: Files >= 5 MiB continue using multipart upload
  • Empty file support: Properly handle empty files using PutObject
  • Comprehensive tests: Added tests for all edge cases and boundaries

Test Coverage

Added comprehensive unit tests:

  • Small file handling (< 5 MiB → PutObject)
  • Boundary cases (exactly 5 MiB, just under 5 MiB)
  • Empty file handling
  • Multiple small writes
  • ContentType and ContentEncoding preservation

All existing tests continue to pass.

Breaking Changes

None. The change is transparent to users - the API and configuration remain the same. Files are uploaded successfully regardless of size.

Checklist

  • Code changes implement the fix
  • Unit tests added for new behavior
  • All existing tests pass
  • No breaking changes to API or configuration
  • Behavior is transparent to users

triddell added 5 commits May 6, 2026 08:51
The lines archive format was not adding a trailing newline after the last
message in a batch. When multiple batches are written to the same output
stream (e.g., with aws_s3_stream and batching enabled), the batches would
concatenate without proper separation, causing the last line of one batch
to merge with the first line of the next batch.

This resulted in "message contains multiple valid documents" errors when
parsing JSONL files, as two JSON objects would appear on a single line.

The fix adds a trailing newline to ensure each batch is properly terminated
and can be safely concatenated with subsequent batches.
Prevents EntityTooSmall errors by ensuring timer-based flushes never upload
parts < 5MB. S3 requires all non-final parts to be >= 5MB; only the final
part (uploaded during Close) can be smaller.
@maxtheaxe

Copy link
Copy Markdown

@triddell this still doesn't actually guarantee that messages get delivered, since it's not actually acking part uploads, doesn't support resuming in-progress uploads, and aborts in-progress uploads for messages that have already been acked. please take a look at my PR #895, which solves your <5MB problem and also the other aforementioned issues, among other things.

@triddell

Copy link
Copy Markdown
Contributor Author

@maxtheaxe hey, I'll take a look and do some testing in my environment with it. those are great additions. have you been using this output?

@maxtheaxe

Copy link
Copy Markdown

@triddell am trying to—I'm thinking in addition to the things I added here, I probably also need an idle_timeout (akin to the read_until input) per file on the aws_s3_stream output first

@triddell

Copy link
Copy Markdown
Contributor Author

@maxtheaxe I spent some time testing #895 against a real S3 bucket as promised —
the delayed-ack and resume work is genuinely better than what I have here, and I'd
be glad to see it land.

One thing did turn up though: against real S3, #895 deadlocks on shutdown for
streams that stay under the 5 MiB multipart threshold — i.e. the small-file case
this PR was opened for. The object gets written, but because the messages are only
acked in Close() and Close() only runs once the input's transaction channel
closes (which a finite input won't do until its messages are acked), bento never
terminates on a bounded input; it only finishes on SIGTERM. A stdout output with
the same input exits cleanly, and main exits cleanly on the identical config, so
it looks specific to the new ack timing. Full reproduction is in #895.

So the small-file path still needs a fix before #895 fully supersedes this one.
I'm happy to either (a) keep this PR as a minimal, low-risk fix for the <5 MiB /
EntityTooSmall case in the meantime, or (b) help get #895's shutdown path sorted
so it can cover everything — whichever the maintainers prefer. Either way I can
validate against real S3 quickly.

@triddell

triddell commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Flagging a real-world dependency on this one: we run bento embedded in AWS Lambda with bounded inputs
and frequent sub-5 MiB flushes. The delayed-ack rewrite (#897/#895) deadlocks on bounded inputs in that
setup (acks only resolve in Close(), which never runs without a SIGTERM) — details in #897. Until that PR finalizes acks at end-of-input, this ack-on-write + PutObject-fallback
fix is what makes aws_s3_stream usable for embedded/Lambda deployments. Would love to see it land.

@maxtheaxe

Copy link
Copy Markdown

@triddell does usage of read_until in the way I described in the last comment on my PR solve your use case? if so, here's a docker build of the latest there (with the <5MB fix)

@triddell

triddell commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Why this fix is needed — the EntityTooSmall bug is real and standalone

aws_s3_stream writes via S3 multipart uploads, and S3 requires every part except the last to be ≥ 5 MiB. Today the writer can attempt a sub-5 MiB part in two ways — the buffer-period forceFlush timer firing on a small buffer, and small/empty final objects — and S3 rejects both with EntityTooSmall. Net effect: any object under 5 MiB (including empty) fails to upload, so the output doesn't work for low-volume streams or the common "stream one smallish file to S3" case.

This PR fixes exactly that, and only that:

  • Enforce the 5 MiB minimum on every non-final part — never emit a sub-5 MiB mid-stream part (fixes the timer-driven case).
  • Finalize a small-or-empty object with a single PutObject (preserving ContentType/ContentEncoding), aborting the unused multipart upload. Objects ≥ 5 MiB / true multipart uploads are unchanged.

Tests cover small (<5 MiB) → PutObject, empty → PutObject (no CompleteMultipartUpload), exactly 5 MiB → multipart, and a regression test that the period timer no longer flushes a sub-5 MiB buffer as a mid-stream part.

Relationship to #895

#895 is a broader at-least-once / delayed-ack rewrite of the same writer that also covers the small-part case as part of a larger change (delayed acks + interrupted-upload recovery). This PR is intentionally the minimal, behavior-preserving fix: it keeps the output's existing ack-on-write semantics and just makes small/empty objects valid. Those existing semantics are what let the output work for bounded/CLI inputs — "finish the input, write the file, stop." I've written up the full, code-level reason the two approaches can't share one path without an opt-in flag in a comment on #895. Suggested sequencing: land this as the standalone EntityTooSmall bug fix, and let the at-least-once work build on top as opt-in.

@triddell

triddell commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@maxtheaxe Just made the comment above here and then another on your PR. It seems we can't do both of these approaches, what's needed for a CLI execution and what you are looking at for a streaming version, at the same time without a flag changing the behavior. Would like to hear your feedback for sure.

I run Bento in lambdas as a CLI app, actually parallel versions of Bento within a lambda often times. I definitely need the CLI execution to not have to wait for a timeout and I think that would be everyone's expectation. When the input is done, write the file(s), stop the Bento execution.

@maxtheaxe

Copy link
Copy Markdown

@triddell I do think we can do both of these, but I can't spend anymore time on it today. will get back to you soon.

@triddell
triddell force-pushed the fix/aws-s3-stream-small-files branch from c6327b1 to 92769be Compare July 8, 2026 00:08
@triddell

triddell commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Follow-on change on this branch: an opt-in compression: gzip field on aws_s3_stream.

Doing gzip via a compress batch processor runs before partition_by, which caused two prod issues: per-message compress produced multi-member gzip files, and batch archive+compress merged records across partitions. This moves compression into the writer, after partitioning.

  • One *gzip.Writer per partition writer → exactly one continuous gzip stream per object.
  • Multipart-safe: part sizing on compressed bytes; the gzip writer is never flushed/reset mid-stream, only closed once for the footer, so S3 reassembles the parts into one valid stream.
  • Integrates with this PR's small-file path: footer is finalized before the < 5 MiB check, so PutObject handles the compressed body (empty → valid empty gzip).
  • Sets Content-Encoding: gzip automatically; default none, fully backward compatible.
  • Covered by unit tests (small/multipart/empty) and a LocalStack integration test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants