Skip to content

fix(io): sync and check close of output files - #1047

Merged
nh13 merged 2 commits into
mainfrom
nh/output-close-errors
Oct 8, 2026
Merged

nh13 merged 2 commits into
mainfrom
nh/output-close-errors

Conversation

@nh13

@nh13 nh13 commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Builds on #1046 (merged). Two commits, each building and passing tests on its own.

1. fix(sort): fail when the I/O writer ends short of the blocks submitted

The sort's io_writer_loop already failed on a gap in the block serials it received, but a lost final block left no gap. If its compress job was abandoned (a worker panic, or the pool shutting down before the block was written), its result sender was dropped and the writer saw a clean end of input, so it stamped an EOF marker onto a truncated stream.

Block serials are now issued by the writer's PermitPool, which counts them (issue_serial). Once its input closes, the writer fails if it wrote fewer blocks than were issued. It also rejects any block whose serial the pool did not issue. A producer that bypasses the count therefore fails on its first block and can't silently disable the check. No production success path drops the pool early, so in practice this guards the worker-panic case.

2. fix(io): sync and check close of output files

Output files were finished by flushing and then dropping the File. On Unix File::flush is a no-op and dropping a File discards the result of close(2). Errors the OS reports only after the last write were lost, and the command exited 0 with a short or corrupt output. On NFS, ENOSPC/EDQUOT/EIO from flushing dirty pages first appear at close. On local filesystems a write-back EIO is reported only by fsync/fdatasync.

Every production output file is now finished with sync_data (regular files only) and then a checked close (nix::unistd::close; EINTR counts as success). This follows htslib's bgzf_close. Like htslib's fd_flush (hfile.c), a sync that fails with EINVAL, ENOTSUP or EOPNOTSUPP means "sync not supported" (e.g. F_FULLFSYNC on an SMB mount on macOS). ENOTTY is treated the same way, because macOS returns it from F_FULLFSYNC on filesystems with no handler for it; htslib calls plain fsync, so it doesn't see ENOTTY. In that case the sync is logged at debug and skipped, and the checked close still runs. Any other sync error fails.

