Conversation
…ta is collected AbstractDependencyManager.deriveChildManager() now returns `this` when no new management data (versions, scopes, optionals, local paths, exclusions) was collected at the current depth AND management is already being applied (depth >= applyFrom). This avoids creating distinct-but-semantically-equal DependencyManager instances that defeat the BfDependencyCollector pool cache — the pool key includes the manager, so unique managers cause pool misses, which lets the skipper prune subtrees that should have been served from the cache. The isApplied() guard is necessary: at depth < applyFrom, returning `this` would freeze the depth counter and prevent management rules from ever being applied to descendants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gnodet
left a comment
There was a problem hiding this comment.
Clean, well-targeted fix for the cache-transparency bug described in #2013. The optimization to reuse the DependencyManager instance when no new management data is collected is semantically correct — it's placed at exactly the right point in deriveChildManager (after all management-data collection but before newInstance()), and the isApplied() guard prevents freezing depth below the applyFrom threshold.
A couple of minor, non-blocking observations:
- The unit test only covers
TransitiveDependencyManager.DefaultDependencyManager(applyFrom=0) also benefits from the optimization and could be tested with a similarassertSame/assertNotSamepattern. The existingtestDefaulttest indirectly covers management correctness, so this is a minor coverage gap. - An additional test where depth-1 context contributes management data (making
this.managedVersionsnon-null), then the next depth has an empty context (optimization fires), and a subsequent depth has management data for the same key would explicitly document the "nearer-to-root wins" invariant — analysis ofgetManagedVersionconfirms it iterates the path from root (first match wins), so this is functionally correct already.
Thread safety is fine: the optimization returns this without mutation, and newInstance creates new ArrayList copies of this.path, so concurrent calls from different BF collector branches are safe.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
…tests Add two tests suggested by review on PR #2014: 1. testDeriveChildManagerReusesInstanceWithDefaultManager — verifies the optimization also works with DefaultDependencyManager (applyFrom=0), where instance reuse fires at the very first derivation. 2. testNearerToRootWinsAfterOptimizationReusesInstance — documents that the "nearer-to-root wins" invariant holds after the optimization reuses an instance: root's version management at depth 1 still wins when a deeper POM tries to override the same key, because containsManagedVersion finds it in ancestors and skips collection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for the review! Both suggestions addressed in 0802960:
|
gnodet
left a comment
There was a problem hiding this comment.
Re-review after new commit (0802960).
The new commit cleanly addresses both suggestions from the previous review:
-
DefaultDependencyManager coverage — new test verifies the optimization fires at the very first derivation (depth 0→1) since
applyFrom=0makesisApplied()true immediately. TheassertSame/assertNotSameassertions are accurate. -
Nearer-to-root-wins invariant test — verifies that when a depth-1 context contributes management data and the next depth has an empty context (optimization reuses
this), a subsequent depth with management data for the same key correctly inherits the nearer-to-root value. The assertion chain throughcontainsManagedVersion→ skip local collection → all five managed variables stay null is sound.
Test coverage for the optimization is now comprehensive across both manager implementations, with and without management data, and across the applyFrom boundary. No issues found.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes #2013 by making AbstractDependencyManager.deriveChildManager() reuse the same DependencyManager instance when no new management data is introduced (and management is already applied), preventing BF collector pool-cache key churn and restoring cache transparency.
Changes:
- Added an instance-reuse fast path in
AbstractDependencyManager.deriveChildManager()when derivation is a semantic no-op at applied depths. - Added unit tests validating reuse behavior across
TransitiveDependencyManagerandDefaultDependencyManager, plus a “nearer-to-root wins” invariant test. - Added an integration test scenario (with new artifact-description fixtures) reproducing the BF pool-cache transparency issue and asserting consistent graph structure.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java | Reuses manager instances for no-op derivations at applied depths to avoid BF pool-cache misses. |
| maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java | Adds unit tests for instance-reuse behavior and precedence invariants. |
| maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfWithSkipperDependencyCollectorTest.java | Adds integration test ensuring pool-cache transparency with TransitiveDependencyManager. |
| maven-resolver-impl/src/test/resources/artifact-descriptions/pool-cache-transparency/gid_root_1.0.ini | Adds root fixture for diamond-like dependency graph. |
| maven-resolver-impl/src/test/resources/artifact-descriptions/pool-cache-transparency/gid_b_1.0.ini | Adds b fixture depending on c. |
| maven-resolver-impl/src/test/resources/artifact-descriptions/pool-cache-transparency/gid_b-alt_1.0.ini | Adds b-alt fixture depending on c. |
| maven-resolver-impl/src/test/resources/artifact-descriptions/pool-cache-transparency/gid_c_1.0.ini | Adds c fixture depending on d. |
| maven-resolver-impl/src/test/resources/artifact-descriptions/pool-cache-transparency/gid_d_1.0.ini | Adds d fixture with no dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // root has two children: b and b-alt | ||
| DependencyNode rootNode = result.getRoot(); | ||
| assertEquals(2, rootNode.getChildren().size(), "root should have 2 children (b, b-alt)"); | ||
|
|
||
| // b → c | ||
| DependencyNode b = rootNode.getChildren().get(0); | ||
| assertEquals("b", b.getArtifact().getArtifactId()); | ||
| assertFalse(b.getChildren().isEmpty(), "b should have children"); | ||
|
|
||
| // b → c → d | ||
| DependencyNode cUnderB = b.getChildren().get(0); | ||
| assertEquals("c", cUnderB.getArtifact().getArtifactId()); | ||
| assertFalse(cUnderB.getChildren().isEmpty(), "c under b should have children (d)"); | ||
|
|
||
| // b-alt → c (this is the key assertion: c under b-alt must also have children) | ||
| DependencyNode bAlt = rootNode.getChildren().get(1); | ||
| assertEquals("b-alt", bAlt.getArtifact().getArtifactId()); | ||
| assertFalse(bAlt.getChildren().isEmpty(), "b-alt should have children"); | ||
|
|
||
| DependencyNode cUnderBAlt = bAlt.getChildren().get(0); | ||
| assertEquals("c", cUnderBAlt.getArtifact().getArtifactId()); |
| if (managedVersions == null | ||
| && managedScopes == null | ||
| && managedOptionals == null | ||
| && managedLocalPaths == null | ||
| && managedExclusions == null | ||
| && isApplied()) { | ||
| return this; | ||
| } |
Summary
Fixes #2013 — BfDependencyCollector pool cache hit/miss changes graph structure (cache-transparency violation).
Root cause:
TransitiveDependencyManager.deriveChildManager()always created a new instance (with a uniquepathfield and incrementingdepth), making every pool key unique. The BF collector's pool cache includes theDependencyManagerin itsGraphKey, so distinct-but-semantically-equal managers caused pool misses. On a miss, the GACE-based skipper was consulted and could prune a node's children to zero — even though the same node had children when served from the cache via a different traversal path.Fix:
AbstractDependencyManager.deriveChildManager()now returnsthiswhen:depth >= applyFrom— theisApplied()guard)The
isApplied()guard is critical: atdepth < applyFrom, returningthiswould freeze the depth counter and preventisApplied()from ever returning true for descendants, breaking management rule application.This optimization addresses the common case for transitive dependencies whose POMs do not declare
<dependencyManagement>.ClassicDependencyManager(Maven 3.x) was not affected because itsderiveUntil=2causedisDerived()to returnthisat depth ≥ 2.Changes
AbstractDependencyManager.deriveChildManager()— added instance-reuse optimization before thenewInstance()callDependencyManagerTest— new unit testtestDeriveChildManagerReusesInstanceWhenNoNewManagementData()verifying the optimization withassertSame/assertNotSameat different depthsBfWithSkipperDependencyCollectorTest— new integration testtestPoolCacheTransparencyWithTransitiveDependencyManager()with a diamond-like graph (root → b → c → dandroot → b-alt → c → d) verifying thatcunderb-altretains its childrenTest plan
DependencyManagerTest.testDeriveChildManagerReusesInstanceWhenNoNewManagementData— verifies instance reuse at depth ≥ applyFrom with no management data, and new instance creation otherwiseBfWithSkipperDependencyCollectorTest.testPoolCacheTransparencyWithTransitiveDependencyManager— end-to-end test demonstrating the bug is fixedmaven-resolver-util: 447 tests,maven-resolver-impl: 468 tests)🤖 Generated with Claude Code