From 9e2e7ebaeba1e575a979ffab8d45b6637bfeb58f Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Tue, 14 Jul 2026 13:39:41 -0500 Subject: [PATCH] Include module resources bundle in task hash for -resume caching When a module uses local binaries via its `resources/` directory (the `nextflow.enable.moduleBinaries` feature flag), those files were not part of the task hash used for `-resume` caching. As a result, editing a module's `resources` script did not invalidate the cache and `-resume` returned a stale cached result instead of re-executing the task. Add the module resources bundle fingerprint to the task hash key list in `TaskHasher.compute()`, guarded by `session.enableModuleBinaries()` (matching the existing guard in `TaskProcessor.getBinDirs()`), so edits to module binaries correctly force task re-execution while unchanged resources still hit the cache. Note: this does not affect Wave setups, where the resources are baked into the container and already covered by the container fingerprint. Fixes #6128 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Ben Sherman --- .../nextflow/processor/TaskHasher.groovy | 9 +++++ .../nextflow/processor/TaskHasherTest.groovy | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskHasher.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskHasher.groovy index 723488a6de..478157dbaa 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskHasher.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskHasher.groovy @@ -96,6 +96,15 @@ class TaskHasher { keys.addAll(binEntries) } + // add the fingerprint of the module resources bundle (e.g. module binaries) + // since these files are not staged individually like the project bin dir + final moduleBundle = session.enableModuleBinaries() ? processor.getModuleBundle() : null + if( moduleBundle && moduleBundle.hasEntries() ) { + final fingerprint = moduleBundle.fingerprint() + log.trace "Task: ${task.processor.name} > Adding module bundle fingerprint: ${fingerprint}" + keys.add(fingerprint) + } + // add environment modules (`module` directive) final modules = task.getConfig().getModule() if( modules ) { diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskHasherTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskHasherTest.groovy index 4e7461205e..913646c047 100644 --- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskHasherTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskHasherTest.groovy @@ -20,6 +20,7 @@ import java.nio.file.Path import nextflow.Session import nextflow.script.ProcessConfig +import nextflow.script.bundle.ResourcesBundle import spock.lang.Specification /** * @@ -94,6 +95,44 @@ class TaskHasherTest extends Specification { result.contains(Path.of('/some/path/bar.sh')) } + def 'should include module bundle fingerprint in the task hash' () { + + given: 'two module bundles with the same fingerprint, then one with a different fingerprint' + def bundleA1 = Mock(ResourcesBundle) { asBoolean() >> true; hasEntries() >> true; fingerprint() >> 'aaaaaaaa' } + def bundleA2 = Mock(ResourcesBundle) { asBoolean() >> true; hasEntries() >> true; fingerprint() >> 'aaaaaaaa' } + def bundleB = Mock(ResourcesBundle) { asBoolean() >> true; hasEntries() >> true; fingerprint() >> 'bbbbbbbb' } + and: 'a task whose process resolves those bundles across successive hash computations' + def session = Mock(Session) { + getUniqueId() >> UUID.fromString('b69b6eeb-b332-4d2c-9957-c291b15f498c') + getBinEntries() >> [:] + enableModuleBinaries() >> true + } + def processor = Mock(TaskProcessor) { + getName() >> 'hello' + getSession() >> session + getConfig() >> Mock(ProcessConfig) + getModuleBundle() >>> [bundleA1, bundleA2, bundleB] + } + def task = Mock(TaskRun) { + getSource() >> 'hello world' + isContainerEnabled() >> false + getConfig() >> Mock(TaskConfig) + getProcessor() >> processor + } + def hasher = Spy(new TaskHasher(task)) + hasher.getTaskGlobalVars() >> [:] + + when: + def hashA1 = hasher.compute() + def hashA2 = hasher.compute() + def hashB = hasher.compute() + + then: 'the same bundle fingerprint yields the same hash' + hashA1 == hashA2 + and: 'a different bundle fingerprint invalidates the cache' + hashA1 != hashB + } + def 'should get task directive vars' () { given: def processor = Spy(TaskProcessor) {