Skip to content

feat(compression-coordinator): Add CompressionInputBuilder for partitioning S3 objects into compression-task inputs. - #2403

Merged
Bill-hbrhbr merged 24 commits into
y-scope:mainfrom
Bill-hbrhbr:compression-coordinator-partition
Jul 22, 2026
Merged

feat(compression-coordinator): Add CompressionInputBuilder for partitioning S3 objects into compression-task inputs.#2403
Bill-hbrhbr merged 24 commits into
y-scope:mainfrom
Bill-hbrhbr:compression-coordinator-partition

Conversation

@Bill-hbrhbr

@Bill-hbrhbr Bill-hbrhbr commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  • Ensure all workflows pass.
  • Add unit tests to cover basic partition behavior.

Summary by CodeRabbit

  • New Features
    • Added S3 object partitioning to generate compression task inputs, including buffered batching and final flush behaviour.
    • Batches are arranged using filename-similarity so related objects are preserved in ordering.
    • Added bucket and key-prefix validation before accepting objects for partitioning.
  • Bug Fixes
    • Improved S3-specific error reporting for bucket and key-prefix mismatches during task input generation.

LinZhihao-723 and others added 10 commits July 20, 2026 08:04
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.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Adds 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

Layer / File(s) Summary
Partitioning contracts and wiring
components/compression-coordinator/src/lib.rs, components/compression-coordinator/src/error.rs, components/compression-coordinator/Cargo.toml, components/compression-coordinator/src/partition.rs
Exports the partition module, adds dependencies, and introduces S3 bucket and key-prefix mismatch errors.
Input buffering and batch emission
components/compression-coordinator/src/partition.rs
CompressionInputBuilder estimates object sizes, groups similar filenames, orders files round-robin, emits S3InputSource batches, and tests flush and on-the-fly partitioning.

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
Loading

Possibly related PRs

  • y-scope/clp#2401: Introduces the S3 task I/O types consumed by the partitioning module.
  • y-scope/clp#2402: Updates the shared coordinator error type used by this change.

Suggested reviewers: linzhihao-723

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding CompressionInputBuilder to partition S3 objects into compression-task inputs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Bill-hbrhbr Bill-hbrhbr changed the title feat(compression-coordinator): Add partition mechanism. feat(compression-coordinator): Add pre-submission file-buffer partitioning. Jul 20, 2026
Bill-hbrhbr and others added 4 commits July 20, 2026 15:47
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
LinZhihao-723 previously approved these changes Jul 20, 2026

@LinZhihao-723 LinZhihao-723 left a comment

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.

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

  • PathsToCompressBufferCompressionInputBuilderWhy: 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.
  • RoundRobinPartitionRoundRobinIteratorWhy: 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 only Iterator impl.
  • Methods that lied about their effectpartition_and_compresspartition, submit_partition_for_compressionpush_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_sourcesinto_task_input_sourcesWhy: Rust convention. A conversion that consumes self is into_*, and the get_ prefix is discouraged.
  • Fields aligned with what they holdtasks_input_sourcespartitioned_task_inputs, total_file_sizetotal_buffered_size, file_size_to_trigger_compressionbuffer_size_to_trigger_partition, filesbuffer. 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_fileadd, into_remaining_filesinto_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 of ObjectMetadata. Why: storing full ObjectMetadata per object was wasteful — the bucket is identical for every entry (now enforced by add), and the raw size has no consumer once the estimate exists. This restores the shape of Python's own FileMetadata.
  • The estimate is computed once, in the constructor. Why: it was previously recomputed on every partitioning pass, and computing it in FileMetadata::new means no call site can forget it.

Control flow

  • has_more_files flag replaced by a labeled break 'partitioning. Why: the flag existed only to communicate loop exit between two nested loops, which a label states directly.
  • total_buffered_size decremented 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 to buffer — 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_filenames rewritten as a single fold with the predicate in a match guard. Why: eliminates one String allocation per file that existed only to dodge a borrow conflict, removes the empty-input special case, and inverts the predicate so >= threshold reads as "same group" instead of < threshold meaning "start a new one".

