Conversation
Container images are resolved during task hash computation, before the task reaches the executor. A ContainerResolver that fails with a plain unchecked exception (e.g. nf-wave's BadResponseException, which extends RuntimeException) reaches TaskProcessor.resumeOrDie unwrapped, where the 'error instanceof ProcessException' guard rejects it. checkErrorStrategy() is never entered, so the configured errorStrategy is not consulted at all and 'finish' is silently downgraded to 'terminate', killing in-flight tasks. Normalise resolver failures to ProcessUnrecoverableException at the single call site in TaskRun so the usual classification runs: 'retry' and 'ignore' are still escalated to 'terminate' because the failure is unrecoverable, while 'finish' is honoured. Failures that already carry process semantics pass through untouched, and a checked exception rethrown by a proxied resolver as UndeclaredThrowableException is unwrapped first. Fixes nextflow-io#7446 Signed-off-by: Mohit Ak <Mohit-Ak@users.noreply.github.com>
✅ Deploy Preview for nextflow-docs canceled.
|
Mohit-Ak
marked this pull request as ready for review
August 28, 2026 12:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Container resolution runs during task hash computation, on a dataflow operator thread, well before the task ever reaches the executor. When a
ContainerResolverfails there with a plain unchecked exception, the error arrives atTaskProcessor.resumeOrDie()unwrapped — and the guard there is:nf-wave'sBadResponseExceptionextendsRuntimeException, notProcessException, so the guard fails,checkErrorStrategy()is never entered, anderrorStrategykeeps its initial value ofTERMINATE. The configured strategy is not consulted at all, and nothing is logged to say so.The user-visible consequence is that
finishis silently downgraded toterminate. That distinction matters:finishsays nothing about the failed task, it governs what happens to everything else already running. Withterminate, unrelated in-flight jobs get killed because one process had an unreachable image.Worth noting the same plugin already gets this right on its asynchronous path —
WaveClient.checkContainerCompletion()throwsProcessUnrecoverableException, which is aProcessException, so it reachescheckErrorStrategy()and behaves correctly. Two code paths, the same logical failure, different outcomes decided by which exception class happens to be thrown.What this changes
I fixed it in core rather than in
nf-wave, at the single point where any resolver is invoked —TaskRun.containerInfo0(). That coversDefaultContainerResolver,WaveContainerResolver, and any third-party plugin resolver, rather than patching one throw site in one plugin and leaving the same shape live elsewhere.resolveImage0()wraps the resolver call and normalises failures toProcessUnrecoverableException, which is exactly the classification this failure deserves.checkErrorStrategy()then does the rest with no further changes:retryandignoreare soft, so they still escalate toTERMINATE— unchanged, and deliberately so; retrying an unresolvable image is pointless.finishandterminateare hard, so they pass through as configured. That is the whole fix.Two details in the implementation:
ProcessExceptionand subclasses) are rethrown untouched, so the async Wave path and anything else already doing the right thing is unaffected.UndeclaredThrowableException; that gets unwrapped first so the real cause is what's classified and reported. I hit this while writing the tests, not in theory.I did not widen the
resumeOrDie()guard itself (option 2 in the issue) — it has a much larger blast radius over every error path in the processor, and the resolver call site gets the same coverage for the failure class actually at hand.Testing
Three cases added to
TaskRunTest, covering the reported failure and the two boundaries around it: an unchecked resolver failure is wrapped, an already-ProcessExceptionfailure is not re-wrapped, and ajava.lang.Errorstill propagates untouched rather than being swallowed into a process exception.RED check first — with the tests in place and the fix stashed:
That first failure is the bug: the plain
RuntimeExceptionescapes unclassified, which is precisely what makesresumeOrDie()skip the strategy check.Then the full module suite,
./gradlew :nextflow:test:master(2e13110): 4217 tests, 0 failures, 0 errors, 89 skippedDelta is exactly the three added tests, so nothing was broken, silently skipped, or deselected.
What I could not verify end-to-end is the reproducer from the issue, since it needs a live Wave endpoint and an AWS Batch executor. The unit tests pin the exception classification, which is the actual defect; the behavioural consequence for
finishfollows fromcheckErrorStrategy(), which is unchanged and already covered by the existing suite.Fixes #7446