fix(core): batch-safe hashing for maven and gradle#34446
fix(core): batch-safe hashing for maven and gradle#34446FrozenPandaz merged 22 commits intomasterfrom
Conversation
|
View your CI Pipeline Execution ↗ for commit e7a9491
☁️ Nx Cloud last updated this comment at |
cf36e18 to
71d9c98
Compare
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
ef9a35e to
92f349b
Compare
940564b to
b74c720
Compare
ebc28bc to
a270277
Compare
fd6015c to
1248e72
Compare
c7c71a0 to
a0a0b8e
Compare
cefe089 to
16b112e
Compare
2a37451 to
671f050
Compare
When tasks with dependentTasksOutputFiles are co-batched with their dependencies, hashes are computed using outputs from a previous run that may be stale. This adds a validation step that checks whether dependency outputs on disk match the dependency's current hash, skips cache reads for untrustworthy hashes, and re-hashes after the batch completes with fresh outputs.
With output fingerprinting enabled for all cache operations (not just batch mode), tasks whose outputs are already on disk now correctly report "existing outputs match the cache" instead of "local cache". Updated e2e assertions across cache, run, ng-add, and nx-init-angular tests to expect the new status when outputs haven't been deleted.
Wrap identifyTasksWithStaleDepsOutputs and getInputs in try-catch so that targets without proper input configuration (e.g. inferred maven targets) don't crash the entire batch execution.
hashTasks filters out tasks that already have a hash. The previous code cleared hashes on the result copies (created by runBatch via spread) but called hashTasks on batch.taskGraph which holds the originals — still with their hashes. This caused hashTasks to skip them entirely, leaving the copies with undefined hashes that crashed napi when passed to cache.put. Clear hashes on the originals so hashTasks picks them up, then sync the fresh hashes back to the result copies.
The testCompile target hash was missing src/test/java/**/*.java because CacheConfig used the wrong parameter name for the compiler plugin's test source roots. Also removes an unnecessary fallback in MavenExpressionResolver that masked the issue.
When Maven task inputs reference gitignored paths (like target/classes), they were being converted to dependent task outputs using the directory path itself. This caused pattern matching issues because: 1. Outputs like lib:compile produce: lib/target/classes/Foo.class 2. Pattern was: "target/classes" (just the directory) 3. We need: "**/*" (to match files within that directory) Changes: - MojoAnalyzer: Use parameter's glob pattern (default "**/*") for gitignored inputs - NxTargetFactory: Use "nx-build-state.json" pattern for gitignored build state - create-maven-project: Check if .gitignore exists before reading This ensures dependent task outputs correctly match files from dependency outputs.
The batch executor was always resolving the promise even when the batch runner JAR exited with a non-zero code. This meant that test failures were not being propagated back to the CLI. Now properly rejects the promise when exitCode !== 0, ensuring that: - Failed tests cause the nx command to throw an error - The test output is captured and displayed - The overall command exits with the correct exit code
When a batch task has stale dependency outputs, only that task was marked stale. Transitive dependents (e.g. package depends on compile via noop phases) were incorrectly served from cache and removed from the batch graph, breaking the dependency chain ordering.
…Config Gitignored input paths (e.g. target/classes, target/test-classes) are already auto-detected by MojoAnalyzer and converted to dependentTasksOutputFiles. The explicit entries were duplicating this.
Hashing now happens topologically so tasks are always hashed against fresh dependency outputs. This removes the stale detection, transitive propagation, and post-batch re-hashing machinery which is no longer needed.
Output fingerprinting (daemon-free OutputFingerprints Rust service and hashTaskOutput napi binding) is not related to batch processing. Revert to the daemon-only shouldCopyOutputsFromCache path that exists on master.
Batch tasks with depsOutputs inputs (e.g. app:compile depending on lib:compile outputs) were getting incorrect hashes because all tasks were hashed upfront before any ran, meaning dependency outputs didn't exist on disk yet. Replace the flat hash-all approach with three phases: 1. Topological cache resolution - walk batch graph level by level, hash roots, check cache, restore outputs before hashing dependents 2. Run remaining uncached tasks via batch executor 3. Re-hash tasks with depsOutputs after batch completes so postRunSteps caches under the correct hash
The DependentTaskOutput class and dependentTaskOutputs field on MojoConfig were never populated by any configuration, making the forEach loop in MojoAnalyzer dead code.
- Extract hashBatchTasks() helper to eliminate 3 duplicate hashTask call sites - Remove unused hashTasks import - Rename ranInBatch to batchTaskIds for clarity - Parallelize Phase 3 re-hashing instead of sequential awaits - Pre-filter tasks before calling getInputs() to avoid unnecessary config parsing
Remove pre-execution hashing (Phase 2) and post-execution re-hashing (Phase 3). Instead, hash all batch tasks once after execution when outputs are fresh on disk. Tasks with depsOutputs get correct hashes on the first pass since sibling outputs already exist.
671f050 to
0d1dc1c
Compare
Task history lifecycles were snapshotting task.hash eagerly in endTasks, but batch tasks haven't been hashed yet at streaming time. This caused the Rust native layer to crash with "Missing field hash". Instead of hashing mid-batch, store TaskResult references in endTasks and build TaskRun objects lazily in endCommand when task.hash is guaranteed to be set by the post-batch re-hash. Also use original task references instead of spread copies so hash mutations flow through.
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud has identified a possible root cause for your failed CI:
Our test suite encountered a heap out of memory error during nx:test execution. This failure has a 7.49% historical flakiness rate and appears to be an environmental resource constraint rather than a code regression. We should increase the Node.js heap size for CI (NODE_OPTIONS=--max-old-space-size=4096) or optimize test suite chunking to address this recurring issue.
No code changes were suggested for this issue.
🔂 A CI rerun has been triggered by adding an empty commit to this branch.
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
🎓 Learn more about Self-Healing CI on nx.dev
9bc40b5 to
bc02ab8
Compare
Instead of breaking when zero cache hits occur at a level, walk the entire graph and partition tasks into cache-eligible vs ineligible. Tasks with depsOutputs whose dependencies were not cached are skipped (their dep outputs aren't on disk), while all other tasks are still checked against cache at every level.
bc02ab8 to
e7a9491
Compare
## Current Behavior After #34446, batch tasks with `depsOutputs` inputs had their hashing deferred until after execution. This meant the streaming `endTasks` callback fired with `task.hash = undefined`, which Cloud/DTE rejects. ## Expected Behavior All batch tasks always have a valid hash when `endTasks` is called. Tasks with `depsOutputs` get a preliminary hash upfront (based on whatever outputs are on disk), then are re-hashed after execution with fresh outputs for correct cache storage. ### How it works 1. **Phase 1** now hashes ALL root tasks at each level (not just cache-eligible ones). Ineligible tasks get a preliminary hash so the streaming callback always has something valid to send. 2. **Phase 2** runs the batch, then clears and re-hashes all tasks that ran — outputs are fresh on disk, so depsOutputs tasks get correct final hashes. 3. The re-hash logic is consolidated into a single block after both code paths (cache-enabled and cache-skipped). ## Related Issue(s) Fixes the undefined hash regression from #34446 --------- Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com> Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
|
This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request. |
Current Behavior
In batch mode (Maven/Gradle), all task hashes are computed upfront in
processScheduledBatchbefore the batch executor runs any tasks. Tasks withdependentTasksOutputFiles(akadepsOutputs) get hashed using whatever dependency outputs happen to be on disk from a previous run. This leads to:Non-batch mode doesn't have this problem because it uses lazy hashing — tasks with
depsOutputsare only hashed after their dependencies complete and fresh outputs exist on disk.Expected Behavior
Batch mode hashes tasks topologically — each task is hashed only after its dependencies have run and their outputs are on disk. This means hashes are always computed against fresh outputs, eliminating both false cache hits and false cache misses.
How it works
applyFromCacheOrRunBatchnow has two phases:Topological cache resolution — Walk the entire batch task graph level by level. At each level, partition root tasks into cache-eligible vs ineligible. A task is ineligible for cache if it has
depsOutputsinputs AND any of its dependencies were not cached (their outputs aren't on disk, so the hash would be wrong). Hash and check cache for eligible tasks, then remove all roots from the graph to expose the next level — even when some tasks are cache misses. This ensures the walk continues past cache misses to find deeper cache hits.Run remaining tasks, then hash — Rebuild a run graph from all non-cached task IDs and run them through the batch executor. After the batch completes, hash all tasks that ran. Since all outputs (including from sibling batch tasks) are now fresh on disk, tasks with
depsOutputsget correct hashes on the first pass — no re-hash needed.Task history lifecycle fix
The batch streaming callback calls
endTasksas tasks finish mid-batch, but tasks haven't been hashed yet at that point (hash is deferred to post-execution). Previously,TaskHistoryLifeCycleandLegacyTaskHistoryLifeCycleeagerly snapshottedtask.hashinendTasks, which sentundefinedto the native Rust layer causing a "Missing fieldhash" crash.Fix: Both lifecycles now store
TaskResultreferences inendTasksand defer buildingTaskRunobjects untilendCommand, whentask.hashis guaranteed to be set by the post-batch re-hash. The streaming callback andrunBatchreturn value also now use the original task object reference (instead of spread copies) so that the hash mutation fromhashBatchTasksflows through to all stored references.Example: 3 tasks over 3 runs
Consider a batch with three tasks in a linear chain: A → B → C
lib:compile. Inputs: source files only. NodepsOutputs.app:compile. Depends on A. Inputs: onlydepsOutputsfrom Task A (e.g.,target/classes/**). No source file inputs.app:checkstyle. Depends on B. Not cacheable.Hash notation:
H(inputs…)means the hash is a function of those inputs.Run 1 — Fresh (no outputs on disk, empty cache)
[A](B depends on A, C depends on B). Hash A → H_AnonCachedTaskIds. Remove all roots.[B]. B hasdepsOutputsand A is non-cached → B is ineligible. Added tononCachedTaskIds. Remove all roots.[C]. C has nodepsOutputs→ eligible. Hash C → H_C. Cache miss. Added tononCachedTaskIds. Remove all roots.nonCachedTaskIds= {A, B, C}. Run batch: all 3 tasks execute. A producestarget/classes/.H(A_outputs)= H_B, C → H_CRun 2 — Warm (nothing changed, cache populated from Run 1)
[A]. Hash A → H_Atarget/classes/to disk.[B]. B hasdepsOutputsbut A is cached (not innonCachedTaskIds) → B is eligible. Hash B →H(A_outputs)= H_B (A's outputs just restored!)[C]. Hash C → H_C. Not cacheable → no hit. Added tononCachedTaskIds.nonCachedTaskIds= {C}. Run batch with just C. Hash C post-execution.Run 3 — Source changed (A's source modified, stale outputs from Run 2 still on disk)
[A]. Hash A →H(A_src')= H_A' (new hash!)nonCachedTaskIds.[B]. B hasdepsOutputsand A is non-cached → B is ineligible. Added tononCachedTaskIds.[C]. C has nodepsOutputs→ eligible. Hash C → H_C. Cache miss. Added tononCachedTaskIds.H(A_new_outputs)= H_B', C → H_CSummary of hashes across runs
Maven plugin fixes
Several fixes to the Maven plugin to ensure correct batch behavior:
depsOutputspatterns liketarget/classesare now resolved using glob patterns, fixing issues with.gitignored output directories.maven:test: Test task inputs now correctly include test source files so hash changes when tests are modified.testCompiletask hash: ThetestCompiletarget now includessrc/test/javain its inputs.Related Issue(s)
Related to #30949