API surface and behavior

  • add validates the object's bucket and returns Result<(), Error>, with a new Error::S3BucketMismatch variant. Why: an object from a different bucket would otherwise be silently emitted inside an S3InputSource carrying 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_sources flushes internally; flush is now private. Why: previously flush took &mut self while the getter consumed self, 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 expose flush at all — its only remaining use was the footgun of forcing out an undersized partition mid-stream. The public surface is now exactly new / 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 RoundRobinIterator rotation. 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.

@LinZhihao-723

Copy link
Copy Markdown
Member

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

  • PathsToCompressBufferCompressionInputBuilderWhy: 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.
  • RoundRobinPartitionRoundRobinIteratorWhy: 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 only Iterator impl.
  • Methods that lied about their effectpartition_and_compresspartition, submit_partition_for_compressionpush_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_sourcesinto_task_input_sourcesWhy: Rust convention. A conversion that consumes self is into_*, and the get_ prefix is discouraged.
  • Fields aligned with what they holdtasks_input_sourcespartitioned_task_inputs, total_file_sizetotal_buffered_size, file_size_to_trigger_compressionbuffer_size_to_trigger_partition, filesbuffer. 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_fileadd, into_remaining_filesinto_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 of ObjectMetadata. Why: storing full ObjectMetadata per object was wasteful — the bucket is identical for every entry (now enforced by add), and the raw size has no consumer once the estimate exists. This restores the shape of Python's own FileMetadata.
  • The estimate is computed once, in the constructor. Why: it was previously recomputed on every partitioning pass, and computing it in FileMetadata::new means no call site can forget it.

Control flow

  • has_more_files flag replaced by a labeled break 'partitioning. Why: the flag existed only to communicate loop exit between two nested loops, which a label states directly.
  • total_buffered_size decremented 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 to buffer — 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_filenames rewritten as a single fold with the predicate in a match guard. Why: eliminates one String allocation per file that existed only to dodge a borrow conflict, removes the empty-input special case, and inverts the predicate so >= threshold reads as "same group" instead of < threshold meaning "start a new one".

API surface and behavior

  • add validates the object's bucket and returns Result<(), Error>, with a new Error::S3BucketMismatch variant. Why: an object from a different bucket would otherwise be silently emitted inside an S3InputSource carrying 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_sources flushes internally; flush is now private. Why: previously flush took &mut self while the getter consumed self, 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 expose flush at all — its only remaining use was the footgun of forcing out an undersized partition mid-stream. The public surface is now exactly new / 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 RoundRobinIterator rotation. 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.

@LinZhihao-723
LinZhihao-723 marked this pull request as ready for review July 20, 2026 22:11
@LinZhihao-723
LinZhihao-723 requested a review from a team as a code owner July 20, 2026 22:11
@LinZhihao-723 LinZhihao-723 changed the title feat(compression-coordinator): Add pre-submission file-buffer partitioning. feat(compression-coordinator): Add CompressionInputBuilder for partitioning S3 objects into compression-task inputs. Jul 20, 2026

@LinZhihao-723 LinZhihao-723 left a comment

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.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between fbe476e and 3fe7229.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • components/compression-coordinator/Cargo.toml
  • components/compression-coordinator/src/error.rs
  • components/compression-coordinator/src/lib.rs
  • components/compression-coordinator/src/partition.rs

Comment on lines +115 to +144
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();
}

@coderabbitai coderabbitai Bot Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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 -C3

Repository: 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)
PY

Repository: 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 || true

Repository: 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai open an issue to use NonZero rust type for this./

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 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: #2412Use a non-zero Rust type for compression target archive size.

LinZhihao-723 and others added 5 commits July 21, 2026 12:53
Co-authored-by: Bingran Hu <bingran.hu@yscope.com>
Co-authored-by: Bingran Hu <bingran.hu@yscope.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe7229 and 33233e6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • components/compression-coordinator/Cargo.toml
  • components/compression-coordinator/src/compression_job_submitter/spider.rs
  • components/compression-coordinator/src/error.rs

Comment thread components/compression-coordinator/Cargo.toml Outdated
Comment thread components/compression-coordinator/src/error.rs Outdated
@Bill-hbrhbr
Bill-hbrhbr merged commit d1248be into y-scope:main Jul 22, 2026
28 checks passed
@Bill-hbrhbr
Bill-hbrhbr deleted the compression-coordinator-partition branch July 22, 2026 01:47
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