feat(compression-coordinator): Add CompressionInputBuilder for partitioning S3 objects into compression-task inputs. - #2403
Conversation
The initial and maximum job-state poll backoffs were fixed constants inside the Spider implementation, so a caller had no way to tune how aggressively a job is polled. Both are now `Duration` parameters of `run_s3_compression_job_to_completion`, leaving the pacing decision with the caller that knows how long its jobs typically run.
WalkthroughChangesAdds public S3 input partitioning with bucket and key-prefix validation, estimated-size buffering, filename grouping, round-robin ordering, threshold-based batch emission, and unit tests. Compression coordination
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant CompressionInputBuilder
participant RoundRobinIterator
participant S3InputSource
Caller->>CompressionInputBuilder: add(ObjectMetadata)
CompressionInputBuilder->>RoundRobinIterator: consume grouped FileMetadata
CompressionInputBuilder->>S3InputSource: create S3 input batch
CompressionInputBuilder-->>Caller: return partitioned inputs
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bill's partition.rs is byte-identical to the version this branch started from, so the refactored version is kept as-is. His merge of main brings only y-scope#2401, which this branch already has via rebase. The net change is his combined crate doc in lib.rs.
LinZhihao-723
left a comment
There was a problem hiding this comment.
Summary of changes
Everything below was applied to partition.rs (plus a new error variant in error.rs and dev-dependencies in Cargo.toml). No production behavior changed except where called out explicitly.
Naming
PathsToCompressBuffer→CompressionInputBuilder— Why: the name came from the Python and described neither what the type holds nor what it does. It holds object metadata, not paths, and its job is to build compression-task inputs rather than merely to buffer.RoundRobinPartition→RoundRobinIterator— Why: it was never a partition; it is the rotating source that partitions are drawn from. The new name also matches the one trait it implements, which is the crate's onlyIteratorimpl.- Methods that lied about their effect —
partition_and_compress→partition,submit_partition_for_compression→push_input_source. Why: neither compressed nor submitted anything; both only appended to an in-memory vector. The names were inherited from Python, where the real dispatch happens in a separate scheduler far from this file — context that does not exist in the Rust crate. get_tasks_input_sources→into_task_input_sources— Why: Rust convention. A conversion that consumesselfisinto_*, and theget_prefix is discouraged.- Fields aligned with what they hold —
tasks_input_sources→partitioned_task_inputs,total_file_size→total_buffered_size,file_size_to_trigger_compression→buffer_size_to_trigger_partition,files→buffer. Why: the old names described sizes of individual files rather than of the buffer, and referred to "compression" for a threshold that triggers partitioning. add_file→add,into_remaining_files→into_flatten.- Derived identifiers kept in sync — locals renamed alongside their types. Why: the review guide treats half-applied renames as defects, and locals are the part most easily missed.
Data representation
- Buffer holds a private
FileMetadata { path, estimated_size }instead ofObjectMetadata. Why: storing fullObjectMetadataper object was wasteful — the bucket is identical for every entry (now enforced byadd), and the raw size has no consumer once the estimate exists. This restores the shape of Python's ownFileMetadata. - The estimate is computed once, in the constructor. Why: it was previously recomputed on every partitioning pass, and computing it in
FileMetadata::newmeans no call site can forget it.
Control flow
has_more_filesflag replaced by a labeledbreak 'partitioning. Why: the flag existed only to communicate loop exit between two nested loops, which a label states directly.total_buffered_sizedecremented per file rather than per partition. Why: the invariant becomes continuous — the total always equals the sum over files not yet pulled, which is exactly what is written back tobuffer— so the underflow argument is local rather than global. It also retired the two-line duplication the labeled break had forced onto the exhaustion path.group_files_by_similar_filenamesrewritten as a singlefoldwith the predicate in a match guard. Why: eliminates oneStringallocation per file that existed only to dodge a borrow conflict, removes the empty-input special case, and inverts the predicate so>= thresholdreads as "same group" instead of< thresholdmeaning "start a new one".
API surface and behavior
addvalidates the object's bucket and returnsResult<(), Error>, with a newError::S3BucketMismatchvariant. Why: an object from a different bucket would otherwise be silently emitted inside anS3InputSourcecarrying the builder's bucket — a task pointed at a key that does not exist where it was told to look. Validation happens before any mutation, so a rejected object leaves no trace. This is a behavior change.into_task_input_sourcesflushes internally;flushis now private. Why: previouslyflushtook&mut selfwhile the getter consumedself, so a caller who forgot to flush silently discarded every object not yet forming a full partition. Moving the call inside removed the obligation, and with it the reason to exposeflushat all — its only remaining use was the footgun of forcing out an undersized partition mid-stream. The public surface is now exactlynew/add/into_task_input_sources, with no ordering a caller can get wrong. This is a behavior change.
Documentation
- Docstrings rewritten across the type, including the struct's own, which now explains what a partition is — the first place in the file to do so.
Tests
- Three unit tests added (none existed): partitioning deferred to the internal flush, partitioning triggered on the fly, and
RoundRobinIteratorrotation. Why: every restructuring above had been verified only by throwaway fuzzers that do not live in the repo. - Two invariants asserted on both partition tests — every object appears exactly once across the output, and every partition satisfies
total - last < target_archive_size. Why: the second is a direct transcription of the loop's postcondition and applies to every partition, including the sub-target final one. - Mutation-verified. Four deliberate regressions were introduced and confirmed to fail the tests. Why: passing tests only prove the tests and the code agree; this shows they detect breakage.
@Bill-hbrhbr Please review my changes carefully. |
CompressionInputBuilder for partitioning S3 objects into compression-task inputs.
LinZhihao-723
left a comment
There was a problem hiding this comment.
Directly modified the PR title.
# Conflicts: # Cargo.lock # components/compression-coordinator/Cargo.toml # components/compression-coordinator/src/compression_job_submitter/spider.rs # components/compression-coordinator/src/error.rs
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/compression-coordinator/src/partition.rs`:
- Around line 115-144: Prevent target_archive_size from being zero in the
partitioning flow: preferably validate and reject zero during configuration or
builder construction before partition() runs. If zero must remain accepted,
update partition() so each partition consumes at least one file and advances
total_buffered_size, preventing add() from looping indefinitely.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7cd853e6-bf7d-47a9-a7b7-ad1f264b6972
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
components/compression-coordinator/Cargo.tomlcomponents/compression-coordinator/src/error.rscomponents/compression-coordinator/src/lib.rscomponents/compression-coordinator/src/partition.rs
| fn partition(&mut self, flush_buffer: bool) { | ||
| if !flush_buffer && self.total_buffered_size < self.target_archive_size { | ||
| return; | ||
| } | ||
| if self.buffer.is_empty() { | ||
| return; | ||
| } | ||
|
|
||
| let mut rr_iterator = RoundRobinIterator::new(std::mem::take(&mut self.buffer)); | ||
|
|
||
| 'partitioning: while flush_buffer || self.total_buffered_size >= self.target_archive_size { | ||
| let mut partition = Vec::new(); | ||
| let mut partition_size = 0; | ||
|
|
||
| while partition_size < self.target_archive_size { | ||
| let Some(file) = rr_iterator.next() else { | ||
| self.push_input_source(partition); | ||
| break 'partitioning; | ||
| }; | ||
|
|
||
| partition_size += file.estimated_size; | ||
| self.total_buffered_size -= file.estimated_size; | ||
| partition.push(file.path); | ||
| } | ||
|
|
||
| self.push_input_source(partition); | ||
| } | ||
|
|
||
| self.buffer = rr_iterator.into_flatten(); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether target_archive_size is validated to be non-zero anywhere upstream.
rg -n 'target_archive_size' --type=rust -C3Repository: y-scope/clp
Length of output: 149
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== partition.rs ==\n'
sed -n '1,260p' components/compression-coordinator/src/partition.rs
printf '\n== search target_archive_size and archive_size ==\n'
rg -n 'target_archive_size|archive_size' components -C 3 || true
printf '\n== file list around config types ==\n'
git ls-files | rg 'clp_io_config|config|output|compression-coordinator|partition\.rs'Repository: y-scope/clp
Length of output: 50367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('components/compression-coordinator/src/partition.rs')
print(p.exists(), p.stat().st_size if p.exists() else None)
PYRepository: y-scope/clp
Length of output: 160
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for any validation of zero/non-zero archive size in the coordinator area.
rg -n 'NonZero|zero|target_archive_size|archive size' components/compression-coordinator -C 2 || trueRepository: y-scope/clp
Length of output: 5151
Guard against target_archive_size == 0.
partition() never consumes a file in that case, so total_buffered_size never changes and add() can spin forever because the trigger threshold is also zero. Reject zero at config/build time or force one file through per partition.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/compression-coordinator/src/partition.rs` around lines 115 - 144,
Prevent target_archive_size from being zero in the partitioning flow: preferably
validate and reject zero during configuration or builder construction before
partition() runs. If zero must remain accepted, update partition() so each
partition consumes at least one file and advances total_buffered_size,
preventing add() from looping indefinitely.
There was a problem hiding this comment.
@coderabbitai open an issue to use NonZero rust type for this./
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
gh issue create \
--repo y-scope/clp \
--assignee Bill-hbrhbr \
--title "Use a non-zero Rust type for compression target archive size" \
--body "$(cat <<'EOF'
## Summary
Represent the compression target archive size with an appropriate Rust non-zero integer type (for example, `std::num::NonZeroU64`) rather than a plain integer that permits zero.
## Rationale
`CompressionInputBuilder` in `components/compression-coordinator/src/partition.rs` relies on `target_archive_size` as a progress threshold. A value of zero can cause the partitioning loop to emit empty partitions without consuming buffered files, so `add()` may not terminate. Encoding the non-zero constraint in the configuration type prevents this invalid state from reaching the partitioner.
## Affected areas
- The configuration type that defines `target_archive_size`
- Configuration deserialization and validation
- `components/compression-coordinator/src/partition.rs`
- Any call sites, tests, or serialization paths using `target_archive_size`
## Acceptance criteria
- `target_archive_size` is represented as a non-zero Rust integer type throughout the relevant configuration and partitioning interfaces.
- Zero is rejected during configuration parsing/validation with a clear error.
- `CompressionInputBuilder` no longer needs to handle a zero target archive size at runtime.
- Existing configuration consumers and tests are updated, with coverage that zero is rejected.
## Backlinks
- Pull request: https://github.com/y-scope/clp/pull/2403
- Review comment: https://github.com/y-scope/clp/pull/2403#discussion_r3617931013
Requested by `@Bill-hbrhbr`.
EOF
)"Length of output: 1852
@Bill-hbrhbr, created and assigned the follow-up issue: #2412 — Use a non-zero Rust type for compression target archive size.
Co-authored-by: Bingran Hu <bingran.hu@yscope.com>
# Conflicts: # Cargo.lock
Co-authored-by: Bingran Hu <bingran.hu@yscope.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/compression-coordinator/Cargo.toml`:
- Around line 20-23: Move the tokio and tracing entries from [dev-dependencies]
to [dependencies] in Cargo.toml, preserving their existing versions and
features. Keep non-empty-string under [dev-dependencies], and ensure the runtime
imports used by spider.rs resolve in normal builds.
In `@components/compression-coordinator/src/error.rs`:
- Line 6: Remove the stray “<<<<<<< HEAD” merge-conflict marker at the start of
error.rs, leaving valid Rust syntax and preserving the surrounding error
definitions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 06931401-25a0-483e-bce1-7a0dedee4778
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
components/compression-coordinator/Cargo.tomlcomponents/compression-coordinator/src/compression_job_submitter/spider.rscomponents/compression-coordinator/src/error.rs
Description
Adds the Rust implementation of the compression coordinator’s file-buffer partitioning algorithm, based on the previous job-orchestration implementation.
The algorithm follows the original path used when ingestion file ordering does not need to be maintained.
It first groups files by similar log filenames using Levenshtein similarity, then constructs compression partitions by selecting files from each group in round-robin order. This prevents a single log stream or filename group from dominating the compression pool.
Checklist
breaking change.
Validation performed
Summary by CodeRabbit