Skip to content

feat(sort): add --max-temp-files to tune the spill-file consolidation limit - #643

Merged
nh13 merged 2 commits into
mainfrom
tf_expose_sort_temp_limit
Jul 23, 2026
Merged

nh13 merged 2 commits into
mainfrom
tf_expose_sort_temp_limit

Conversation

@tfenne

@tfenne tfenne commented Jul 22, 2026 •

Copy link
Copy Markdown
Member

What

Adds --max-temp-files <N> to fgumi sort, exposing the previously-hardcoded temp-file consolidation limit (DEFAULT_MAX_TEMP_FILES = 64).

  • Optional usize, minimum 2 (values < 2 are rejected — a merge needs at least two inputs). For effectively unlimited, pass a large value.
  • Wired through build_sorter to the existing RawExternalSorter::max_temp_files builder, following the same Option-override pattern as --sort-threads / --merge-threads.
  • Unset → the engine default (64, samtools-compatible) is used, so behavior is byte-identical unless you opt in.

Why

The external sort spills sorted runs to disk; once the number of runs reaches the limit, the oldest ~half are consolidated into a single run so the final k-way merge opens fewer files. That consolidation merge is single-threaded, so on large inputs it adds real wall-time. Sorting a 1.29 B-read WGS BAM with defaults (≈6 GiB budget → ~93 spilled runs) triggered one or two consolidation folds costing roughly 15–38% of total wall-clock — overhead a higher limit eliminates. Concurrency during the final merge is bounded by --threads, not the file count, so raising the limit mainly trades open file descriptors for fewer consolidation passes.

Notes for embedders

The flag declares no clap default_value and its help text states no number, so a tool that embeds Sort via #[command(flatten)] can set its own default (resolving None in code) without the flattened help advertising a wrong value. fgumi's own default (64) is documented only in fgumi's command long_about, which flatten does not propagate.

Testing

