gnodet-bot commented on code in PR #2132:
URL: https://github.com/apache/maven-resolver/pull/2132#discussion_r4114456788


##########
maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java:
##########
@@ -254,18 +255,76 @@ public DependencyNode transformGraph(DependencyNode node, 
DependencyGraphTransfo
             throws RepositoryException {
         String cf = ConfigUtils.getString(
                 context.getSession(), DEFAULT_CONFLICT_RESOLVER_IMPL, 
CONFIG_PROP_CONFLICT_RESOLVER_IMPL);
+
         ConflictResolver delegate;
-        if (AUTO_CONFLICT_RESOLVER.equals(cf) || 
CLASSIC_CONFLICT_RESOLVER.equals(cf)) {
+
+        if (AUTO_CONFLICT_RESOLVER.equals(cf)) {
+            delegate = selectConflictResolver(node, context);
+        } else if (CLASSIC_CONFLICT_RESOLVER.equals(cf)) {
             delegate = new ClassicConflictResolver(versionSelector, 
scopeSelector, optionalitySelector, scopeDeriver);
         } else if (PATH_CONFLICT_RESOLVER.equals(cf)) {
             delegate = new PathConflictResolver(versionSelector, 
scopeSelector, optionalitySelector, scopeDeriver);
         } else {
             throw new IllegalArgumentException("Unknown conflict resolver: " + 
cf + "; known are "
                     + Arrays.asList(AUTO_CONFLICT_RESOLVER, 
PATH_CONFLICT_RESOLVER, CLASSIC_CONFLICT_RESOLVER));
         }
+
         return delegate.transformGraph(node, context);
     }
 
+    /**
+     * Automatically selects the conflict resolver based on the estimated 
memory requirements.
+     * PathConflictResolver is used for dependency trees that fit within the 
memory threshold,
+     * while ClassicConflictResolver is used for larger trees.
+     */
+    private ConflictResolver selectConflictResolver(DependencyNode node, 
DependencyGraphTransformationContext context)
+            throws RepositoryException {
+
+        if (context.get(TransformationContextKeys.SORTED_CONFLICT_IDS) == 
null) {
+            new ConflictIdSorter().transformGraph(node, context);
+        }
+
+        Runtime rt = Runtime.getRuntime();
+        long available = rt.maxMemory() - (rt.totalMemory() - rt.freeMemory());
+
+        // Estimate the maximum number of Path tree nodes that would fit in 
25% of available heap.
+        // Each Path object costs ~200 bytes (object header + fields + 
children list entry).
+        int maxPathNodes = (int) Math.min(available / (4L * 200), 
Integer.MAX_VALUE);

Review Comment:
   ⚠️ **Still non-deterministic (not addressed from previous review).** 
`Runtime.freeMemory()` reflects GC state, allocation pressure from other 
threads, and JVM ergonomics. The same dependency graph can get 
`PathConflictResolver` on one invocation and `ClassicConflictResolver` on the 
next.
   
   Moreover, since PR #2153 merged, `PathConflictResolver` itself uses 
depth-bounded expansion (`IdentityHashMap<DependencyNode, Integer>`) to cap 
`Path` creation. The OOM that motivated this heuristic is fixed at the source. 
This entire memory-estimation block is now solving a problem that no longer 
exists.



##########
maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java:
##########
@@ -119,8 +120,8 @@ public class ConflictResolver implements 
DependencyGraphTransformer {
     /**
      * The name of the conflict resolver implementation to use: "auto" 
(default), "path", or "classic" (same as Maven 3).
      * <p>
-     * When set to "auto", the resolver will currently just use "classic". The 
idea here, is that this value will
-     * always select the best (most robust, most performant) one, which 
currently is "classic".
+     * When set to "auto", the resolver automatically selects the most 
appropriate implementation based on the
+     * dependency graph size and available memory.

Review Comment:
   💡 **Javadoc contradiction not addressed (from previous review).** This 
property-level doc claims auto mode "automatically selects the most appropriate 
implementation," but the class-level Javadoc at lines 47 and 52-53 still says:
   - "PathConflictResolver — Not yet recommended for production"
   - "All projects: Use ClassicConflictResolver for optimal correctness"
   
   These are contradictory. If auto mode can select PathConflictResolver, the 
class-level docs need updating too — or this Javadoc should stay conservative 
until the class docs are revised.



##########
maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java:
##########
@@ -905,29 +929,24 @@ private static DependencyNode makeDependencyNode(
     @ParameterizedTest
     @MethodSource("conflictResolverSource")
     void denseGraphDoesNotOom(ConflictResolver conflictResolver) throws 
RepositoryException {
-        // M=N=K=100: without the fix, creates M*N + M*N*K = 1,010,000 Path 
objects (~80 MB)
-        // → OutOfMemoryError on CI. With the fix: O(N+K) = 200 expansions, 
trivial memory.
-        int M = 100; // parent modules
-        int N = 100; // hub modules (shared, each with children)
-        int K = 100; // sub-hub leaf modules (shared across hubs)
+        int M = 100;

Review Comment:
   ⚠️ **Don't strip documentation comments from existing tests.** These 
comments (added by PR #2153) explain the test structure — e.g., `M=N=K=100: 
without the fix, creates M*N + M*N*K = 1,010,000 Path objects (~80 MB)` and the 
purpose of each node group. Removing them makes the test harder to understand 
for future contributors. Please revert the comment removals from 
`denseGraphDoesNotOom`, `autoSelectionResolvesConflictsCorrectly`, and 
`defaultConfigUsesAutoSelection`.



##########
maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java:
##########
@@ -254,18 +255,76 @@ public DependencyNode transformGraph(DependencyNode node, 
DependencyGraphTransfo
             throws RepositoryException {
         String cf = ConfigUtils.getString(
                 context.getSession(), DEFAULT_CONFLICT_RESOLVER_IMPL, 
CONFIG_PROP_CONFLICT_RESOLVER_IMPL);
+
         ConflictResolver delegate;
-        if (AUTO_CONFLICT_RESOLVER.equals(cf) || 
CLASSIC_CONFLICT_RESOLVER.equals(cf)) {
+
+        if (AUTO_CONFLICT_RESOLVER.equals(cf)) {
+            delegate = selectConflictResolver(node, context);
+        } else if (CLASSIC_CONFLICT_RESOLVER.equals(cf)) {
             delegate = new ClassicConflictResolver(versionSelector, 
scopeSelector, optionalitySelector, scopeDeriver);
         } else if (PATH_CONFLICT_RESOLVER.equals(cf)) {
             delegate = new PathConflictResolver(versionSelector, 
scopeSelector, optionalitySelector, scopeDeriver);
         } else {
             throw new IllegalArgumentException("Unknown conflict resolver: " + 
cf + "; known are "
                     + Arrays.asList(AUTO_CONFLICT_RESOLVER, 
PATH_CONFLICT_RESOLVER, CLASSIC_CONFLICT_RESOLVER));
         }
+
         return delegate.transformGraph(node, context);
     }
 
+    /**
+     * Automatically selects the conflict resolver based on the estimated 
memory requirements.
+     * PathConflictResolver is used for dependency trees that fit within the 
memory threshold,
+     * while ClassicConflictResolver is used for larger trees.
+     */
+    private ConflictResolver selectConflictResolver(DependencyNode node, 
DependencyGraphTransformationContext context)
+            throws RepositoryException {
+
+        if (context.get(TransformationContextKeys.SORTED_CONFLICT_IDS) == 
null) {
+            new ConflictIdSorter().transformGraph(node, context);
+        }
+
+        Runtime rt = Runtime.getRuntime();
+        long available = rt.maxMemory() - (rt.totalMemory() - rt.freeMemory());
+
+        // Estimate the maximum number of Path tree nodes that would fit in 
25% of available heap.
+        // Each Path object costs ~200 bytes (object header + fields + 
children list entry).
+        int maxPathNodes = (int) Math.min(available / (4L * 200), 
Integer.MAX_VALUE);
+
+        // Walk the dependency tree to count total nodes (including 
diamond-expanded duplicates).
+        // The Path tree mirrors this structure, so the count directly 
reflects Path tree size.
+        // Use early-exit: stop counting once we exceed the threshold.
+        if (treeExceedsThreshold(node, maxPathNodes)) {
+            return new ClassicConflictResolver(versionSelector, scopeSelector, 
optionalitySelector, scopeDeriver);
+        } else {
+            return new PathConflictResolver(versionSelector, scopeSelector, 
optionalitySelector, scopeDeriver);
+        }

Review Comment:
   ⚠️ **Heuristic no longer matches PathConflictResolver behavior.** 
`treeExceedsThreshold` counts shared nodes multiple times (no visited set), 
mirroring the *old* PathConflictResolver expansion. After PR #2153, 
PathConflictResolver uses depth-bounded expansion and does NOT re-traverse 
subtrees of nodes already expanded at a shallower depth. The count from 
`treeExceedsThreshold` now **overestimates** PathConflictResolver's actual 
memory usage, causing unnecessary fallback to `ClassicConflictResolver` — 
exactly the situation auto-selection was supposed to avoid.



##########
maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java:
##########
@@ -860,6 +857,33 @@ void defaultConfigUsesAutoSelection() throws 
RepositoryException {
         assertSame(baz1Node, fooNode.getChildren().get(1));
     }
 
+    /**
+     * Verifies that tree threshold checking stops as soon as the number of 
visited nodes
+     * exceeds the configured threshold.
+     */
+    @org.junit.jupiter.api.Test
+    void treeThresholdStopsEarly() throws Exception {
+        ConflictResolver delegating = new ConflictResolver(
+                new NearestVersionSelector(),
+                new JavaScopeSelector(),
+                new SimpleOptionalitySelector(),
+                new JavaScopeDeriver());
+
+        DependencyNode root = makeDependencyNode("some-group", "root", "1.0");
+        DependencyNode child = makeDependencyNode("some-group", "child", 
"1.0");
+        DependencyNode grandChild = makeDependencyNode("some-group", 
"grand-child", "1.0");
+
+        root.setChildren(mutableList(child));

Review Comment:
   💡 **Reflection-based test is fragile.** Using `setAccessible(true)` to test 
a `private` method couples the test to an implementation detail. If the method 
is renamed, moved, or its signature changes, this test breaks silently at 
runtime rather than at compile time.
   
   Consider either:
   - Making `treeExceedsThreshold` package-private (it's a pure utility with no 
side effects)
   - Testing the behavior through the public API instead (verify that `auto` 
config selects the expected resolver for small vs large graphs)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to