Skip to content

Commit 9c31886

Browse files
authored
Merge pull request #19 from msk-access/release/0.8.3
Release 0.8.3
2 parents 4345f1c + a40edf9 commit 9c31886

30 files changed

Lines changed: 465 additions & 114 deletions

File tree

CHANGELOG.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,56 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.8.3] - 2026-04-28
6+
7+
### Fixed
8+
- **GIL Deadlock in Parallel Modules**: Wrapped all Rayon `par_iter()` calls in
9+
`py.allow_threads()` across `mfsd.rs`, `uxm.rs`, `extract_motif.rs`, and
10+
`region_mds.rs`. Previously, `pyo3-log` attempted to acquire the GIL from
11+
Rayon worker threads while the main thread held it, causing indefinite hangs
12+
on multi-threaded runs (observed as 16-hour wall time on IRIS HPC).
13+
- **mFSD Silent Error Swallowing**: Replaced `Err(_) => continue` in BAM record
14+
iterator with a logged error breaker (max 1000 consecutive errors). Previously,
15+
corrupt BAM regions could cause infinite silent loops.
16+
- **FILTER_MAF Substring Match**: Changed `sample_id in tsb` to exact match
17+
(`sample_id == tsb`) preventing `T01` from matching `T01-XS1`.
18+
- **FILTER_MAF Comment Lines**: Stripped `#` comment lines from filtered MAF output
19+
in both multi-sample and single-sample modes.
20+
- **mFSD 0-Variant Guard**: Added early exit at both Python (`wrapper.py`) and Rust
21+
layers when MAF has 0 data lines. Produces header-only TSV instead of attempting
22+
BAM access.
23+
- **mFSD GC Correction Fallback**: When reference FASTA is unavailable or GC lookup
24+
fails for a region, GC correction is now skipped (weight=1.0) instead of silently
25+
using a hardcoded 50% GC.
26+
27+
### Added
28+
- **mFSD BAM I/O Diagnostics**: Per-variant timing, BAM open/fetch latency logging,
29+
record counts, and slow-variant warnings (>30s). All diagnostics use structured
30+
`log` macros (visible with `--verbose`).
31+
- **mFSD Header Constant**: Extracted 46-column TSV header into `MFSD_HEADER` module
32+
constant, eliminating duplication between normal output and 0-variant early-exit.
33+
34+
### Changed
35+
- **Minimum Python**: `requires-python` raised from `>=3.8` to `>=3.10` (Python
36+
3.8 and 3.9 are EOL).
37+
- **Minimum Rust**: Added `rust-version = "1.87"` MSRV to `Cargo.toml`.
38+
- **Cargo Dependencies**: Updated 81 semver-compatible transitive dependencies
39+
(rayon 1.11→1.12, anyhow 1.0.100→1.0.102, flate2 1.1.5→1.1.9, etc.).
40+
- **Diagnostic Logging**: Converted all `eprintln!` breadcrumbs inside parallel
41+
closures to structured `debug!`/`warn!` macros for proper timestamp/level
42+
formatting through Python's logging framework.
43+
44+
### Documentation
45+
- **Developer Guide**: Added "PyO3 + Rayon GIL deadlock" Known Gotcha with correct
46+
and incorrect code examples.
47+
- **Developer Guide**: Updated contributing checklist test count (244→357).
48+
49+
### Tests
50+
- Added `test_mfsd_zero_variants` — verifies 0-variant input produces header-only TSV.
51+
- Added `test_mfsd_maf_with_comment_lines` — verifies MAFs with `#` comment headers
52+
are parsed correctly.
53+
- Total: 357 passed, 4 skipped.
54+
555
## [0.8.2] - 2026-03-26
656

757
### Fixed

docs/development/developer-guide.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,44 @@ This gotcha caused a silent data loss bug in v0.8.0 where `MDS.exon.tsv` and
354354

355355
---
356356

