Commit 7513a6b
fix(s3-output)!: stream uploads via TM PartStream, bound peak RAM
The S3 sink used to accumulate every per-prefix batch in a single
`CodecEncoder<Vec<u8>>` for the lifetime of the run (default mode) or
until `batch_max_mb` crossed. On long runs this OOM'd: with N prefixes
and M MB matched per prefix, peak RAM = N × M before the first byte
was uploaded.
Replace the buffer with a streaming pipeline:
filter worker → CodecEncoder<ChannelWriter> → mpsc(Bytes, cap=2)
→ EncoderPartStream → tm::io::InputStream::from_part_stream
→ TM multipart upload
`ChannelWriter` is a sync `io::Write` that ships each codec block as a
`Bytes` chunk through a tokio mpsc. `EncoderPartStream` impls TM's
`PartStream`, aggregating up to `multipart_part_mb` before yielding one
`PartData` per poll; the trailing partial is marked `is_last(true)` on
channel EOF. Peak resident per active prefix drops to
~`channel_cap × block_size + part_size` (sub-10 MB at default settings)
regardless of total batch size.
PrefixBatch now holds `upload: Option<ActiveUpload>` — opened lazily on
the first matched line (no empty MPUs), closed on `bytes_sent >=
batch_max_mb` or end-of-run. Each open spawns a driver task that joins
the TM handle and folds per-batch counts into global counters (or
`lines_dropped` + `fatal` on failure). `finish()` closes any trailing
uploads on a blocking thread (codec frame trailer writes hit
`ChannelWriter::write` → `blocking_send`, which panics from a runtime
worker) and awaits all driver handles.
Trade-offs documented in the module header:
- Always-MPU. `from_part_stream` is MPU-only; sub-multipart_threshold
batches now use 3 S3 API calls instead of 1. Functional only.
- No full-batch retry. Once compressed bytes leave the encoder they're
gone — TM's per-part retries are the only retry. The sink already
treated failed uploads as dropped batches, so this is a
non-regression.
Removed dead pool plumbing as part of the rewrite:
- S3OutputConfig.upload_tasks (and the YAML knob)
- OutputCli.s3_upload_tasks
- --s3-output-upload-tasks CLI flag
- UploadJob struct, upload_tx flume queue, uploader_task
HTTP sink's `upload_tasks` is unaffected — that path still uses a
worker pool.
Instrumentation: `Inner.peak_inflight_bytes` (sink-global high-water
of bytes resident in mpsc + reader pending across all active uploads)
is now surfaced in `OutputStats.extras` so the e2e suite can assert
it. `report_completion` emits `extras` as a JSON string instead of
Debug repr so consumers (and tests) can parse it.
Test coverage (test-first):
- s3_output_does_not_buffer_whole_run_in_memory — bulky fixture
(200K matches × 2 prefixes ≈ 56 MB plaintext), default mode, asserts
peak_inflight_bytes ≤ 12 MB. Pre-fix: 61 MB resident → fails.
Post-fix: passes well under cap.
- Existing s3_output_end_to_end_{no_batching, batched, gzip, plaintext,
multipart, json_array} all still pass against the streaming path.
- New unit tests on ChannelWriter (inflight/peak/bytes_sent accounting,
BrokenPipe on reader drop).
BREAKING: --s3-output-upload-tasks CLI flag and S3OutputConfig.upload_tasks
removed (the per-prefix streaming model has no worker pool to size).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 7b3f9cf commit 7513a6b
7 files changed
Lines changed: 840 additions & 289 deletions
File tree
- src
- config
- pipeline
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
168 | | - | |
169 | | - | |
170 | 168 | | |
171 | 169 | | |
172 | 170 | | |
| |||
482 | 480 | | |
483 | 481 | | |
484 | 482 | | |
485 | | - | |
486 | 483 | | |
487 | 484 | | |
488 | 485 | | |
| |||
526 | 523 | | |
527 | 524 | | |
528 | 525 | | |
529 | | - | |
530 | 526 | | |
531 | 527 | | |
532 | 528 | | |
| |||
545 | 541 | | |
546 | 542 | | |
547 | 543 | | |
548 | | - | |
549 | 544 | | |
550 | 545 | | |
551 | 546 | | |
| |||
632 | 627 | | |
633 | 628 | | |
634 | 629 | | |
635 | | - | |
636 | 630 | | |
637 | 631 | | |
638 | 632 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
82 | 81 | | |
83 | 82 | | |
84 | 83 | | |
| |||
150 | 149 | | |
151 | 150 | | |
152 | 151 | | |
153 | | - | |
154 | 152 | | |
155 | 153 | | |
156 | 154 | | |
| |||
271 | 269 | | |
272 | 270 | | |
273 | 271 | | |
274 | | - | |
275 | 272 | | |
276 | 273 | | |
277 | 274 | | |
| |||
337 | 334 | | |
338 | 335 | | |
339 | 336 | | |
340 | | - | |
341 | 337 | | |
342 | 338 | | |
343 | 339 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
267 | | - | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | 267 | | |
272 | 268 | | |
273 | 269 | | |
| |||
321 | 317 | | |
322 | 318 | | |
323 | 319 | | |
324 | | - | |
325 | 320 | | |
326 | 321 | | |
327 | 322 | | |
| |||
814 | 809 | | |
815 | 810 | | |
816 | 811 | | |
817 | | - | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
818 | 817 | | |
819 | 818 | | |
820 | 819 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
0 commit comments