Skip to content

feat(parquet): add sealed-partition compaction module (RFC 0009) - #97

Merged
jensholdgaard merged 3 commits into
mainfrom
feat/compaction-module
Jun 3, 2026
Merged

feat(parquet): add sealed-partition compaction module (RFC 0009)#97
jensholdgaard merged 3 commits into
mainfrom
feat/compaction-module

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 3, 2026

Copy link
Copy Markdown
Owner

What

Next compaction slice (epic #94), building on the reader-side manifest support (#96): the compactor that consolidates a partition's small files and commits via the manifest. RFC 0009 §3.4/§3.5.

Changes

  • Manifest::write_atomic — serialize to manifest.json.tmp, then rename over manifest.json. The rename is the commit point (RFC 0009 §3.4): a reader sees the old or new manifest, never a partial write. Entries are validated before any bytes hit disk.
  • new compaction modulecompact_partition(bucket_root, &PartitionKey):
    1. resolves the partition's live files (manifest, else glob),
    2. reads their rows via Reader::open_partition (which validates every row's tenant + time bucket against the partition — §3.9 / RFC0009.5),
    3. rewrites them as one file via Writer (rows copied, never re-mined — RFC0009.2),
    4. commits the manifest to name only the consolidated file,
    5. GCs the superseded inputs.
      A no-op for fewer than two live files.

Correctness (RFC0009.2 / .3 / .5)

  • No torn read on first compaction. On a partition with no prior manifest, a concurrent glob reader would briefly see inputs and the new file. So the compactor first bootstraps a manifest naming the current inputs — the same set the glob already returns, so nothing visible changes — making the reader manifest-authoritative before the consolidated file appears. From then on the new file stays invisible until the commit names it.
  • Crash safety. Any failure before the commit leaves the inputs untouched (the partition reads exactly as before); a crash after the commit leaves only harmless orphans the manifest already excludes (a later GC sweep reclaims them).
  • Isolation. Operates within a single partition; open_partition aborts on a mis-partitioned input rather than merging it.

Tests (colocated)

  • compacts_two_files_into_one_preserving_rows — 2 files (5 rows) → 1 live file, all rows intact, inputs GC'd.
  • single_file_partition_is_a_no_op.
  • bumps_generation_from_an_existing_manifest (5 → 6).
  • Manifest::write_atomic round-trip / overwrite / invalid-entry rejection.
  • Existing querier suite still green (it reads through the manifest these now write).

Not in scope (later epic #94 slices)

The background scheduler (sealed-partition selection, cadence/grace — RFC 0004), the standalone orphan-GC sweep, audit events + telemetry, and the crash-recovery/proptest + D2/D3 bench.

Verification (local)

  • cargo fmt --all --check
  • cargo clippy -p ourios-parquet -p ourios-querier --all-targets --all-features -- -D warnings
  • cargo test -p ourios-parquet -p ourios-querier --all-features

Epic: #94.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Partition compaction consolidates multiple data files into one with cleanup of superseded files.
    • Atomic manifest publishing to ensure consistent manifest updates and generation bumping.
    • Validation now rejects duplicate filenames; non-UTF8 filenames are handled gracefully.
    • Compaction reports garbage-collection failures without aborting a successful commit.
  • Behavior

    • Single-file partitions are no-ops (no unnecessary compaction).
  • Tests

    • Unit tests for merging, no-op single-file behavior, generation bumping, duplicate-file rejection, and atomic manifest writes.

Next compaction slice (epic #94), building on the reader-side
manifest support (#96): the compactor that consolidates a
partition's small files and commits via the manifest.

- `Manifest::write_atomic` — serialize to `manifest.json.tmp`, then
  rename over `manifest.json`. The rename is the commit point
  (RFC 0009 §3.4); a reader sees the old or new manifest, never a
  partial write. Validates entries before any bytes hit disk.
- new `compaction` module — `compact_partition(bucket_root,
  &PartitionKey)` reads a partition's live files (manifest or glob),
  rewrites their rows as one file via Reader/Writer (rows copied,
  never re-mined — RFC0009.2), commits the manifest to name only the
  consolidated file, then GCs the superseded inputs. `Reader::
  open_partition` validates every row belongs to the partition
  (§3.9 / RFC0009.5). A no-op for <2 live files.

Correctness: on a partition with no prior manifest, the compactor
first bootstraps a manifest naming the current inputs (the same set
the glob returns — no visible change), making the reader
manifest-authoritative before the consolidated file appears, so a
concurrent query never sees inputs + new together (RFC0009.3, no
torn read). Any failure before the commit leaves the inputs
untouched; a crash after the commit leaves only harmless orphans the
manifest already excludes.

Colocated tests: row-preserving consolidation (2 files → 1, all rows
intact), single-file no-op, generation bump from an existing
manifest, plus manifest `write_atomic` round-trip / invalid-entry
rejection. The background scheduler (sealed-partition selection,
cadence) and orphan-GC sweep are later slices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard requested a review from Copilot June 3, 2026 06:05
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 150e079f-ed68-4693-8cca-bace187a0757

📥 Commits

Reviewing files that changed from the base of the PR and between d09ca75 and a372c85.

📒 Files selected for processing (1)
  • crates/ourios-parquet/src/manifest.rs

📝 Walkthrough

Walkthrough

Adds atomic manifest publishing and sealed-partition compaction: compact_partition merges manifest-aware live Parquet inputs into one output, atomically updates the partition manifest with a bumped generation, and garbage-collects superseded inputs.

Changes

Partition Compaction and Manifest Safety

Layer / File(s) Summary
Manifest atomic write and validation
crates/ourios-parquet/src/manifest.rs
Manifest::write_atomic validates filenames, serializes canonical JSON, writes manifest.json.tmp, and atomically renames it to manifest.json. Tests verify overwrite/read-after-write, rejection of invalid filenames, and duplicate filename validation.
Partition compaction core and helpers
crates/ourios-parquet/src/compaction.rs
compact_partition enumerates live files (manifest-aware), no-ops for <2 inputs, streams rows from inputs into a single consolidated Parquet file, bumps or bootstraps manifest generation, atomically publishes the manifest, and attempts to delete superseded inputs while counting non-fatal GC failures. Includes helpers live_files and file_names and tests for merge correctness, no-op semantics, and generation bumping.
Crate-level module exposure
crates/ourios-parquet/src/lib.rs
Adds pub mod compaction; and re-exports compact_partition, CompactionOutcome, Committed, and CompactionError at the crate root.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • jensholdgaard/ourios#96: Related manifest model/read/validation changes that pair with this PR's write_atomic and compaction logic.
  • jensholdgaard/ourios#45: Reader API used by compaction (Reader::open_partition + read_all) was added/modified in this PR and is directly coupled.

Poem

🐰 I hop through manifests, stitching files as one,

Rows join paws beneath a compacting sun.
Generations bump with an atomic cheer,
Old crumbs cleared gently — the burrow is clear.
A tidy patch, compact and dear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a sealed-partition compaction module, and includes RFC reference (0009) for context.
Description check ✅ Passed The description comprehensively covers the PR's scope, implementation details, correctness guarantees, tests, and verification steps, with linked RFC and epic references.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/compaction-module

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@crates/ourios-parquet/src/compaction.rs`:
- Around line 148-153: The current compaction code collects every row into a
single Vec (the local variable rows) by calling
Reader::open_partition(...).read_all(), which can OOM; instead stream each input
file into the Writer: for each file in inputs call Reader::open_partition(file,
partition.clone()), iterate over its records (do not call read_all or extend
into rows), call Writer::write_row (or the appropriate write method) for each
record and increment the running row count; after finishing a file continue with
the next—apply the same change for the similar block that currently
reads/extends rows (the 158-163 section) so no full-partition buffering occurs.
- Around line 180-194: The loop that GC's superseded input files after commit
currently returns Err on any remove_file failure (in compaction.rs where
std::fs::remove_file(file) is matched), causing the whole compaction to be
reported as failed; instead, swallow non-critical delete errors: do not return
CompactionError::Io after commit—log or warn about the failing path (use the
existing logging/tracing facility) and continue to the next file so cleanup
failures do not mark a committed compaction as failed. Locate the for file in
&inputs loop and replace the Err(source) branch that returns Err with a
non-fatal log (including op="remove superseded input", the path and source) and
continue.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d356c389-f0a3-4124-8036-8384053b6c32

📥 Commits

Reviewing files that changed from the base of the PR and between cb0810a and e731cb6.

📒 Files selected for processing (3)
  • crates/ourios-parquet/src/compaction.rs
  • crates/ourios-parquet/src/lib.rs
  • crates/ourios-parquet/src/manifest.rs

Comment thread crates/ourios-parquet/src/compaction.rs Outdated
Comment thread crates/ourios-parquet/src/compaction.rs Outdated

Copilot AI 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.

Pull request overview

Adds the writer-side half of RFC 0009’s sealed-partition compaction flow in ourios-parquet: an atomic manifest swap as the commit point, plus a compactor that rewrites a partition’s live Parquet inputs into a single consolidated file and then GC’s superseded inputs.

Changes:

  • Add Manifest::write_atomic to publish manifest.json via a manifest.json.tmp + rename swap, plus unit tests.
  • Introduce compaction module with compact_partition(bucket_root, &PartitionKey) implementing bootstrap-manifest → read → rewrite → commit → GC, plus colocated tests.
  • Export the new module and public compaction API types from ourios-parquet’s lib.rs.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
crates/ourios-parquet/src/manifest.rs Adds atomic manifest rewrite (write_atomic) and tests verifying overwrite + invalid-entry rejection.
crates/ourios-parquet/src/lib.rs Wires the new compaction module into the crate’s public API (module + re-exports).
crates/ourios-parquet/src/compaction.rs Implements sealed-partition compaction and tests (two→one compaction, no-op behavior, generation bump).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ourios-parquet/src/manifest.rs Outdated
Comment thread crates/ourios-parquet/src/compaction.rs Outdated
Comment thread crates/ourios-parquet/src/compaction.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/ourios-parquet/src/compaction.rs (1)

133-133: 💤 Low value

Consider caching the manifest read to avoid a second I/O round-trip.

live_files() already reads the manifest at line 232, but the result (and generation) is discarded. Then Manifest::read() is called again at line 152 solely to retrieve the generation. Under the single-writer assumption this is correct, but it's wasteful I/O.

A possible refactor: have live_files return (Vec<PathBuf>, Option<Manifest>) or introduce a helper struct, so the generation is available without a second read.

Also applies to: 151-154

🤖 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 `@crates/ourios-parquet/src/compaction.rs` at line 133, The code is doing
duplicate manifest I/O: live_files(&partition_dir) already reads the manifest
but its Manifest is discarded and Manifest::read(...) is called again to get
generation; refactor live_files to return both the list of live file paths and
the optional Manifest (e.g., -> (Vec<PathBuf>, Option<Manifest>)) or a small
helper struct, then update callers (the site assigning let inputs =
live_files(&partition_dir)? and the code that currently calls
Manifest::read(...) for generation) to use the manifest returned from live_files
to obtain the generation instead of performing a second Manifest::read; ensure
all call sites handle None/manifests consistently and remove the redundant
Manifest::read invocation.
🤖 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.

Nitpick comments:
In `@crates/ourios-parquet/src/compaction.rs`:
- Line 133: The code is doing duplicate manifest I/O: live_files(&partition_dir)
already reads the manifest but its Manifest is discarded and Manifest::read(...)
is called again to get generation; refactor live_files to return both the list
of live file paths and the optional Manifest (e.g., -> (Vec<PathBuf>,
Option<Manifest>)) or a small helper struct, then update callers (the site
assigning let inputs = live_files(&partition_dir)? and the code that currently
calls Manifest::read(...) for generation) to use the manifest returned from
live_files to obtain the generation instead of performing a second
Manifest::read; ensure all call sites handle None/manifests consistently and
remove the redundant Manifest::read invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44d63700-aafa-4779-8df5-560a1ac26115

📥 Commits

Reviewing files that changed from the base of the PR and between e731cb6 and d09ca75.

📒 Files selected for processing (2)
  • crates/ourios-parquet/src/compaction.rs
  • crates/ourios-parquet/src/manifest.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/ourios-parquet/src/manifest.rs

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread crates/ourios-parquet/src/manifest.rs
@jensholdgaard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

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