357+
### PyO3 + Rayon: Always Release the GIL Before `par_iter`
358+
359+
When calling Rayon `par_iter()` from a `#[pyfunction]`, the GIL must be
360+
released first. Otherwise, `pyo3-log` (our global logger) tries to acquire
361+
the GIL from worker threads while the main thread holds it**deadlock**.
362+
363+
```rust
364+
// ✅ CORRECT: release GIL before parallel work
365+
fn my_function(py: Python, ...) -> PyResult<...> {
366+
let results = py.allow_threads(|| {
367+
items.par_iter().map(|item| {
368+
info!("Processing {}", item); // safe — GIL is released
369+
process(item)
370+
}).collect()
371+
});
372+
Ok(results)
373+
}
374+
375+
// ❌ WRONG: GIL held during par_iter
376+
fn my_function(...) -> PyResult<...> {
377+
let results = items.par_iter().map(|item| {
378+
info!("Log from worker"); // DEADLOCK — pyo3-log needs GIL
379+
process(item)
380+
}).collect();
381+
Ok(results)
382+
}
383+
```
384+
385+
**Affected modules** (all patched in v0.8.3):
386+
`mfsd.rs`, `uxm.rs`, `extract_motif.rs`, `region_mds.rs`.
387+
388+
**Rule**: Any `#[pyfunction]` using `rayon::par_iter()` MUST accept `py: Python`
389+
and wrap the parallel block in `py.allow_threads(|| { ... })`.
390+
391+
This gotcha caused 16-hour wall-time hangs on the IRIS HPC cluster in v0.8.2.
392+
393+
---
394+
357395
### `_core.pyi` — Rust Extension Stub Maintenance
358396