cargo ci-fmt, cargo ci-lint (clippy pedantic), and cargo ci-test all pass (5738 tests). New tests cover parsing (unset / explicit), rejection of values < 2, and the build_sorter wiring (asserted against a freshly-constructed sorter's default rather than a hardcoded 64).

Summary by CodeRabbit

  • New Features
    • Added a --max-temp-files <usize> option to control when temporary spilled runs are consolidated.
    • Added parse-time validation requiring values ≥ 2; invalid inputs are rejected.
    • Updated command help and examples, including the default of 64 and the effect of raising/lowering the limit.
  • Documentation
    • Updated the performance-tuning guide to describe --max-temp-files, its trade-offs, and its constraints.
  • Tests
    • Added unit tests covering unset/minimum/explicit values, rejection of invalid inputs, and that the effective limit is applied as expected without changing output correctness.

@tfenne
tfenne requested a review from nh13 as a code owner July 22, 2026 19:59
@tfenne
tfenne temporarily deployed to github-actions July 22, 2026 19:59 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nh13, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8ce94bfb-ef12-44de-8e79-e55cf3611a03

📥 Commits

Reviewing files that changed from the base of the PR and between e26b763 and da65744.

📒 Files selected for processing (4)
  • crates/fgumi-sort/src/external.rs
  • docs/LAST_SYNCED
  • docs/src/guide/performance-tuning.md
  • src/lib/commands/sort.rs

Walkthrough

The sort command adds --max-temp-files, validates values of at least 2, and conditionally configures RawExternalSorter. Documentation, accessor coverage, and tests cover explicit, default, invalid, and output-identity behavior.

Changes

Temporary file limit

Layer / File(s) Summary
CLI option and documentation
src/lib/commands/sort.rs, docs/src/guide/performance-tuning.md, docs/LAST_SYNCED
The sort command adds and documents --max-temp-files, including validation, default behavior, consolidation semantics, and an example.
Sorter configuration and coverage
src/lib/commands/sort.rs, crates/fgumi-sort/src/external.rs
The optional limit is wired into RawExternalSorter, exposed through temp_file_limit, and tested for parsing, defaults, invalid values, and unchanged output bytes and record counts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SortCLI
  participant build_sorter
  participant RawExternalSorter
  SortCLI->>build_sorter: parsed max_temp_files
  build_sorter->>RawExternalSorter: apply max_temp_files(n)
  RawExternalSorter-->>build_sorter: temp_file_limit()
Loading

Suggested reviewers: nh13

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding --max-temp-files to control spill-file consolidation.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tf_expose_sort_temp_limit

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.

@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 `@src/lib/commands/sort.rs`:
- Around line 157-159: Update the help text for the --max-temp-files threshold
in the sort command to say consolidation occurs when spilled runs reach the
limit, replacing “exceed” with wording that matches the engine’s len >=
max_temp_files behavior.
- Around line 875-905: Expand the max_temp_files parser tests in
test_parse_max_temp_files and test_parse_max_temp_files_rejects_below_two to
cover the valid minimum value 2, negative input, and non-numeric input. Assert
that 2 parses successfully and the negative/non-numeric values return a parse
error while preserving the existing below-two cases.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a667f651-5010-40fd-887e-5675f6ef4313

📥 Commits

Reviewing files that changed from the base of the PR and between acc21d4 and fb814f2.

📒 Files selected for processing (2)
  • crates/fgumi-sort/src/external.rs
  • src/lib/commands/sort.rs

Comment thread src/lib/commands/sort.rs Outdated
Comment thread src/lib/commands/sort.rs

@nh13 nh13 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.

Minor suggestions, when you've addressed them, feel free to squash-merge

Comment thread src/lib/commands/sort.rs Outdated
Comment thread src/lib/commands/sort.rs Outdated
Comment thread src/lib/commands/sort.rs
Comment thread src/lib/commands/sort.rs Outdated
Comment thread src/lib/commands/sort.rs Outdated
@codecov

codecov Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.54%. Comparing base (9113f4e) to head (da65744).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #643   +/-   ##
=======================================
  Coverage   93.53%   93.54%           
=======================================
  Files         175      175           
  Lines      105970   106014   +44     
=======================================
+ Hits        99123    99167   +44     
  Misses       6847     6847           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tfenne added a commit that referenced this pull request Jul 23, 2026
…-rolled parser

Addresses review feedback on #643.

Replace the custom `parse_max_temp_files` with `clap::builder::RangedU64ValueParser::<usize>`, which parses as u64, enforces the `>= 2` floor, and converts back to usize via `TryFrom`. This keeps the field `Option<usize>` (so tools that flatten `Sort` are unaffected) while letting clap own parsing, range-checking, and overflow. clap provides no ranged value parser for `usize` itself, which is why the parser was hand-rolled in the first place; going through `u64` is the idiomatic way to range-check a `usize`.

The rejection messages are now accurate: an overflowing value reports "number too large to fit in target type" rather than the previous, misleading "not a non-negative integer", and a value below the floor reports "N is not in 2..".

Also correct the command-overview wording (runs are consolidated once they "reach" `--max-temp-files`, not "exceed", matching the engine's `len >= max_temp_files` guard), document the flag in the performance-tuning guide's Sort section, and bump docs/LAST_SYNCED.

Expand the parser tests to cover the minimum accepted value (2) and the rejected cases (0, 1, negative, non-numeric, overflow).
@tfenne
tfenne temporarily deployed to github-actions July 23, 2026 02:39 — with GitHub Actions Inactive

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/commands/sort.rs (1)

900-914: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Exercise consolidation, not just the accessor.

The new test proves that Some(n) reaches temp_file_limit(), but never creates multiple spill runs or verifies output identity. Add an end-to-end case that forces consolidation and compares default output with a small explicit limit such as Some(2); otherwise stable-order or byte-output regressions can pass unnoticed.

🤖 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 `@src/lib/commands/sort.rs` around lines 900 - 914, Add an end-to-end test
alongside test_build_sorter_wires_max_temp_files that uses input large enough to
create multiple spill runs, runs sorting once with the default max_temp_files
and once with Some(2), then compares the resulting output bytes or records for
exact identity. Ensure the test exercises consolidation rather than only
inspecting temp_file_limit(), while preserving the existing accessor test.
🤖 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.

Outside diff comments:
In `@src/lib/commands/sort.rs`:
- Around line 900-914: Add an end-to-end test alongside
test_build_sorter_wires_max_temp_files that uses input large enough to create
multiple spill runs, runs sorting once with the default max_temp_files and once
with Some(2), then compares the resulting output bytes or records for exact
identity. Ensure the test exercises consolidation rather than only inspecting
temp_file_limit(), while preserving the existing accessor test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 62c93e9e-3a2b-4ddd-812d-50b6b3951f0c

📥 Commits

Reviewing files that changed from the base of the PR and between fb814f2 and 39458d5.

📒 Files selected for processing (3)
  • docs/LAST_SYNCED
  • docs/src/guide/performance-tuning.md
  • src/lib/commands/sort.rs

nh13 pushed a commit that referenced this pull request Jul 23, 2026
…-rolled parser

Addresses review feedback on #643.

Replace the custom `parse_max_temp_files` with `clap::builder::RangedU64ValueParser::<usize>`, which parses as u64, enforces the `>= 2` floor, and converts back to usize via `TryFrom`. This keeps the field `Option<usize>` (so tools that flatten `Sort` are unaffected) while letting clap own parsing, range-checking, and overflow. clap provides no ranged value parser for `usize` itself, which is why the parser was hand-rolled in the first place; going through `u64` is the idiomatic way to range-check a `usize`.

The rejection messages are now accurate: an overflowing value reports "number too large to fit in target type" rather than the previous, misleading "not a non-negative integer", and a value below the floor reports "N is not in 2..".

Also correct the command-overview wording (runs are consolidated once they "reach" `--max-temp-files`, not "exceed", matching the engine's `len >= max_temp_files` guard), document the flag in the performance-tuning guide's Sort section, and bump docs/LAST_SYNCED.

Expand the parser tests to cover the minimum accepted value (2) and the rejected cases (0, 1, negative, non-numeric, overflow).
@nh13
nh13 force-pushed the tf_expose_sort_temp_limit branch from 39458d5 to e26b763 Compare July 23, 2026 05:31
@nh13
nh13 temporarily deployed to github-actions July 23, 2026 05:31 — with GitHub Actions Inactive

@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: 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 `@src/lib/commands/sort.rs`:
- Around line 337-352: Bound the max_temp_files argument in the clap value
parser so accepted values cannot exceed a safe process/system file-descriptor
limit, rather than allowing arbitrary values through range(2..). Preserve the
existing minimum of 2 and update the related parsing tests, including the
100_000 case, to verify oversized values are rejected or safely clamped
according to the established configuration behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ff44937-9ebf-409e-8315-4cd921182150

📥 Commits

Reviewing files that changed from the base of the PR and between 39458d5 and e26b763.

📒 Files selected for processing (4)
  • crates/fgumi-sort/src/external.rs
  • docs/LAST_SYNCED
  • docs/src/guide/performance-tuning.md
  • src/lib/commands/sort.rs

Comment thread src/lib/commands/sort.rs
Comment on lines +337 to +352
/// Maximum number of temporary spill files kept before the oldest are
/// consolidated into a single run.
///
/// Large inputs spill many sorted runs to disk. When the number of runs
/// reaches this limit, the oldest are merged together in a single pass so
/// the final k-way merge opens fewer files at once. Raising it avoids
/// repeated consolidation passes on very large inputs (at the cost of more
/// open file descriptors during the final merge); lowering it keeps fewer
/// files open. Must be at least 2; to effectively disable consolidation,
/// pass a value larger than the number of runs you expect to spill.
///
/// When unset, a built-in default is used (see this command's help
/// overview for the default value).
#[arg(long = "max-temp-files", value_parser = clap::builder::RangedU64ValueParser::<usize>::new().range(2..))]
pub max_temp_files: Option<usize>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No upper bound on --max-temp-files risks fd exhaustion during merge.

Values are only floored at 2 (range(2..)); nothing caps against the process/system open-file limit. A moderately large value (e.g. the 100_000 case already exercised in parsing tests) that doesn't happen to disable consolidation entirely could make the final k-way merge attempt to open one reader per surviving chunk file simultaneously, which can exceed typical ulimit -n (1024–4096) and fail mid-merge. This mirrors an unresolved suggestion from a prior review round ("should we bound the maximum number of temp files? Or clamp it to the system maximum?").

🤖 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 `@src/lib/commands/sort.rs` around lines 337 - 352, Bound the max_temp_files
argument in the clap value parser so accepted values cannot exceed a safe
process/system file-descriptor limit, rather than allowing arbitrary values
through range(2..). Preserve the existing minimum of 2 and update the related
parsing tests, including the 100_000 case, to verify oversized values are
rejected or safely clamped according to the established configuration behavior.

@nh13

nh13 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Sorry for the churn on this one — I just pushed a fix for the outstanding CodeRabbit comment (added an end-to-end test that forces spill-file consolidation and asserts the output is byte-identical between the default limit and --max-temp-files 2, rather than only checking the accessor). Should be good to merge once CI passes. Thanks for your patience!

tfenne added 2 commits July 22, 2026 22:40
… limit

The external sort consolidates spilled runs once their count reaches a hardcoded limit (64, matching samtools) by merging the oldest ~half into a single run. On large inputs that spill many runs this fires repeated consolidation passes whose merge loop is single-threaded, adding significant wall-time even though the final k-way merge is unaffected.

Expose the limit as `--max-temp-files <N>` on `fgumi sort`, wired through `build_sorter` to the existing `RawExternalSorter::max_temp_files` builder. The flag is optional with a minimum of 2; when unset the engine default (64) applies, so behavior is byte-identical unless the user opts in. Raising it lets large sorts skip the extra consolidation passes, at the cost of more open file descriptors during the final merge.

The flag deliberately declares no clap default and states no number in its help text, so tools that embed this `Sort` via `#[command(flatten)]` can apply their own default without inheriting a wrong one. Adds a `temp_file_limit()` accessor so the option-to-builder wiring can be unit tested.
…-rolled parser

Addresses review feedback on #643.

Replace the custom `parse_max_temp_files` with `clap::builder::RangedU64ValueParser::<usize>`, which parses as u64, enforces the `>= 2` floor, and converts back to usize via `TryFrom`. This keeps the field `Option<usize>` (so tools that flatten `Sort` are unaffected) while letting clap own parsing, range-checking, and overflow. clap provides no ranged value parser for `usize` itself, which is why the parser was hand-rolled in the first place; going through `u64` is the idiomatic way to range-check a `usize`.

The rejection messages are now accurate: an overflowing value reports "number too large to fit in target type" rather than the previous, misleading "not a non-negative integer", and a value below the floor reports "N is not in 2..".

Also correct the command-overview wording (runs are consolidated once they "reach" `--max-temp-files`, not "exceed", matching the engine's `len >= max_temp_files` guard), document the flag in the performance-tuning guide's Sort section, and bump docs/LAST_SYNCED.

Expand the parser tests to cover the minimum accepted value (2) and the rejected cases (0, 1, negative, non-numeric, overflow).
@nh13
nh13 force-pushed the tf_expose_sort_temp_limit branch from e26b763 to da65744 Compare July 23, 2026 05:40
@nh13
nh13 temporarily deployed to github-actions July 23, 2026 05:40 — with GitHub Actions Inactive
@nh13
nh13 merged commit 458ed07 into main Jul 23, 2026
14 checks passed
@nh13
nh13 deleted the tf_expose_sort_temp_limit branch July 23, 2026 05:48

This branch was previously deployed

1 inactive deployment
github-actions — da657449 Deployed Jul 23, 2026 by nh13 via coverage #2937
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