Skip to content

fix(clustalo/align): wait for compression and propagate pigz failures - #12894

Open
dnncha wants to merge 1 commit into
nf-core:masterfrom
dnncha:fix/clustalo-wait-for-compression
Open

dnncha wants to merge 1 commit into
nf-core:masterfrom
dnncha:fix/clustalo-wait-for-compression

Conversation

@dnncha

@dnncha dnncha commented Sep 6, 2026

Copy link
Copy Markdown

Problem

The compressed output expression uses clustalo ... -o >(pigz ... > result.aln.gz). Bash does not wait for that process substitution as part of the foreground command, and the compressor's status is not included in the Clustal Omega exit status, even with set -euo pipefail.

This can report task success before compression finishes, or after a compressor failure when the producer has already successfully written its buffered output. The .aln.gz file may exist but be incomplete or invalid.

Fix

Append && wait $! to the compressed-output command, with the dollar sign escaped in the Groovy string. The producer's failure is preserved by &&; after producer success, Bash waits for the compressor and returns its failure status. This keeps streaming, avoids an intermediate uncompressed file, and preserves the separation of alignment data from verbose stdout. The uncompressed branch and existing nf-test snapshots are unchanged.

Reproduction and tests

bash -euo pipefail -c 'printf alignment > >(cat >/dev/null; exit 42)'
# exit 0 before
bash -euo pipefail -c 'printf alignment > >(cat >/dev/null; exit 42) && wait $!'
# exit 42 after

Added a dependency-free regression test that extracts the actual module output expression and runs it in Bash with stand-in executables:

python modules/nf-core/clustalo/align/tests/test_compression.py -v

Five cases cover delayed compression completion, compressor failure after draining all input (no SIGPIPE assumption), producer failure, uncompressed output, and separation of stdout progress messages from the compressed alignment. Executed against the pinned original and patched source: 2 failures before; all 5 pass after.

These are shell lifecycle tests, not an alignment-engine or full Nextflow validation. Nextflow/nf-test and the native tools are unavailable in this execution environment; the existing native integration tests still need CI. The standalone regression is run with the command above and is not claimed to be automatically wired into nf-test.

Prepared with AI assistance. Based on upstream commit 781f2625386fe0bf2f337c7b99f56e618c59fc99.

Preserve streaming output and separate verbose stdout. Add five standalone
shell lifecycle regression tests without changing alignment options.

@SPPearce SPPearce left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels complicated.
Why does this tool not just make the output file and then gzip it?

@dnncha

dnncha commented Sep 9, 2026

Copy link
Copy Markdown
Author

That would be simpler, and seems a reasonable approach. I kept the existing streaming setup to make the correction small and avoid writing an intermediate uncompressed alignment. The problem is that the foreground command can finish successfully while compression is still running, or after the compressor has failed.

Writing the alignment first and then running pigz would also address that, provided either failure makes the task fail. I’m happy to revise in that direction if that is your preference. I would retain the failure checks and verify the revised module through Nextflow/nf-test; the current five checks use shell stand-ins, so they do not establish native workflow integration.

@SPPearce

SPPearce commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@lrauschning , @erikrikarddaniel , do you have any input on this? I don't know why it is being done in this way.

@erikrikarddaniel

Copy link
Copy Markdown
Member

@lrauschning , @erikrikarddaniel , do you have any input on this? I don't know why it is being done in this way.

No idea. I have no recollection of implementing this and have nothing against doing it the simple way, i.e. gzip after creation of the alignment file. That won't leave an intermediate which would be my only concern in general.

@SPPearce

SPPearce commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Ok, let's do that, make the file and then gzip it if required, as we generally do. Much less error prone.
Don't need that python test at that point.

@lrauschning

Copy link
Copy Markdown
Contributor

Hi @SPPearce,