359397
`src/krewlyzer/_core.pyi` is a **type stub** for the compiled Rust/PyO3 extension
@@ -384,7 +422,7 @@ python -m mypy src/krewlyzer/ --ignore-missing-imports --no-error-summary
384422
- [ ] Code follows existing patterns
385423
- [ ] Added/updated tests
386424
- [ ] Updated documentation
387-
- [ ] Ran `pytest tests/` — 244 pass, 4 skipped
425+
- [ ] Ran `pytest tests/`357 pass, 4 skipped
388426
- [ ] If Rust functions changed: updated `src/krewlyzer/_core.pyi` stub
389427
- [ ] Ran Python lint (matches CI lint job):
390428
```bash

nextflow/main.nf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ include { KREWLYZER } from './workflows/krewlyzer/main'
3333
def helpMessage() {
3434
log.info """
3535
================================================================
36-
K R E W L Y Z E R P I P E L I N E (v${workflow.manifest.version ?: '0.8.2'})
36+
K R E W L Y Z E R P I P E L I N E (v${workflow.manifest.version ?: '0.8.3'})
3737
================================================================
3838
Usage:
3939
nextflow run main.nf --samplesheet samples.csv --ref hg19.fa [options]

nextflow/modules/local/krewlyzer/build_pon/main.nf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
process KREWLYZER_BUILD_PON {
1111
tag "$assay"
1212
label 'process_high'
13-
container "ghcr.io/msk-access/krewlyzer:0.8.2"
13+
container "ghcr.io/msk-access/krewlyzer:0.8.3"
1414

1515
input:
1616
path sample_list // Text file with BAM/CRAM/BED.gz paths (one per line)
@@ -60,7 +60,7 @@ process KREWLYZER_BUILD_PON {
6060
6161
cat <<-END_VERSIONS > versions.yml
6262
"${task.process}":
63-
krewlyzer: 0.8.2
63+
krewlyzer: 0.8.3
6464
END_VERSIONS
6565
"""
6666
}

nextflow/modules/local/krewlyzer/extract/main.nf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
process KREWLYZER_EXTRACT {
1111
tag "$meta.id"
1212
label 'process_medium'
13-
container "ghcr.io/msk-access/krewlyzer:0.8.2"
13+
container "ghcr.io/msk-access/krewlyzer:0.8.3"
1414

1515
input:
1616
tuple val(meta), path(bam), path(bai)
@@ -65,7 +65,7 @@ process KREWLYZER_EXTRACT {
6565
6666
cat <<-END_VERSIONS > versions.yml
6767
"${task.process}":
68-
krewlyzer: 0.8.2
68+
krewlyzer: 0.8.3
6969
END_VERSIONS
7070
"""
7171
}

nextflow/modules/local/krewlyzer/filter_maf/main.nf

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ process FILTER_MAF {
5353
unique_tsbs = set()
5454
5555
for line in infile:
56-
outfile.write(line) # always write — no filtering
56+
# Skip comment lines (not needed by downstream mFSD parser)
5757
if line.startswith('#'):
5858
continue
59+
outfile.write(line) # pass through all data rows
5960
fields = line.strip().split('\\t')
6061
if tsb_col is None:
6162
for i, col in enumerate(fields):
@@ -70,7 +71,7 @@ process FILTER_MAF {
7071
if len(unique_tsbs) > 1:
7172
print(f"WARNING: MAF contains {len(unique_tsbs)} unique Tumor_Sample_Barcodes: {unique_tsbs}", file=sys.stderr)
7273
print(f"WARNING: single_sample_maf=true so no filtering applied — all samples included", file=sys.stderr)
73-
if unique_tsbs and not any(sample_id_lower in tsb.lower() for tsb in unique_tsbs):
74+
if unique_tsbs and not any(sample_id_lower == tsb.lower() for tsb in unique_tsbs):
7475
print(f"WARNING: sample '{sample_id}' not found in Tumor_Sample_Barcode values: {unique_tsbs}", file=sys.stderr)
7576
7677
with open('versions.yml', 'w') as vf:
@@ -95,9 +96,8 @@ process FILTER_MAF {
9596
tsb_col = None
9697
9798
for line in infile:
98-
# Handle comment lines (keep them)
99+
# Skip comment lines (not needed by downstream mFSD parser)
99100
if line.startswith('#'):
100-
outfile.write(line)
101101
continue
102102
103103
fields = line.strip().split('\\t')
@@ -119,9 +119,9 @@ process FILTER_MAF {
119119
120120
total_rows += 1
121121
122-
# Filter: check if sample_id is a substring of Tumor_Sample_Barcode
122+
# Filter: exact case-insensitive match on Tumor_Sample_Barcode
123123
if tsb_col is not None:
124-
if len(fields) > tsb_col and sample_id_lower in fields[tsb_col].lower():
124+
if len(fields) > tsb_col and sample_id_lower == fields[tsb_col].lower():
125125
outfile.write(line)
126126
variant_count += 1
127127
else:

nextflow/modules/local/krewlyzer/fsc/main.nf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
process KREWLYZER_FSC {
1212
tag "$meta.id"
1313
label 'process_medium'
14-
container "ghcr.io/msk-access/krewlyzer:0.8.2"
14+
container "ghcr.io/msk-access/krewlyzer:0.8.3"
1515

1616
input:
1717
tuple val(meta), path(bed)
@@ -64,7 +64,7 @@ process KREWLYZER_FSC {
6464
6565
cat <<-END_VERSIONS > versions.yml
6666
"${task.process}":
67-
krewlyzer: 0.8.2
67+
krewlyzer: 0.8.3
6868
END_VERSIONS
6969
"""
7070
}

nextflow/modules/local/krewlyzer/fsd/main.nf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
process KREWLYZER_FSD {
1111
tag "$meta.id"
1212
label 'process_medium'
13-
container "ghcr.io/msk-access/krewlyzer:0.8.2"
13+
container "ghcr.io/msk-access/krewlyzer:0.8.3"
1414

1515
input:
1616
tuple val(meta), path(bed)
@@ -57,7 +57,7 @@ process KREWLYZER_FSD {
5757
5858
cat <<-END_VERSIONS > versions.yml
5959
"${task.process}":
60-
krewlyzer: 0.8.2
60+
krewlyzer: 0.8.3
6161
END_VERSIONS
6262
"""
6363
}

nextflow/modules/local/krewlyzer/fsr/main.nf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
process KREWLYZER_FSR {
1111
tag "$meta.id"
1212
label 'process_medium'
13-
container "ghcr.io/msk-access/krewlyzer:0.8.2"
13+
container "ghcr.io/msk-access/krewlyzer:0.8.3"
1414

1515
input:
1616
tuple val(meta), path(bed)
@@ -60,7 +60,7 @@ process KREWLYZER_FSR {
6060
6161
cat <<-END_VERSIONS > versions.yml
6262
"${task.process}":
63-
krewlyzer: 0.8.2
63+
krewlyzer: 0.8.3
6464
END_VERSIONS
6565
"""
6666
}

nextflow/modules/local/krewlyzer/mfsd/main.nf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
process KREWLYZER_MFSD {
1414
tag "$meta.id"
1515
label 'process_medium'
16-
container "ghcr.io/msk-access/krewlyzer:0.8.2"
16+
container "ghcr.io/msk-access/krewlyzer:0.8.3"
1717

1818
input:
1919
tuple val(meta), path(bam), path(bai), path(variants)
@@ -81,7 +81,7 @@ process KREWLYZER_MFSD {
8181
8282
cat <<-END_VERSIONS > versions.yml
8383
"${task.process}":
84-
krewlyzer: 0.8.2
84+
krewlyzer: 0.8.3
8585
END_VERSIONS
8686
"""
8787
}

0 commit comments

Comments
 (0)