Skip to content

fix: keep record order when reads are parallelised - #105

Merged
cauliyang merged 1 commit into
mainfrom
fix/deterministic-parallel-reads
Sep 20, 2026
Merged

cauliyang merged 1 commit into
mainfrom
fix/deterministic-parallel-reads

Conversation

@cauliyang

Copy link
Copy Markdown
Owner

Problem

main's MSRV job (Rust 1.90) failed on deepbiop-fq's io::tests::test_write_fq_parallel:

assertion `left == right` failed
  left: "record2"
 right: "record1"

The test reads records with par_bridge() and compares them element-wise with the source order, but par_bridge().collect() does not preserve order. It only passed before because calculate_worker_count(None) returned 1; now that None means "all cores", the ordering assumptions are exercised — and several library functions silently returned records in arbitrary order (flagged in review as "par_bridge() scrambles record order").

Fix

Record decoding is inherently serial (a single &mut reader behind par_bridge's mutex), so the reads now happen in file order and into_par_iter() parallelises the per-record work, which preserves order:

  • deepbiop-bam: read_all, filter_by_mapping_quality, extract_read_pairs, extract_features, count_chimeric, bam2fq, chimeric::count, chimeric::event
  • deepbiop-fa: select_record_from_fa_by_stream
  • deepbiop-fq::predicts: pt files collected in sorted walk order so max_predicts keeps the same files every run
  • bam2fq now propagates malformed records instead of unwrap()ing

Tests

  • new crates/deepbiop-bam/tests/read_order.rs: asserts identical record order across worker_count 1/2/default and for filter_by_mapping_quality/extract_features; verified to fail when par_bridge is reintroduced
  • test_write_fq_parallel now asserts the sequential file order and compares the parallel read as a multiset

Reading is serial, so the per-record work is parallelised with par_iter, which
preserves order; par_bridge().collect() returned records in arbitrary order. That
became visible once the default worker count changed to 'all cores': the FASTQ
write test compares records element-wise and flaked with SIGILL-free but spurious
'record2 != record1' failures on MSRV.

- bam reader read_all/filter_by_mapping_quality/extract_read_pairs/extract_features/
  count_chimeric, bam2fq, chimeric count/events, and fa select_record_from_fa_by_stream
  now read in file order and map in parallel
- bam2fq propagates malformed records instead of unwrapping
- predicts: pt files collected in sorted walk order so max_predicts is deterministic
- tests: FASTQ write asserts the sequential file order and compares the parallel
  read as a multiset; new bam read_order test pins order across worker counts
  (fails if par_bridge is reintroduced)
Copilot AI lite review requested due to automatic review settings September 20, 2026 05:32

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

Copilot review overview

🟡 Changes recommended

Unresolved ordering and large-input memory regressions remain in several BAM processing paths.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 6 Medium severity · 1 Low severity

Open (7)
What changed in this PR

This PR restores deterministic record ordering while retaining parallel processing and makes prediction-file selection deterministic.

Changes:

  • Replaces unordered parallel reads with ordered processing.
  • Sorts prediction files before applying max_predicts.
  • Adds ordering regression tests and updates FASTQ tests.
File Summary
crates/​deepbiop-fq/​src/​predicts.rs Deterministic prediction-file selection.
crates/​deepbiop-fq/​src/​io.rs Updated parallel FASTQ ordering test.
crates/​deepbiop-fa/​src/​io.rs Ordered FASTA selection.
crates/​deepbiop-bam/​tests/​read_order.rs BAM ordering regression tests.
crates/​deepbiop-bam/​src/​reader/​mod.rs Ordered BAM reads and transformations.
crates/​deepbiop-bam/​src/​io.rs Ordered BAM-to-FASTQ conversion.
crates/​deepbiop-bam/​src/​chimeric/​event.rs Chimeric event processing.
crates/​deepbiop-bam/​src/​chimeric/​count.rs Chimeric-read filtering.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +65 to +68
Ok(records
.into_par_iter()
.filter(|record| is_retain_record(record) && is_chimeric_record(record))
.collect::<Vec<bam::Record>>())
Comment on lines +202 to +206
let events = records
.into_par_iter()
.filter(|record| {
is_retain_record(record)
&& is_chimeric_record(record)
Comment on lines +32 to +33
let name =
String::from_utf8_lossy(record.name().unwrap_or_default().as_ref()).to_string();
Comment on lines +103 to +105
Ok(records
.into_par_iter()
.filter(|record| {
Comment on lines +241 to +245
let records = self
.reader
.records()
.par_bridge()
.try_fold(
|| 0usize,
|acc, result| {
let record = result.context("Failed to read BAM record")?;
Ok::<usize, anyhow::Error>(if record.flags().is_supplementary() {
acc + 1
} else {
acc
})
},
)
.try_reduce(
|| 0usize,
|a: usize, b: usize| Ok::<usize, anyhow::Error>(a + b),
)
.collect::<std::io::Result<Vec<_>>>()
.context("Failed to read BAM records")?;
Comment on lines +277 to +280
let records = reader
.records()
.par_bridge()
.filter_map(|record| {
let record = record.unwrap();
.collect::<std::io::Result<Vec<_>>>()
.context("Failed to read FASTA records")?;
Comment on lines +75 to +79
let mut feature_reader = BamReader::open(Path::new(TEST_BAM), None).unwrap();
let features = feature_reader.extract_features().unwrap();
assert_eq!(
features.len(),
all.len(),
@cauliyang
cauliyang merged commit 5ae0cd9 into main Sep 20, 2026
32 checks passed
@cauliyang
cauliyang deleted the fix/deterministic-parallel-reads branch September 20, 2026 05:41
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