Design

  • fgumi-bam-io gains OutputFile (sync + checked close; OutputFile::unsynced skips the sync), the OutputSink trait (Write + Send plus close(self: Box<Self>)), close_buffered, open_output_sink (stdout for -//dev/stdout, otherwise an OutputFile), and persist_after_close (close a temp, then rename it).
  • Stdout is a block-buffered duplicate of fd 1 (OutputFile::unsynced). It is flushed and its close is checked, but it is never synced, and fd 1 stays open. Pipes and /dev/null skip the sync but still get the checked close.
  • Public API change: fgumi_bam_io::open_output_writer now returns Box<dyn OutputSink> instead of Box<dyn Write + Send>. Callers should call close() when done; dropping still works but discards errors.
  • nix is now a dependency of fgumi-bam-io on all Unix targets, not just Linux.

Sites

  • BgzfWriterEnum::finish and IndexingBamWriter::finish.
  • WriteBgzfFile (every chain command's BAM output). The inline .bai is written only after the BAM closed cleanly.
  • WriteRawFile (fgumi fastq output) now opens through open_output_sink. - and /dev/stdout now get the same checked stdout close as the BAM sink; before, they used a flush-only io::stdout() or reopened /dev/stdout.
  • write_bai_index and the sort's MergeOutputTarget::persist both use persist_after_close, so the temp is closed before the rename and a failure leaves nothing at the destination. The merge temp is synced once, by its writer, not again before the rename.
  • Sort: PooledBamWriter closes its output. Spill chunks are still just dropped, because this process reads them back.
  • Simulate: FastqWriter (both arms), ParallelGzipWriter, and the truth/includelist TSVs. Close errors name the file.

Out of scope: spill and run files, telemetry outputs, and fgumi-metrics' write_metrics_atomic. That function already sync_alls before its rename, and sharing the helper would add a fgumi-metrics -> fgumi-bam-io dependency.

Cost

Local measurements (macOS, where sync_data is F_FULLFSYNC): 6-8 ms for 100 MB and 8-70 ms for 2 GiB. The checked close takes about 10 µs. This has not been measured on EBS yet. Before merging, the plan is one fgumi-benchmarks AWS core run on main (with #1046) and one on this branch.

Changes output for

None on success; output bytes are identical. Sync/close errors now fail the command (except unsupported sync), and a truncated sort output or spill chunk now fails the sort.

Tests

  • Sort: a lost final block fails (BGZF and zstd) and closes the permit pool; a complete out-of-order run passes; a serial the pool did not issue is rejected; StagingBuffer serials come from the pool.
  • output.rs:
    • The checked close surfaces EBADF for a descriptor closed underneath it, for both synced and unsynced files.
    • EINVAL/ENOTSUP/EOPNOTSUPP/ENOTTY from sync are skipped and the file is still closed, while EIO, ENOSPC, EDQUOT and EBADF stay fatal.
    • A sync error surfaces even when the close succeeds, and EINTR maps to success.
    • Only regular files are synced; /dev/null and pipes close cleanly.
    • open_output_sink handles both stdout spellings and creates files.
    • persist_after_close closes before renaming, and a failure leaves the destination untouched and removes the temp.
  • Close-before-rename: write_bai_index and MergeOutputTarget::persist with a failing close leave no output and no temp.
  • Failing-sink tests for every finish path: BgzfWriterEnum with 1 and 2 threads, IndexingBamWriter, WriteBgzfFile (with no .bai after a failed close), WriteRawFile, PooledBamWriter plain and indexing, ParallelGzipWriter, and FastqWriter single- and multi-threaded. Each checks that a close error surfaces, and that on success the sink is closed once with one EOF block.
  • The simulate close helper names the path in its error.
  • Byte-identity: BGZF output written through open_output_writer matches the same stream written in memory.
  • The EBADF tests close a raw descriptor, so they skip unless nextest's process-per-test mode is set.

Risk verdict: Command output: none to grouping, consensus, sort order, corrected UMIs, or metrics; reported byte-identity tests pin output bytes. unsafe: none added or modified; no CLAUDE.md allowlist update is indicated. Memory bounds, queue capacity, and thread/backpressure policy: none.

Fix: Detect missing final sort blocks and propagate output sync and close errors.

The supplied change summary reports tests for the new error paths and output identity. The shell output does not independently confirm the diff or test results.

@nh13
nh13 deployed to github-actions October 8, 2026 00:45 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: fulcrumgenomics/fgumi/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Essentials
  • Run ID: 9d5017b6-6db1-46c6-a642-b07a7645865e
📥 Commits

Reviewing files that changed from the base of the PR and between 40bab20 and 3fb5145.

📒 Files selected for processing (2)
  • Cargo.toml
  • crates/fgumi-bam-io/src/writer.rs

Included review availability: This review used your included allowance. 1 included review remains after this review. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.


Walkthrough

Output writers now use checked sinks that report flush, sync, and close failures. BAM, pipeline, simulation, and sorting paths use shared output handling. Sorting also tracks issued compression-job serials and detects missing or unissued results.

Changes

Output finalization and sort tracking

Layer / File(s) Summary
Output sink contract and persistence
Cargo.toml, crates/fgumi-bam-io/Cargo.toml, crates/fgumi-bam-io/src/output.rs, crates/fgumi-bam-io/src/lib.rs
Adds OutputSink, synced OutputFile, buffered close, shared path opening, and close-before-rename persistence.
BAM and pipeline writer finalization
crates/fgumi-bam-io/src/writer.rs, crates/fgumi-pipeline-io/src/sink/*
Writers close sinks after stream finalization. Close failures propagate, and BAM index creation stops when output close fails.
Simulation output finalization
src/lib/commands/simulate/*, src/lib/simulate/*
Truth outputs and FASTQ writers use checked sinks. Finalization errors include the output path.
Sort result tracking and output persistence
crates/fgumi-sort/src/bgzf_io.rs, crates/fgumi-sort/src/worker_pool.rs, crates/fgumi-sort/src/pooled_*_writer.rs, crates/fgumi-sort/src/external.rs
PermitPool issues compression serials. The I/O loop rejects unissued serials and detects missing final results. Sort outputs close before completion, and staged merge files close before rename.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~50 minutes

Change: Bug fix

Suggested labels: fgumi sort

Merge Risk: ⚪ Minimal · up to 3fb51

No actionable merge-blocking risk is established; the checked output-finalization paths are consistent with the intended behavior.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses valid Conventional Commit syntax, has the allowed fix type, uses a relevant io scope, and describes the output-file sync and close-error changes. The description is lowercase, imper…
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.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@nh13

nh13 commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

@codecov

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.17791% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.55%. Comparing base (94577dc) to head (3fb5145).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/fgumi-bam-io/src/output.rs 97.04% 9 Missing ⚠️
crates/fgumi-pipeline-io/src/sink/mod.rs 86.95% 3 Missing ⚠️
src/lib/simulate/parallel_gzip_writer.rs 91.66% 3 Missing ⚠️
crates/fgumi-pipeline-io/src/sink/write_raw.rs 95.55% 2 Missing ⚠️
src/lib/commands/simulate/consensus_reads.rs 33.33% 2 Missing ⚠️
src/lib/commands/simulate/correct_reads.rs 50.00% 2 Missing ⚠️
crates/fgumi-bam-io/src/writer.rs 99.14% 1 Missing ⚠️
crates/fgumi-sort/src/pooled_bam_writer.rs 97.67% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1047      +/-   ##
==========================================
- Coverage   96.55%   96.55%   -0.01%     
==========================================
  Files         300      303       +3     
  Lines      153875   154588     +713     
==========================================
+ Hits       148577   149255     +678     
- Misses       5298     5333      +35     

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

Base automatically changed from nh/bgzf-finish-propagates-eof-error to main October 8, 2026 01:09
@nh13

nh13 commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 8, 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.

nh13 added 2 commits October 7, 2026 21:58
The pooled writers' io_writer_loop fails on a gap in the block serials it
receives, but a lost final block leaves no gap: when its compress job is
abandoned (a pool shutdown while the block is queued or compressing, or a
worker panicking mid-compress) its result sender is dropped and the writer
sees a clean end of input, so it stamped an EOF marker onto a truncated
stream.

Block serials are now issued by the writer's PermitPool, which counts
them. Once its input closes, the writer fails if it wrote fewer blocks
than were issued. It also rejects a block whose serial the pool did not
issue, so a producer that bypasses the count fails on its first block
instead of silently disabling the check.

Changes output for: none on success; a truncated sort output or spill
chunk now fails the command.
Output files were flushed and then dropped. On Unix File::flush is a
no-op and dropping a File discards the close(2) result, so errors
reported only after the last write (write-back EIO, or ENOSPC/EDQUOT
flushed at close on NFS) were lost and the command exited 0.

Add OutputFile and the OutputSink trait to fgumi-bam-io. Closing an
OutputFile syncs its data (regular files only) and closes it with a
checked nix::unistd::close (EINTR counts as success). As in htslib, a
sync failing with EINVAL, ENOTSUP or EOPNOTSUPP is logged and skipped,
as is ENOTTY (macOS F_FULLFSYNC on a filesystem without it); any other
sync error fails. Stdout is a duplicated descriptor whose close
is checked but which is not synced.

Every production output now finishes this way: BAM writers
(BgzfWriterEnum, IndexingBamWriter), the WriteBgzfFile/WriteRawFile
pipeline sinks (WriteRawFile now opens - and /dev/stdout as the BAM
sink does), the sort output writer, and the simulate FASTQ and TSV
writers (close errors name the file). write_bai_index and the sort
merge output close their temp before the rename, through a shared
persist_after_close; the merge temp is not synced twice. Spill files,
which this process reads back, are unchanged.

Public API change: fgumi_bam_io::open_output_writer now returns
Box<dyn OutputSink> instead of Box<dyn Write + Send>; callers should
call close() when done. New: OutputFile, OutputSink, close_buffered,
open_output_sink, persist_after_close. nix is now a dependency of
fgumi-bam-io on all Unix targets, not only Linux.

Changes output for: none on success; sync/close errors now fail the
command (except unsupported sync).
@nh13
nh13 force-pushed the nh/output-close-errors branch from 40bab20 to 3fb5145 Compare October 8, 2026 05:00
@nh13
nh13 deployed to github-actions October 8, 2026 05:00 — with GitHub Actions Active
@nh13

nh13 commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 8, 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.

@nh13
nh13 added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 51c1fb9 Oct 8, 2026
21 checks passed
@nh13
nh13 deleted the nh/output-close-errors branch October 8, 2026 16:01

This branch was successfully deployed

1 active deployment
github-actions — 3fb5145c Deployed Oct 8, 2026 by nh13 via coverage #4912
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.

1 participant