Skip to content

Commit 61d68bb

Browse files
committed
Fix configuration resolution lock conflicts in parallel builds for Gradle 9
1 parent ac7dc77 commit 61d68bb

File tree

6 files changed

+1201
-11
lines changed

6 files changed

+1201
-11
lines changed

src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
import org.gradle.api.logging.Logging;
4040
import org.gradle.api.plugins.ExtraPropertiesExtension;
4141
import org.gradle.internal.deprecation.DeprecationLogger;
42+
import org.gradle.util.GradleVersion;
4243

4344
import java.lang.reflect.Method;
4445
import java.util.*;
4546

4647
public class DependencyRecommendationsPlugin implements Plugin<Project> {
4748
public static final String NEBULA_RECOMMENDER_BOM = "nebulaRecommenderBom";
4849
public static final boolean CORE_BOM_SUPPORT_ENABLED = Boolean.getBoolean("nebula.features.coreBomSupport");
50+
private static final GradleVersion GRADLE_9_0 = GradleVersion.version("9.0");
4951
private Logger logger = Logging.getLogger(DependencyRecommendationsPlugin.class);
5052
private RecommendationProviderContainer recommendationProviderContainer;
5153
//TODO: remove this exclusion once https://github.com/gradle/gradle/issues/6750 is resolved
@@ -75,10 +77,19 @@ private void applyRecommendationsDirectly(final Project project, final Configura
7577
project.afterEvaluate(new Action<Project>() {
7678
@Override
7779
public void execute(Project p) {
80+
// Eagerly resolve and cache all BOMs if using build service approach
81+
if (shouldUseBuildService(p)) {
82+
eagerlyResolveBoms(p, recommendationProviderContainer);
83+
}
84+
7885
p.getConfigurations().all(new ExtendRecommenderConfigurationAction(bomConfiguration, p, recommendationProviderContainer));
7986
p.subprojects(new Action<Project>() {
8087
@Override
8188
public void execute(Project sub) {
89+
// Also eagerly resolve BOMs for subprojects if using build service
90+
if (shouldUseBuildService(sub)) {
91+
eagerlyResolveBoms(sub, recommendationProviderContainer);
92+
}
8293
sub.getConfigurations().all(new ExtendRecommenderConfigurationAction(bomConfiguration, sub, recommendationProviderContainer));
8394
}
8495
});
@@ -87,6 +98,17 @@ public void execute(Project sub) {
8798
}
8899

89100
private void applyRecommendations(final Project project) {
101+
// Add eager BOM resolution for regular (non-core) BOM support if using build service
102+
project.afterEvaluate(new Action<Project>() {
103+
@Override
104+
public void execute(Project p) {
105+
if (shouldUseBuildService(p)) {
106+
// Eagerly resolve and cache all BOMs after project evaluation
107+
eagerlyResolveBoms(p, recommendationProviderContainer);
108+
}
109+
}
110+
});
111+
90112
project.getConfigurations().all(new Action<Configuration>() {
91113
@Override
92114
public void execute(final Configuration conf) {
@@ -101,7 +123,7 @@ public Unit invoke(ResolvableDependencies resolvableDependencies) {
101123
}
102124

103125
for (Dependency dependency : resolvableDependencies.getDependencies()) {
104-
applyRecommendationToDependency(rsFactory, dependency, new ArrayList<ProjectDependency>());
126+
applyRecommendationToDependency(rsFactory, dependency, new ArrayList<ProjectDependency>(), project);
105127

106128
// if project dependency, pull all first orders and apply recommendations if missing dependency versions
107129
// dependency.getProjectConfiguration().allDependencies iterate and inspect them as well
@@ -160,7 +182,7 @@ private boolean isExcludedConfiguration(String confName) {
160182
return false;
161183
}
162184

163-
private void applyRecommendationToDependency(final RecommendationStrategyFactory factory, Dependency dependency, List<ProjectDependency> visited) {
185+
private void applyRecommendationToDependency(final RecommendationStrategyFactory factory, Dependency dependency, List<ProjectDependency> visited, Project rootProject) {
164186
if (dependency instanceof ExternalModuleDependency) {
165187
factory.getRecommendationStrategy().inspectDependency(dependency);
166188
} else if (dependency instanceof ProjectDependency) {
@@ -171,10 +193,10 @@ private void applyRecommendationToDependency(final RecommendationStrategyFactory
171193
try {
172194
ProjectDependency.class.getMethod("getTargetConfiguration");
173195
String targetConfiguration = projectDependency.getTargetConfiguration() == null ? Dependency.DEFAULT_CONFIGURATION : projectDependency.getTargetConfiguration();
174-
175-
DeprecationLogger.whileDisabled(() -> {
176-
configuration[0] = projectDependency.getDependencyProject().getConfigurations().getByName(targetConfiguration);
177-
});
196+
Project dependencyProject = rootProject.findProject(projectDependency.getPath());
197+
if (dependencyProject != null) {
198+
configuration[0] = dependencyProject.getConfigurations().getByName(targetConfiguration);
199+
}
178200
} catch (NoSuchMethodException ignore) {
179201
try {
180202
Method method = ProjectDependency.class.getMethod("getProjectConfiguration");
@@ -183,9 +205,11 @@ private void applyRecommendationToDependency(final RecommendationStrategyFactory
183205
throw new RuntimeException("Unable to retrieve configuration for project dependency", e);
184206
}
185207
}
186-
DependencySet dependencies = configuration[0].getAllDependencies();
187-
for (Dependency dep : dependencies) {
188-
applyRecommendationToDependency(factory, dep, visited);
208+
if (configuration[0] != null) {
209+
DependencySet dependencies = configuration[0].getAllDependencies();
210+
for (Dependency dep : dependencies) {
211+
applyRecommendationToDependency(factory, dep, visited, rootProject);
212+
}
189213
}
190214
}
191215
}
@@ -243,4 +267,71 @@ public Set<String> getReasonsRecursive(Project project) {
243267
return getReasonsRecursive(project.getParent());
244268
return Collections.emptySet();
245269
}
270+
271+
/**
272+
* Determines whether to use the BomResolverService (build service) approach.
273+
*
274+
* <p>The build service is used when:</p>
275+
* <ul>
276+
* <li>Gradle version is 9.0 or higher, OR</li>
277+
* <li>The gradle property 'nebula.dependency-recommender.useBuildService' is set to true</li>
278+
* </ul>
279+
*
280+
* @param project the Gradle project to check
281+
* @return true if build service should be used, false otherwise
282+
*/
283+
private boolean shouldUseBuildService(Project project) {
284+
// Check if explicitly enabled via gradle property
285+
if (project.hasProperty("nebula.dependency-recommender.useBuildService")) {
286+
Object property = project.property("nebula.dependency-recommender.useBuildService");
287+
if (Boolean.parseBoolean(property.toString())) {
288+
return true;
289+
}
290+
}
291+
292+
// Default behavior: use build service for Gradle 9+
293+
GradleVersion currentVersion = GradleVersion.current();
294+
return currentVersion.compareTo(GRADLE_9_0) >= 0;
295+
}
296+
297+
/**
298+
* Eagerly resolves BOM configurations during the configuration phase to prevent
299+
* configuration resolution lock conflicts in parallel builds.
300+
*
301+
* <p>This method is called during {@code afterEvaluate} when exclusive locks are
302+
* available. It instructs the {@link netflix.nebula.dependency.recommender.service.BomResolverService} to resolve all BOM
303+
* configurations and cache the results for later use during dependency resolution.</p>
304+
*
305+
* <p>The eager resolution prevents the need to resolve configurations during the
306+
* dependency resolution phase, which would cause {@code IllegalResolutionException}
307+
* in parallel builds with Gradle 9+.</p>
308+
*
309+
* @param project the Gradle project whose BOM configurations should be resolved
310+
* @param container the recommendation provider container to check for additional BOM providers
311+
*/
312+
private void eagerlyResolveBoms(Project project, RecommendationProviderContainer container) {
313+
try {
314+
// Get the build service
315+
org.gradle.api.provider.Provider<netflix.nebula.dependency.recommender.service.BomResolverService> bomResolverService =
316+
project.getGradle().getSharedServices().registerIfAbsent(
317+
"bomResolver", netflix.nebula.dependency.recommender.service.BomResolverService.class, spec -> {}
318+
);
319+
320+
// Resolve BOMs from the nebulaRecommenderBom configuration
321+
bomResolverService.get().eagerlyResolveAndCacheBoms(project, NEBULA_RECOMMENDER_BOM);
322+
323+
// Also trigger resolution for maven BOM provider if it exists
324+
// This handles mavenBom providers configured in the extension
325+
netflix.nebula.dependency.recommender.provider.MavenBomRecommendationProvider mavenBomProvider = container.getMavenBomProvider();
326+
if (mavenBomProvider != null) {
327+
try {
328+
mavenBomProvider.getVersion("dummy", "dummy"); // Trigger lazy initialization
329+
} catch (Exception e) {
330+
// Expected - just needed to trigger BOM resolution
331+
}
332+
}
333+
} catch (Exception e) {
334+
logger.warn("Failed to eagerly resolve BOMs for project " + project.getPath(), e);
335+
}
336+
}
246337
}

src/main/groovy/netflix/nebula/dependency/recommender/provider/ClasspathBasedRecommendationProvider.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,82 @@
1515
*/
1616
package netflix.nebula.dependency.recommender.provider;
1717

18+
import netflix.nebula.dependency.recommender.service.BomResolverService;
1819
import org.gradle.api.Project;
1920
import org.gradle.api.artifacts.Configuration;
21+
import org.gradle.api.provider.Provider;
22+
import org.gradle.util.GradleVersion;
2023

2124
import java.io.File;
25+
import java.util.Map;
2226
import java.util.Set;
2327

2428
public abstract class ClasspathBasedRecommendationProvider extends AbstractRecommendationProvider {
2529
protected Project project;
2630
protected Configuration configuration;
31+
protected String configName;
32+
protected Provider<BomResolverService> bomResolverService;
33+
private static final GradleVersion GRADLE_9_0 = GradleVersion.version("9.0");
2734

2835
ClasspathBasedRecommendationProvider(Project project, String configName) {
2936
this.project = project;
37+
this.configName = configName;
3038
this.configuration = project.getConfigurations().getByName(configName);
39+
40+
// Only initialize build service if we might use it
41+
if (shouldUseBuildService()) {
42+
this.bomResolverService = project.getGradle().getSharedServices().registerIfAbsent(
43+
"bomResolver", BomResolverService.class, spec -> {}
44+
);
45+
}
3146
}
3247

3348
Set<File> getFilesOnConfiguration() {
3449
return configuration.resolve();
3550
}
51+
52+
/**
53+
* Retrieves BOM recommendations using the shared {@link BomResolverService}.
54+
*
55+
* <p>This method delegates to the build service to get cached BOM recommendations,
56+
* avoiding direct configuration resolution that could cause lock conflicts in
57+
* parallel builds. The build service ensures that all BOM resolution happens
58+
* during the configuration phase when exclusive locks are available.</p>
59+
*
60+
* @param reasons a mutable set that will be populated with reasons explaining
61+
* why specific recommendations were applied
62+
* @return a map of dependency coordinates (groupId:artifactId) to recommended versions
63+
* @throws RuntimeException if the build service is not available or BOM resolution fails
64+
*/
65+
protected Map<String, String> getBomRecommendations(Set<String> reasons) {
66+
if (bomResolverService == null) {
67+
throw new RuntimeException("BomResolverService not available - build service approach not enabled");
68+
}
69+
return bomResolverService.get().getRecommendations(project, configName, reasons);
70+
}
71+
72+
/**
73+
* Determines whether to use the BomResolverService (build service) approach.
74+
*
75+
* <p>The build service is used when:</p>
76+
* <ul>
77+
* <li>Gradle version is 9.0 or higher, OR</li>
78+
* <li>The gradle property 'nebula.dependency-recommender.useBuildService' is set to true</li>
79+
* </ul>
80+
*
81+
* @return true if build service should be used, false otherwise
82+
*/
83+
private boolean shouldUseBuildService() {
84+
// Check if explicitly enabled via gradle property
85+
if (project.hasProperty("nebula.dependency-recommender.useBuildService")) {
86+
Object property = project.property("nebula.dependency-recommender.useBuildService");
87+
if (Boolean.parseBoolean(property.toString())) {
88+
return true;
89+
}
90+
}
91+
92+
// Default behavior: use build service for Gradle 9+
93+
GradleVersion currentVersion = GradleVersion.current();
94+
return currentVersion.compareTo(GRADLE_9_0) >= 0;
95+
}
3696
}

src/main/groovy/netflix/nebula/dependency/recommender/provider/MavenBomRecommendationProvider.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.codehaus.plexus.interpolation.ValueSource;
3232
import org.gradle.api.Project;
3333
import org.gradle.api.artifacts.Configuration;
34+
import org.gradle.util.GradleVersion;
3435

3536
import java.io.File;
3637
import java.io.FileInputStream;
@@ -42,6 +43,7 @@
4243
public class MavenBomRecommendationProvider extends ClasspathBasedRecommendationProvider {
4344
private volatile Map<String, String> recommendations = null;
4445
private Set<String> reasons = new HashSet<>();
46+
private static final GradleVersion GRADLE_9_0 = GradleVersion.version("9.0");
4547

4648
public MavenBomRecommendationProvider(Project project, String configName) {
4749
super(project, configName);
@@ -88,9 +90,21 @@ public String getVersion(String org, String name) throws Exception {
8890
public Map<String, String> getRecommendations() {
8991
if (recommendations == null) {
9092
try {
91-
recommendations = getMavenRecommendations();
93+
// Try to get cached recommendations from build service if using Gradle 9+ or flag is enabled
94+
if (shouldUseBuildService()) {
95+
recommendations = getBomRecommendations(reasons);
96+
} else {
97+
// Fallback to original implementation for older Gradle versions
98+
recommendations = getMavenRecommendations();
99+
}
92100
} catch (Exception e) {
93-
throw new RuntimeException(e);
101+
// Fallback to original implementation if build service fails
102+
try {
103+
recommendations = getMavenRecommendations();
104+
} catch (Exception fallbackException) {
105+
// If both approaches fail, return empty map to avoid failures
106+
recommendations = new HashMap<>();
107+
}
94108
}
95109
}
96110
return recommendations;
@@ -187,4 +201,29 @@ public List<ValueSource> createValueSources(Model model, File projectDir, ModelB
187201
return sources;
188202
}
189203
}
204+
205+
/**
206+
* Determines whether to use the BomResolverService (build service) approach.
207+
*
208+
* <p>The build service is used when:</p>
209+
* <ul>
210+
* <li>Gradle version is 9.0 or higher, OR</li>
211+
* <li>The gradle property 'nebula.dependency-recommender.useBuildService' is set to true</li>
212+
* </ul>
213+
*
214+
* @return true if build service should be used, false otherwise
215+
*/
216+
private boolean shouldUseBuildService() {
217+
// Check if explicitly enabled via gradle property
218+
if (project.hasProperty("nebula.dependency-recommender.useBuildService")) {
219+
Object property = project.property("nebula.dependency-recommender.useBuildService");
220+
if (Boolean.parseBoolean(property.toString())) {
221+
return true;
222+
}
223+
}
224+
225+
// Default behavior: use build service for Gradle 9+
226+
GradleVersion currentVersion = GradleVersion.current();
227+
return currentVersion.compareTo(GRADLE_9_0) >= 0;
228+
}
190229
}

src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import netflix.nebula.dependency.recommender.DependencyRecommendationsPlugin;
2020
import netflix.nebula.dependency.recommender.RecommendationStrategies;
2121
import org.gradle.api.*;
22+
import org.gradle.api.artifacts.Configuration;
2223
import org.gradle.api.artifacts.Dependency;
2324
import org.gradle.api.internal.ConfigureByMapAction;
2425
import org.gradle.api.internal.DefaultNamedDomainObjectList;
@@ -270,6 +271,26 @@ private static class CoreBomSupportProvider extends MavenBomRecommendationProvid
270271
super(project, configName, reasons);
271272
}
272273

274+
@Override
275+
protected Map<String, String> getBomRecommendations(Set<String> reasons) {
276+
// For core BOM support, we need to create detached configuration with regular dependencies
277+
// instead of using the shared service that works with the main configuration
278+
List<Dependency> rawPomDependencies = new ArrayList<>();
279+
for(org.gradle.api.artifacts.Dependency dependency: configuration.getDependencies()) {
280+
rawPomDependencies.add(project.getDependencies().create(dependency.getGroup() + ":" + dependency.getName() + ":" + dependency.getVersion() + "@pom"));
281+
}
282+
Configuration detachedConfig = project.getConfigurations().detachedConfiguration(
283+
rawPomDependencies.toArray(new org.gradle.api.artifacts.Dependency[0]));
284+
285+
// Use the build service with cached data only (no resolution during dependency resolution)
286+
if (bomResolverService != null) {
287+
return bomResolverService.get().getCachedRecommendationsFromConfiguration(detachedConfig, reasons);
288+
} else {
289+
// Fallback - this shouldn't happen in normal usage but prevents errors
290+
throw new RuntimeException("BOM not cached and no build service context available for resolution");
291+
}
292+
}
293+
273294
@Override
274295
Set<File> getFilesOnConfiguration() {
275296
List<Dependency> rawPomDependencies = new ArrayList<>();

0 commit comments

Comments
 (0)