Repository navigation
refactor(consensus,correct): migrate hand-rolled rejects writers to first-class secondary_output - #332
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (11)
📝 WalkthroughWalkthroughConsensus commands (correct, duplex, codec, simplex) now buffer rejected raw BAM records per processed batch (Vec<Vec>) and emit them via the unified pipeline’s secondary output instead of mutex‑protected streaming writers. Added serialize_raw_bam_records to frame records for secondary output. Pipeline runner accepts an optional secondary_output_header (defaults to resolved primary header). Heap-size estimates and tests were updated to account for buffered rejects. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #332 +/- ##
==========================================
+ Coverage 90.79% 90.87% +0.08%
==========================================
Files 78 78
Lines 50801 50775 -26
==========================================
+ Hits 46124 46143 +19
+ Misses 4677 4632 -45 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/lib/unified_pipeline/bam.rs (1)
4232-4331:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep secondary serialization ordered the same way in single-threaded mode.
The parallel path mutates the batch before
secondary_serialize_fnruns, but the single-threaded path still serializes rejects first. That makes rejects depend on--threadswhenevermi_assign_fnand secondary output are both enabled.Fix
@@ - // Step 7a: Secondary serialize (borrows processed) - buffers.secondary.clear(); - if let Some(ref secondary_fn) = fns.secondary_serialize_fn { - (secondary_fn)(&processed, &mut buffers.secondary)?; - } - - // Step 7b: Primary serialize (consumes processed, reuse buffer). + // Step 7a: Primary serialize (consumes processed, reuse buffer). // Run the optional MI Assign hook first so any per-template - // rewriting it does is reflected in the bytes serialize - // emits. + // rewriting it does is reflected in both outputs. buffers.serialized.clear(); let processed = run_mi_assign_single_threaded( fns.mi_assign_fn.as_deref(), &mut next_serial, processed, )?; + buffers.secondary.clear(); + if let Some(ref secondary_fn) = fns.secondary_serialize_fn { + (secondary_fn)(&processed, &mut buffers.secondary)?; + } @@ - buffers.secondary.clear(); - if let Some(ref secondary_fn) = fns.secondary_serialize_fn { - (secondary_fn)(&processed, &mut buffers.secondary)?; - } - - buffers.serialized.clear(); + buffers.serialized.clear(); let processed = run_mi_assign_single_threaded( fns.mi_assign_fn.as_deref(), &mut next_serial, processed, )?; + buffers.secondary.clear(); + if let Some(ref secondary_fn) = fns.secondary_serialize_fn { + (secondary_fn)(&processed, &mut buffers.secondary)?; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/unified_pipeline/bam.rs` around lines 4232 - 4331, In run_bam_pipeline_from_reader_with_secondary ensure the single-threaded branch mirrors the parallel ordering: call secondary_serialize_fn(&processed_batch, &mut buf) using a borrow of the processed result before calling serialize_fn(processed_batch, &header, &mut buf) so the secondary serialization sees the same pre-consumption state as in the parallel path; locate the single-threaded serialization block that currently writes rejects first/second and swap or reorder the calls so secondary_serialize_fn runs with a &P borrow immediately before the primary serialize_fn consumes the P produced by process_fn.tests/integration/test_duplex_command.rs (1)
195-202:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate stale test docs about rejects header semantics.
The comment still describes old mutex-era
SO:unsortedbehavior, which no longer matches the asserted behavior.Suggested wording update
/// 1. The rejects BAM is created and has its `@HD` header rewritten to -/// `SO:unsorted` because reject writes are emitted in mutex-acquisition -/// order rather than input order. +/// match the input BAM sort fields (`SO`/`GO`/`SS`) because rejects are +/// emitted in batch/input order through unified secondary output.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration/test_duplex_command.rs` around lines 195 - 202, The test docblock in tests/integration/test_duplex_command.rs still claims the rejects BAM header is rewritten to `SO:unsorted` due to mutex-acquisition ordering; update that sentence to match the current asserted behavior (remove the `SO:unsorted` claim and the "mutex-acquisition order" rationale) so the comment describes what the test actually asserts about the rejects BAM `@HD` header now (e.g., that it preserves the expected sort-order/header semantics asserted in the test). Edit the comment block above the duplex pipeline test accordingly and remove any reference to the old mutex-era semantics.
🧹 Nitpick comments (4)
tests/integration/test_simplex_command.rs (1)
326-346: ⚡ Quick winAssert at least one rejected record to avoid a header-only false positive.
This test can pass even if no read is actually rejected. Add a rejects-record assertion so it truly exercises the
--min-readsrejection path.Based on learnings: In fgumi consensus-calling tests, treat a paired-end read pair as one template and verify `--min-reads` behavior against template-level rejection.Suggested patch
let rejects_header = bam::io::Reader::new(fs::File::open(&rejects_bam).unwrap()).read_header().unwrap(); let mut rejects_rgs: Vec<BString> = rejects_header.read_groups().keys().cloned().collect(); rejects_rgs.sort(); assert_eq!( rejects_rgs, vec![BString::from("RG1"), BString::from("RG2")], "rejects BAM should inherit the input header's read groups verbatim", ); + + let mut rejects_reader = bam::io::Reader::new(fs::File::open(&rejects_bam).unwrap()); + let _ = rejects_reader.read_header().unwrap(); + let rejected_count = rejects_reader + .records() + .map(|r| r.expect("Failed to read rejects record")) + .count(); + assert!(rejected_count > 0, "Expected at least one rejected record"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration/test_simplex_command.rs` around lines 326 - 346, The test currently only checks headers and can falsely pass if no reads were rejected; update the test to assert there is at least one rejected template in rejects_bam by opening rejects_bam with bam::io::Reader, iterating its records to count unique template names (QNAME) so paired-end reads count as one template, and add an assert that this unique-template count is >= 1 before the header assertions (use the existing rejects_bam variable and then proceed to rejects_header/rejects_rgs checks).src/lib/commands/duplex.rs (1)
1702-1717: ⚡ Quick winAdd a non-empty rejects case to memory-estimate test.
The test currently validates only consensus buffer capacity, not the new reject-buffer accounting branch.
Proposed test extension
#[test] fn test_duplex_processed_batch_memory_estimate() { @@ let batch = DuplexProcessedBatch { consensus_output: ConsensusOutput { data, count: 1 }, rejected_records: Vec::new(), groups_count: 1, stats: ConsensusCallingStats::default(), overlapping_stats: None, }; @@ let estimate = batch.estimate_heap_size(); assert_eq!(estimate, 1024, "estimate should match consensus_output capacity"); + + let mut rejected = Vec::with_capacity(192); + rejected.extend_from_slice(&[1u8; 8]); + let batch_with_rejects = DuplexProcessedBatch { + consensus_output: ConsensusOutput { data: Vec::with_capacity(64), count: 0 }, + rejected_records: vec![rejected], + groups_count: 0, + stats: ConsensusCallingStats::default(), + overlapping_stats: None, + }; + let estimate_with_rejects = batch_with_rejects.estimate_heap_size(); + assert!( + estimate_with_rejects >= 64 + 192 + std::mem::size_of::<Vec<u8>>(), + "estimate should include rejected_records capacity and outer-vec overhead" + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/commands/duplex.rs` around lines 1702 - 1717, Extend the test_duplex_processed_batch_memory_estimate to include a second sub-case where DuplexProcessedBatch.rejected_records is non-empty so the estimate_heap_size path that accounts for reject buffer is exercised; create a rejected_records Vec with a known capacity/length (e.g., a few byte vectors or records), build a DuplexProcessedBatch with that rejected_records plus the ConsensusOutput used already, call estimate_heap_size() and assert the returned value includes both consensus_output capacity and the reject buffer size (reference DuplexProcessedBatch, rejected_records, ConsensusOutput and estimate_heap_size to locate the code).src/lib/commands/codec.rs (1)
1218-1232: ⚡ Quick winExtend memory-estimate test to cover buffered rejects.
estimate_heap_sizenow includesrejected_records, but the test only covers an empty rejects vector.Proposed test extension
#[test] fn test_codec_processed_batch_memory_estimate() { @@ let batch = CodecProcessedBatch { consensus_output: ConsensusOutput { data, count: 1 }, rejected_records: Vec::new(), groups_count: 1, stats: CodecConsensusStats::default(), }; @@ let estimate = batch.estimate_heap_size(); assert_eq!(estimate, 1024, "estimate should match consensus_output capacity"); + + let mut rejected = Vec::with_capacity(256); + rejected.extend_from_slice(&[1u8; 8]); + let batch_with_rejects = CodecProcessedBatch { + consensus_output: ConsensusOutput { data: Vec::with_capacity(128), count: 0 }, + rejected_records: vec![rejected], + groups_count: 0, + stats: CodecConsensusStats::default(), + }; + let estimate_with_rejects = batch_with_rejects.estimate_heap_size(); + assert!( + estimate_with_rejects >= 128 + 256 + std::mem::size_of::<Vec<u8>>(), + "estimate should include rejected_records capacity and outer-vec overhead" + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/commands/codec.rs` around lines 1218 - 1232, Update the test_codec_processed_batch_memory_estimate to include non-empty rejected_records so estimate_heap_size covers buffered rejects: create one or more RejectedRecord (or the concrete type used in rejected_records) containing a buffer with a known capacity/length, add them to the CodecProcessedBatch.rejected_records alongside the existing ConsensusOutput, call batch.estimate_heap_size() and assert the expected value equals the sum of consensus_output capacity plus the heap contribution of the rejected_records buffers; reference the test function name test_codec_processed_batch_memory_estimate, the struct CodecProcessedBatch, field rejected_records and the method estimate_heap_size when making the change.tests/integration/test_bgzf_eof.rs (1)
851-853: ⚡ Quick winAssert the single-threaded header expectation explicitly.
This test documents header behavior but doesn’t verify it.
Proposed assertion addition
assert!(status.success(), "correct single-threaded with rejects failed"); assert_has_bgzf_eof(&output_bam); assert_has_bgzf_eof(&rejects_bam); - // Single-threaded correct writes rejects in input order, so it keeps the - // input header as-is rather than marking it SO:unsorted. + assert_rejects_header_matches_input(&rejects_bam, &input_bam); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/integration/test_bgzf_eof.rs` around lines 851 - 853, Add an explicit assertion in the single-threaded branch of tests/integration/test_bgzf_eof.rs to verify the output header was preserved (i.e., not changed to SO:unsorted). Locate the place where the test reads or stores the resulting header after the single-threaded correct-writes path and assert that it either equals the original input header string or does not contain the "SO:unsorted" tag so the test actually verifies the documented behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/unified_pipeline/bam.rs`:
- Around line 4822-4836: The tests only compare comments() which is too narrow;
update both tests for resolve_secondary_header to assert equality of the entire
Header returned (not just comments) — e.g. replace the comments() assertions
with assertions that resolved equals the primary Header in
test_resolve_secondary_header_defaults_to_primary and that resolved equals the
override Header (and differs from primary) in
test_resolve_secondary_header_uses_override_when_provided; use Header's full
equality (or to_string()/serialization comparison if Header does not implement
PartialEq) so `@HD/`@SQ/@PG and all fields are validated.
---
Outside diff comments:
In `@src/lib/unified_pipeline/bam.rs`:
- Around line 4232-4331: In run_bam_pipeline_from_reader_with_secondary ensure
the single-threaded branch mirrors the parallel ordering: call
secondary_serialize_fn(&processed_batch, &mut buf) using a borrow of the
processed result before calling serialize_fn(processed_batch, &header, &mut buf)
so the secondary serialization sees the same pre-consumption state as in the
parallel path; locate the single-threaded serialization block that currently
writes rejects first/second and swap or reorder the calls so
secondary_serialize_fn runs with a &P borrow immediately before the primary
serialize_fn consumes the P produced by process_fn.
In `@tests/integration/test_duplex_command.rs`:
- Around line 195-202: The test docblock in
tests/integration/test_duplex_command.rs still claims the rejects BAM header is
rewritten to `SO:unsorted` due to mutex-acquisition ordering; update that
sentence to match the current asserted behavior (remove the `SO:unsorted` claim
and the "mutex-acquisition order" rationale) so the comment describes what the
test actually asserts about the rejects BAM `@HD` header now (e.g., that it
preserves the expected sort-order/header semantics asserted in the test). Edit
the comment block above the duplex pipeline test accordingly and remove any
reference to the old mutex-era semantics.
---
Nitpick comments:
In `@src/lib/commands/codec.rs`:
- Around line 1218-1232: Update the test_codec_processed_batch_memory_estimate
to include non-empty rejected_records so estimate_heap_size covers buffered
rejects: create one or more RejectedRecord (or the concrete type used in
rejected_records) containing a buffer with a known capacity/length, add them to
the CodecProcessedBatch.rejected_records alongside the existing ConsensusOutput,
call batch.estimate_heap_size() and assert the expected value equals the sum of
consensus_output capacity plus the heap contribution of the rejected_records
buffers; reference the test function name
test_codec_processed_batch_memory_estimate, the struct CodecProcessedBatch,
field rejected_records and the method estimate_heap_size when making the change.
In `@src/lib/commands/duplex.rs`:
- Around line 1702-1717: Extend the test_duplex_processed_batch_memory_estimate
to include a second sub-case where DuplexProcessedBatch.rejected_records is
non-empty so the estimate_heap_size path that accounts for reject buffer is
exercised; create a rejected_records Vec with a known capacity/length (e.g., a
few byte vectors or records), build a DuplexProcessedBatch with that
rejected_records plus the ConsensusOutput used already, call
estimate_heap_size() and assert the returned value includes both
consensus_output capacity and the reject buffer size (reference
DuplexProcessedBatch, rejected_records, ConsensusOutput and estimate_heap_size
to locate the code).
In `@tests/integration/test_bgzf_eof.rs`:
- Around line 851-853: Add an explicit assertion in the single-threaded branch
of tests/integration/test_bgzf_eof.rs to verify the output header was preserved
(i.e., not changed to SO:unsorted). Locate the place where the test reads or
stores the resulting header after the single-threaded correct-writes path and
assert that it either equals the original input header string or does not
contain the "SO:unsorted" tag so the test actually verifies the documented
behavior.
In `@tests/integration/test_simplex_command.rs`:
- Around line 326-346: The test currently only checks headers and can falsely
pass if no reads were rejected; update the test to assert there is at least one
rejected template in rejects_bam by opening rejects_bam with bam::io::Reader,
iterating its records to count unique template names (QNAME) so paired-end reads
count as one template, and add an assert that this unique-template count is >= 1
before the header assertions (use the existing rejects_bam variable and then
proceed to rejects_header/rejects_rgs checks).
🪄 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: CHILL
Plan: Pro
Run ID: 02430c5d-340f-4677-9849-a0c2a15cc5aa
📒 Files selected for processing (12)
src/lib/commands/codec.rssrc/lib/commands/common.rssrc/lib/commands/correct.rssrc/lib/commands/duplex.rssrc/lib/commands/filter.rssrc/lib/commands/simplex.rssrc/lib/unified_pipeline/bam.rstests/integration/helpers/assertions.rstests/integration/test_bgzf_eof.rstests/integration/test_correct_command.rstests/integration/test_duplex_command.rstests/integration/test_simplex_command.rs
3278f35 to
0ab78d1
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/lib/commands/simplex.rs (1)
1631-1676: ⚡ Quick winUse one
rstestfor the memory-estimate cases.These are the same test shape with different inputs. Folding them into a small
#[rstest]table will reduce duplication and match the repo test convention.As per coding guidelines,
**/*.rs: Userstestfor parameterized tests andproptestfor property-based testing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/commands/simplex.rs` around lines 1631 - 1676, Combine the two duplicate tests into a single parameterized rstest: replace the two functions test_simplex_processed_batch_memory_estimate and test_simplex_processed_batch_memory_estimate_with_rejects with one #[rstest] that supplies two cases (one with consensus_output data cap 1024/len 100 and empty rejected_records, and one with consensus cap 64/rej outer vec with two vectors of capacities 256 and 128 and appropriate count values). In the new test, construct SimplexProcessedBatch and call batch.estimate_heap_size() for each case and assert equality against the expected value computed from the input parameters (refer to ConsensusOutput, rejected_records, and use std::mem::size_of::<Vec<u8>>() to compute outer-vec overhead). Ensure the test function name remains descriptive (e.g., test_simplex_processed_batch_memory_estimates) and add the rstest crate attribute imports if missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/commands/codec.rs`:
- Around line 641-643: The current code logs MI groups processed but the
returned/used value is the number of consensus reads
(processed.consensus_output.count), so change the logging to report "consensus
reads" (or similar) instead of "MI groups" and ensure any variables or log
messages that currently reference groups_processed are replaced with the actual
consensus count (use processed.consensus_output.count or the local count
variable). Update the same pattern in the other occurrences mentioned (around
lines 656–699) so run_bam_pipeline_* return values and subsequent logs
consistently report consensus reads rather than MI groups.
In `@src/lib/commands/duplex.rs`:
- Around line 749-751: The code is logging consensus-read counts
(processed.consensus_output.count) as groups_processed; change this to count
actual MI groups produced by serialize_fn: use the MI-group count field (e.g.,
processed.mi_groups.len() or processed.group_count) and cast it to u64 for
groups_processed, and if no explicit group-count field exists fall back to
dividing consensus count by 2 (processed.consensus_output.count as u64 / 2) to
reflect that a group yields 0 or 2 consensus reads; update every occurrence that
uses processed.consensus_output.count for group counting in the duplex pipeline
(including the other spots referenced around 764–816) and keep types consistent.
In `@src/lib/commands/simplex.rs`:
- Around line 611-624: The code currently swallows errors from
apply_overlapping_consensus inside the threaded path by converting them into
per-group rejections and continuing; to match the single-threaded fast path you
must propagate the error instead of turning it into rejects. Modify the block
that handles apply_overlapping_consensus(&mut raw_records, oc) in the simplex
processing loop so that on Err(...) it returns or forwards the error upward
(preserving the original error value) rather than calling
batch_overlapping.merge(...), batch_stats.record_* and continue; keep resetting
oc.stats() and any necessary cleanup, but ensure the function/method returns the
error from apply_overlapping_consensus (or maps it into the enclosing error
type) so failures in overlapping_caller (overlapping_caller,
apply_overlapping_consensus) are not silently dropped in the threaded path.
In `@tests/integration/helpers/assertions.rs`:
- Around line 69-74: The test is reading SO/GO/SS from hdr_map.other_fields(),
but other_fields() only contains nonstandard tags so standard `@HD` sort tags are
never found; change the assertion to serialize/obtain the actual `@HD` header line
from the header object (e.g., use the header's serialization/to_string method or
API that returns the raw `@HD` line) and parse the SO, GO, SS values from that
serialized `@HD` line, then populate SortFields accordingly instead of using
other_fields(); update the code around hdr_map and SortFields construction to
extract the tags from the serialized header.
---
Nitpick comments:
In `@src/lib/commands/simplex.rs`:
- Around line 1631-1676: Combine the two duplicate tests into a single
parameterized rstest: replace the two functions
test_simplex_processed_batch_memory_estimate and
test_simplex_processed_batch_memory_estimate_with_rejects with one #[rstest]
that supplies two cases (one with consensus_output data cap 1024/len 100 and
empty rejected_records, and one with consensus cap 64/rej outer vec with two
vectors of capacities 256 and 128 and appropriate count values). In the new
test, construct SimplexProcessedBatch and call batch.estimate_heap_size() for
each case and assert equality against the expected value computed from the input
parameters (refer to ConsensusOutput, rejected_records, and use
std::mem::size_of::<Vec<u8>>() to compute outer-vec overhead). Ensure the test
function name remains descriptive (e.g.,
test_simplex_processed_batch_memory_estimates) and add the rstest crate
attribute imports if missing.
🪄 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: CHILL
Plan: Pro
Run ID: e35fad90-2e87-444c-b8a3-7266d87c65b8
📒 Files selected for processing (12)
src/lib/commands/codec.rssrc/lib/commands/common.rssrc/lib/commands/correct.rssrc/lib/commands/duplex.rssrc/lib/commands/filter.rssrc/lib/commands/simplex.rssrc/lib/unified_pipeline/bam.rstests/integration/helpers/assertions.rstests/integration/test_bgzf_eof.rstests/integration/test_correct_command.rstests/integration/test_duplex_command.rstests/integration/test_simplex_command.rs
✅ Files skipped from review due to trivial changes (2)
- tests/integration/test_correct_command.rs
- tests/integration/test_duplex_command.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- tests/integration/test_simplex_command.rs
- src/lib/commands/common.rs
- src/lib/commands/filter.rs
0ab78d1 to
cad793f
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/lib/unified_pipeline/bam.rs (1)
4828-4842: 💤 Low valueConsider
rstestfor the tworesolve_secondary_headercases.The
None-defaults-to-primary andSome-overrides tests exercise the same function over different inputs — a natural fit forrstestcase parameters per the repo coding guidelines.♻️ Optional refactor
- #[test] - fn test_resolve_secondary_header_defaults_to_primary() { - let primary = Header::builder().add_comment("primary").build(); - let resolved = resolve_secondary_header(None, &primary); - assert_eq!(resolved, primary); - } - - #[test] - fn test_resolve_secondary_header_uses_override_when_provided() { - let primary = Header::builder().add_comment("primary").build(); - let override_header = Header::builder().add_comment("rejects").build(); - let resolved = resolve_secondary_header(Some(&override_header), &primary); - assert_eq!(resolved, override_header); - assert_ne!(resolved, primary); - } + #[rstest] + #[case(None, "primary", "primary")] // None → falls back to primary + #[case(Some("rejects"), "primary", "rejects")] // Some → override wins + fn test_resolve_secondary_header( + #[case] override_comment: Option<&str>, + #[case] primary_comment: &str, + #[case] expected_comment: &str, + ) { + let primary = Header::builder().add_comment(primary_comment).build(); + let override_header = + override_comment.map(|c| Header::builder().add_comment(c).build()); + let resolved = + resolve_secondary_header(override_header.as_ref(), &primary); + let expected = Header::builder().add_comment(expected_comment).build(); + assert_eq!(resolved, expected); + }As per coding guidelines: "Use
rstestfor parameterized tests."🤖 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/unified_pipeline/bam.rs` around lines 4828 - 4842, Combine the two unit tests into a single parameterized rstest for resolve_secondary_header: replace test_resolve_secondary_header_defaults_to_primary and test_resolve_secondary_header_uses_override_when_provided with one #[rstest] test that takes an Option<&Header> input and an expected Header, constructing primary via Header::builder().add_comment("primary").build() and override via Header::builder().add_comment("rejects").build() for the Some case, then call resolve_secondary_header(input, &primary) and assert_eq! against the expected; remove the duplicate tests and import rstest::rstest as needed.tests/integration/test_correct_command.rs (1)
234-257: ⚡ Quick winAssert reject order, not just membership.
This proves “no drops / no dups”, but not the new contract that rejects are emitted in input order. A regression back to mutex-acquisition order would still pass here.
Possible assertion shape
let mut reader = bam::io::Reader::new(fs::File::open(&rejects_bam).unwrap()); let _header = reader.read_header().unwrap(); let mut seen: std::collections::HashMap<String, u32> = std::collections::HashMap::new(); + let mut observed_names = Vec::new(); let mut total: usize = 0; for result in reader.records() { let record = result.expect("Failed to read reject record"); let name = record.name().expect("reject record missing read name").to_string(); + observed_names.push(name.clone()); *seen.entry(name).or_insert(0) += 1; total += 1; } + + let expected_order: Vec<String> = ["far_a", "far_b", "far_c"] + .into_iter() + .flat_map(|base| (0..far_family_size).map(move |i| format!("{base}_{i}"))) + .collect(); + assert_eq!(observed_names, expected_order, "rejects should preserve input order");🤖 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 `@tests/integration/test_correct_command.rs` around lines 234 - 257, The test currently verifies only membership/counts of rejects (variables seen, total, expected_rejects) but not their input order; update the test in test_correct_command.rs to assert ordered emission by reading the rejects BAM into a Vec<String> (preserving read order from bam::io::Reader), then compare that vector directly to expected_names (or expected_names[..expected_rejects]) to ensure exact sequence equality; keep or replace the current HashMap/member checks (seen/total) with this ordered comparison and fail early with a clear message if the sequences differ.src/lib/commands/correct.rs (1)
325-332: ⚡ Quick winAdd a unit test for the new heap estimate.
This now drives pipeline throttling and includes a second buffered output path. A small
CorrectProcessedBatch::estimate_heap_size()test would keep later tweaks honest.As per coding guidelines,
src/**/*.rs: Place unit tests alongside source code insrc/directory using#[cfg(test)]modules.🤖 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/correct.rs` around lines 325 - 332, Add a unit test in a #[cfg(test)] mod in the same src file that constructs a CorrectProcessedBatch with controlled kept_raw_records and rejected_records (populate RawRecord instances and Vec<u8> with known lengths/capacities), calls CorrectProcessedBatch::estimate_heap_size(), and asserts it equals the expected sum computed from RawRecord::capacity() for each kept_raw_records, Vec::capacity() for each rejected record plus the vector capacity overheads (kept_raw_records.capacity() * size_of::<RawRecord>() and rejected_records.capacity() * size_of::<Vec<u8>>()). Name the test clearly (e.g., estimate_heap_size_counts_all_buffers) and keep it small and deterministic.
🤖 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/duplex.rs`:
- Around line 679-727: The batch statistics are being merged inside the MI-group
loop which double-counts cumulative stats; remove the per-group merge call
(remove batch_stats.merge(&caller.statistics()) from inside the for MiGroup
loop) and instead, after the loop completes, set/merge batch_stats once from the
caller's cumulative statistics (e.g., batch_stats = caller.statistics() or
batch_stats.merge(&caller.statistics()) once after the loop) so the threaded
path matches the single-threaded behavior; keep per-group rejected_records
collection and overlapping_stats handling unchanged and ensure the final
DuplexProcessedBatch uses the single aggregated batch_stats.
---
Nitpick comments:
In `@src/lib/commands/correct.rs`:
- Around line 325-332: Add a unit test in a #[cfg(test)] mod in the same src
file that constructs a CorrectProcessedBatch with controlled kept_raw_records
and rejected_records (populate RawRecord instances and Vec<u8> with known
lengths/capacities), calls CorrectProcessedBatch::estimate_heap_size(), and
asserts it equals the expected sum computed from RawRecord::capacity() for each
kept_raw_records, Vec::capacity() for each rejected record plus the vector
capacity overheads (kept_raw_records.capacity() * size_of::<RawRecord>() and
rejected_records.capacity() * size_of::<Vec<u8>>()). Name the test clearly
(e.g., estimate_heap_size_counts_all_buffers) and keep it small and
deterministic.
In `@src/lib/unified_pipeline/bam.rs`:
- Around line 4828-4842: Combine the two unit tests into a single parameterized
rstest for resolve_secondary_header: replace
test_resolve_secondary_header_defaults_to_primary and
test_resolve_secondary_header_uses_override_when_provided with one #[rstest]
test that takes an Option<&Header> input and an expected Header, constructing
primary via Header::builder().add_comment("primary").build() and override via
Header::builder().add_comment("rejects").build() for the Some case, then call
resolve_secondary_header(input, &primary) and assert_eq! against the expected;
remove the duplicate tests and import rstest::rstest as needed.
In `@tests/integration/test_correct_command.rs`:
- Around line 234-257: The test currently verifies only membership/counts of
rejects (variables seen, total, expected_rejects) but not their input order;
update the test in test_correct_command.rs to assert ordered emission by reading
the rejects BAM into a Vec<String> (preserving read order from bam::io::Reader),
then compare that vector directly to expected_names (or
expected_names[..expected_rejects]) to ensure exact sequence equality; keep or
replace the current HashMap/member checks (seen/total) with this ordered
comparison and fail early with a clear message if the sequences differ.
🪄 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: CHILL
Plan: Pro
Run ID: 95fe6704-e155-4221-b632-95ebf2693bfb
📒 Files selected for processing (11)
src/lib/commands/codec.rssrc/lib/commands/common.rssrc/lib/commands/correct.rssrc/lib/commands/duplex.rssrc/lib/commands/filter.rssrc/lib/commands/simplex.rssrc/lib/unified_pipeline/bam.rstests/integration/helpers/assertions.rstests/integration/test_bgzf_eof.rstests/integration/test_correct_command.rstests/integration/test_simplex_command.rs
✅ Files skipped from review due to trivial changes (1)
- tests/integration/test_simplex_command.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- tests/integration/helpers/assertions.rs
- src/lib/commands/common.rs
- tests/integration/test_bgzf_eof.rs
cad793f to
8990a9e
Compare
|
@coderabbitai review |
…ader Add a `secondary_output_header: Option<Header>` parameter to `run_bam_pipeline_from_reader_with_secondary`. Defaults to the resolved primary output header (filter.rs's existing behavior). Consensus commands need this so the rejects BAM advertises the input header (raw input records) while the primary BAM advertises the consensus header.
Replace the hand-rolled `Arc<Mutex<Option<RawBamWriter>>>` in `execute_threads_mode` with the unified pipeline's first-class secondary output. Rejected records are now collected into the per-batch `CorrectProcessedBatch::rejected_records` field and serialized via a `secondary_serialize_fn`, so they land in batch-input order rather than mutex-acquisition order. The pipeline finalizes the secondary writer internally, dropping the post-pipeline mutex/finish dance. Drops `header_as_unsorted` for the rejects header: the pipeline reuses the input header (via `secondary_output_header = None`, which falls back to the resolved primary header — same as input header for correct), and the input's claimed sort order remains valid because rejects are now in input order (a subset of an SO-X stream is still SO-X). Add `assert_rejects_header_matches_input` for tests that previously asserted the old hand-rolled `SO:unsorted` contract; keep `assert_header_unsorted` for the simplex/duplex/codec rejects tests until those commands are migrated in follow-up commits.
Mirror the correct.rs migration. Both reject sources (input records on batch-level skip + consensus-caller rejects) collect into `SimplexProcessedBatch::rejected_records` and flow through the unified pipeline's first-class secondary output. The rejects BAM advertises the input header (passed via `secondary_output_header = Some(input_header)`) so raw-input RG/PG/contig metadata is preserved while the primary BAM keeps the consensus header. Hoist the inline closures into named bindings so the pipeline call can branch between the secondary-aware and plain entry points without duplicating the closure bodies.
Mirror the simplex migration. `DuplexProcessedBatch::rejected_records` collects bytes returned by the consensus caller; the unified pipeline's secondary output writes them in batch-input order with the input header.
Mirror the duplex migration. Both reject paths (success and duplex- disagreement recovery) append to `CodecProcessedBatch::rejected_records` and flow through the unified pipeline's first-class secondary output. Drop the now-unused `assert_header_unsorted` helper from the integration test harness; all four migrated commands use `assert_rejects_header_matches_input` instead.
…pipeline Issue 329 acceptance criterion: "A small note in each migrated command's module docs pointing readers at the unified pipeline pattern." The in-function comments added during the migration commits captured the intent but weren't visible from rustdoc; promote them to module-level `//!` docstrings on `correct`, `simplex`, `duplex`, and `codec` so the breadcrumb to `run_bam_pipeline_from_reader_with_secondary` shows up in the rendered docs and at the top of each file.
Five fixes from a self-review of commits f43f492..4e5f1a1: 1. Stale comment in correct.rs's `serialize_fn` ("Rejects were already streamed to disk in process_fn") removed; rejects now flow through `secondary_serialize_fn`, matching the comment style used by the other three migrated commands. 2. correct.rs's `process_fn` was cloning each rejected record via `raw.as_ref().to_vec()` even though `raw_records` is consumed immediately after the loop. Switch to `for raw in raw_records` moving out via `RawRecord::into_inner()`. Removes one alloc + memcpy per rejected record on the rejects hot path; matches what simplex/duplex/codec already do (their callers return owned bytes). 3. Document the new `secondary_output_header: Option<Header>` parameter on `run_bam_pipeline_from_reader_with_secondary` with a `# Parameters` doc block explaining when to pass `None` vs `Some(input_header)`. 4. Hoist the byte-identical `secondary_serialize_fn` bodies out of the four migrated commands and filter's private `serialize_raw_records` into a single `pub(crate) fn serialize_raw_bam_records<R: AsRef<[u8]>>( records: &[R], output: &mut Vec<u8>) -> io::Result<u64>` in commands/common.rs. Eliminates ~70 lines of framing duplication and gives the five callers one canonical home for BAM-record framing. 5. Add `test_simplex_command_rejects_inherits_input_read_groups`: an integration test that asserts the rejects BAM advertises the input header's RG1+RG2, while the primary BAM advertises the consensus- collapsed RG "A". Catches a regression where a caller would accidentally swap the secondary_output_header arg. All 2070 tests pass; lint and rustdoc clean.
a95618f to
5cd9d1a
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
…wning type (#368) `DuplexProcessedBatch`, `SimplexProcessedBatch`, and `CodecProcessedBatch` all read `consensus_output.data.capacity()` directly from their `MemoryEstimate::estimate_heap_size` impls. `ConsensusOutput` is `{ data: Vec<u8>, count: usize }` today, so the estimate is accurate, but any future owned heap-allocating field added to `ConsensusOutput` would silently undercount in all three batch impls — and the unified pipeline's memory-bound queueing would mis-throttle as a result. Add an inherent `ConsensusOutput::estimate_heap_size(&self) -> usize` that owns the accounting for its own fields, and replace the three `.consensus_output.data.capacity()` call sites with `.consensus_output.estimate_heap_size()`. The current return value is unchanged (`data.capacity()`), so existing `MemoryEstimate` tests for all three batches keep passing without modification. Inherent method rather than `impl MemoryEstimate for ConsensusOutput` to avoid adding `fgumi-bam-io` as a dependency of `fgumi-consensus` just to host the trait impl; the issue body lists both forms as acceptable. Two new unit tests in `fgumi-consensus` lock the delegation contract: empty `ConsensusOutput` -> 0, populated -> `data.capacity()`. Audited the rest of the codebase for the same "reach into a sibling type's private field" pattern in `MemoryEstimate` impls: - The `rejected_records: Vec<Vec<u8>>` accounting in all three batch impls inspects a field the batch itself owns, not a sibling type's internals — that's the batch doing its own job, not in scope. - All other `MemoryEstimate` impls (grouper, template, fastq_parse, mi_group, unified_pipeline::{base,fastq}, commands::{filter, dedup, extract, clip, correct}, and the trait's built-ins in fgumi-bam-io) already delegate via `estimate_heap_size` rather than reading inner capacities directly. Flagged by CodeRabbit on PR #332 as defense-in-depth. Closes #339
…tract) (#397) `fgumi correct --rejects` stamped the rejects BAM with header_as_unsorted(self.header), stripping the input's @hd GO/SS sort fields and forcing SO:unsorted. Per the PR #332 rejects contract, rejects are raw-input records (input order, already via correct's fan-out branch) and must carry the INPUT header verbatim. For correct, self.header IS the input header (correct preserves input order and does not swap in a consensus header), so use it as-is for the rejects writer. Fixes test_bgzf_eof::{test_correct_rejects_has_bgzf_eof, test_correct_single_threaded_rejects_has_bgzf_eof}. The simplex/duplex/codec rejects paths still use header_as_unsorted via their mutex side-channel; correcting those requires the full fan-out migration (input header + input order together, since a header-only change would mislabel the mutex-order records) — tracked separately.
…n-out (#332) (#398) The simplex, duplex, and codec consensus steps streamed rejected reads to a side-channel `Arc<Mutex<Option<RawBamWriter>>>`: workers flushed under a mutex (so rejects landed in mutex-acquisition order, not input order) and the rejects BAM was stamped `SO:unsorted` via header_as_unsorted(output_header) (dropping the input's @hd GO/SS + raw-input RG/PG). That violates the PR #332 rejects contract (rejects are raw-input records → INPUT header verbatim + INPUT order; only the primary BAM carries the consensus header) and is what correct/filter already do. Migrate all three to the same `Process2WithWorkerState` fan-out as correct_step_with_rejects: branch 0 carries the consensus DecompressedBlock, branch 1 carries `[len][record]`-framed rejected raw-input records that flow through the ordered serialize/BgzfCompress/WriteBgzfFile stages — giving input order, and a rejects writer configured with the captured input header. Each consensus step factory splits into a 2-output `*_with_rejects` variant (used with --rejects) and a 1-output `*_kept_only` variant (the framework has no public discard sink). The 3 mutex writers + their finalize-hook flush + the SO:unsorted restamp are deleted. Fixes test_bgzf_eof::{test_simplex,test_duplex,test_codec}_rejects_has_bgzf_eof (assert_rejects_header_matches_input). All 14 bgzf_eof tests + the full simplex/duplex/codec suites pass; clippy + fmt clean.
…tract) (#397) `fgumi correct --rejects` stamped the rejects BAM with header_as_unsorted(self.header), stripping the input's @hd GO/SS sort fields and forcing SO:unsorted. Per the PR #332 rejects contract, rejects are raw-input records (input order, already via correct's fan-out branch) and must carry the INPUT header verbatim. For correct, self.header IS the input header (correct preserves input order and does not swap in a consensus header), so use it as-is for the rejects writer. Fixes test_bgzf_eof::{test_correct_rejects_has_bgzf_eof, test_correct_single_threaded_rejects_has_bgzf_eof}. The simplex/duplex/codec rejects paths still use header_as_unsorted via their mutex side-channel; correcting those requires the full fan-out migration (input header + input order together, since a header-only change would mislabel the mutex-order records) — tracked separately.
…n-out (#332) (#398) The simplex, duplex, and codec consensus steps streamed rejected reads to a side-channel `Arc<Mutex<Option<RawBamWriter>>>`: workers flushed under a mutex (so rejects landed in mutex-acquisition order, not input order) and the rejects BAM was stamped `SO:unsorted` via header_as_unsorted(output_header) (dropping the input's @hd GO/SS + raw-input RG/PG). That violates the PR #332 rejects contract (rejects are raw-input records → INPUT header verbatim + INPUT order; only the primary BAM carries the consensus header) and is what correct/filter already do. Migrate all three to the same `Process2WithWorkerState` fan-out as correct_step_with_rejects: branch 0 carries the consensus DecompressedBlock, branch 1 carries `[len][record]`-framed rejected raw-input records that flow through the ordered serialize/BgzfCompress/WriteBgzfFile stages — giving input order, and a rejects writer configured with the captured input header. Each consensus step factory splits into a 2-output `*_with_rejects` variant (used with --rejects) and a 1-output `*_kept_only` variant (the framework has no public discard sink). The 3 mutex writers + their finalize-hook flush + the SO:unsorted restamp are deleted. Fixes test_bgzf_eof::{test_simplex,test_duplex,test_codec}_rejects_has_bgzf_eof (assert_rejects_header_matches_input). All 14 bgzf_eof tests + the full simplex/duplex/codec suites pass; clippy + fmt clean.
Closes #329.
Summary
secondary_output_header: Option<Header>torun_bam_pipeline_from_reader_with_secondaryso the rejects BAM can carry a different header than the primary output. Defaults (None) reuse the resolved primary header —filter's prior behavior. Consensus commands passSome(input_header)so the rejects BAM advertises the raw-input header rather than the consensus header.correct,simplex,duplex,codec(execute_threads_mode) from a hand-rolledArc<Mutex<Option<RawBamWriter>>>to the unified pipeline's first-classsecondary_outputpath. Each command'sProcessedBatchgains arejected_records: Vec<Vec<u8>>field; rejects flow through a per-batch buffer and the pipeline drains them viasecondary_serialize_fnin batch-input order.header_as_unsortedfrom the four rejects sites: the input header's claimed sort order remains valid for the rejects subset because rejects are now in input/batch-serial order (a subset of an SO-X stream is still SO-X).secondary_serialize_fnframing into a singlepub(crate) fn serialize_raw_bam_records<R: AsRef<[u8]>>(records: &[R], output: &mut Vec<u8>) -> io::Result<u64>incommands/common.rs. Filter's previously-privateserialize_raw_recordsis removed in favor of the shared helper.Order change (user-visible)
Rejects records previously landed in mutex-acquisition order across worker threads (arbitrary, non-deterministic). After this change they land in batch-serial order = input order, matching the primary BAM. Rejects content is unchanged; only the order changes.
Existing integration tests use content (set/count) assertions, not specific orderings, so they pass unchanged. The one helper that asserted the old hand-rolled
SO:unsortedcontract (assert_header_unsorted) is replaced byassert_rejects_header_matches_input(rejects, input), which verifies the rejects header inherits the input's sort fields verbatim.Memory profile note
The hand-rolled path streamed rejects to disk during
process_fnunder a mutex — per-batch reject memory was effectively zero. The new path collects rejects inbatch.rejected_records: Vec<Vec<u8>>until the pipeline's serialize step consumes them. At high reject rates this adds in-flight memory bounded by the pipeline'squeue_memory_limit. Each migrated command'sMemoryEstimateimpl was updated to account forrejected_records.iter().map(Vec::capacity).sum() + rejected_records.capacity() * size_of::<Vec<u8>>(), so the pipeline correctly throttles.Per-command perf bench: deferred
The issue's acceptance criteria include a per-command perf bench (≤5% wall + RSS regression). The local benchmark host wasn't quiet at the time of this PR (load average 32, BWA + indexer + media all running). The bench will land in a follow-up commit on this branch — either when the local host is quiet (`hyperfine` + `gtime` + `sudo purge` between trials) or on the documented `m7g.4xlarge` AWS instance (`/usr/bin/time -v`). The bench artifact will land at `docs/perf/issue-329-rejects-secondary.md`.
Test plan
Out of scope (per issue)