I added this to the clustalo (and if memory serves, also to other MSA modules that don't support writing to stdout), as uncompressed MSA output can get very large due to gaps -- essentially, they scale with O(n^2) for diverse sequences. Writing uncompressed output to disk was both using a lot of scratch space and becoming a performance bottlenecks in some tests I ran.
I can't comment on the pytest as I've never seen an nfcore module use anything like it, but the && wait $! solution seems alright to me.

Cheers,
Leon

@dnncha

dnncha commented Sep 12, 2026

Copy link
Copy Markdown
Author

Thanks Leon, that explains the streaming choice. Given the scratch-space cost you’ve seen, @SPPearce, do you still prefer writing the alignment first, or keeping streaming with the explicit wait? I’m happy with either approach provided completion and compressor failures are handled. I’ll follow the module’s usual test setup.

@SPPearce

Copy link
Copy Markdown
Contributor

Ok, if there is a significant reason not to write the intermediate file that is fine.
I don't think we need the python test here though (or even have a way to run it at the moment).
Can you please join the nf-core community via the github-invitations channel on the nf-core slack, to run the tests.

@erikrikarddaniel

Copy link
Copy Markdown
Member

Corroboration from the other end, in case it is useful: we hit this on real data in nf-core/phyloplace while turning clustalo/align's compress option on, before finding this PR.

  • Every .aln.gz above roughly 120 KB came out truncated — gzip -tunexpected end of file. The ~8 KB outputs won the race, which is exactly why the minimal test profiles stayed green and nothing looked wrong.
  • The consequence downstream is worse than a mis-reported exit status. EPA-NG hangs on a short gzip stream rather than failing, so a task sat for its full one-hour time limit and was killed with exit 143. That reads like a stalled executor, not corrupt data — it took reading .exitcode and the file's gzip -t to find the real cause.

So this is a silent-corruption bug with a downstream reader that blocks indefinitely, not only an error-propagation one. That seems worth having in the PR description.

I also checked && wait $! in this module's own container (bash 5.2.15, mulled-v2-4cefc38542f86c17596c29b35a059de10387c6a7):

$ printf alignment > >(cat >/dev/null; exit 42)
exit=0
$ printf alignment > >(cat >/dev/null; exit 42) && wait $!
exit=42

and with a deliberately slow writer, wait does block until the file is complete — so it fixes the truncation as well as the status. Note it relies on bash setting $! for a process substitution, which is bash 5.1+; fine for the pinned container, possibly worth a thought for the conda profile on older systems.

@lrauschning's scratch-space point convinced me, for what it is worth — I had independently gone the write-then-compress route in #12991 and am dropping or converting that part to follow whatever lands here.

One thing that may be out of scope for this PR: the same >(pigz ...) idiom is in kalign/align, magus/align and muscle5/super5, so all three have the same latent truncation. Happy to apply whichever form is settled on here to those three in a follow-up, rather than guessing now.

Investigated with AI assistance (Claude Code); the truncation, the container bash behaviour and the EPA-NG hang were all measured rather than inferred.

erikrikarddaniel added a commit to erikrikarddaniel/nf-core-modules that referenced this pull request Sep 18, 2026
nf-core#12894 fixes the same truncation there, predates this PR, and already has
maintainer direction: keep streaming through the process substitution and
add `&& wait $!`, rather than writing an uncompressed intermediate, because
uncompressed MSA output scales with gaps and costs real scratch space.

Carrying a competing change here would just duplicate it. The remaining four
modules are about reading compressed input and stand on their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpvh3T7aVgU5XyjqcYQLQD
erikrikarddaniel added a commit to erikrikarddaniel/phyloplace that referenced this pull request Sep 18, 2026
That PR fixes the truncated .gz independently and predates ours, and its
approach was settled with the module's author: keep streaming through the
process substitution and add `&& wait $!`, rather than writing an
uncompressed intermediate, because uncompressed MSA output scales with gaps
and costs real scratch space.

This patch is now byte-identical to what is proposed there, so it will
disappear cleanly at the next `nf-core modules update` instead of
conflicting with it.

Verified on the data that exposed the bug: all 21 *.aln.gz across the suite
pass `gzip -t`, where previously everything above roughly 120 KB was
truncated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpvh3T7aVgU5XyjqcYQLQD
@SPPearce

Copy link
Copy Markdown
Contributor

@dnncha , can you please join the organisation via the github-invitations channel on the nf-core slack, so your tests run.

@SPPearce SPPearce left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm happy to merge this pattern without the python script.

erikrikarddaniel added a commit to erikrikarddaniel/nf-core-modules that referenced this pull request Sep 21, 2026
New compress_alignment take: input is passed to CLUSTALO_ALIGN and
MAFFT_ALIGN, so callers can have the profile alignments written gzipped.
Uncompressed alignments are mostly gaps and get large. The subworkflow
also emits the EPA-NG log and the hmmbuild output.

clustalo/align: wait for the pigz process substitution before exiting.
The shell does not wait for >(...), so the task could finish before pigz
had flushed, leaving a truncated .aln.gz (seen for outputs above ~120 KB,
which made EPA-NG hang on the short stream). The fix is the `&& wait $!`
from nf-core#12894, without its separate Python test.

Callers must now pass compress_alignment; false keeps the old behaviour.

Co-Authored-By: dnncha <5190258+dnncha@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqGkxDi4vYRXZJxVqkAcwU
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.

4 participants