Skip to content

Commit 2fec7d8

Browse files
authored
Merge pull request #140 from nebula-plugins/backport-buildservice-logic
Fix configuration resolution lock conflicts in parallel builds for Gradle 9
2 parents ac7dc77 + ab2c3a7 commit 2fec7d8

File tree

6 files changed

+1203
-11
lines changed

6 files changed

+1203
-11
lines changed

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

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import netflix.nebula.dependency.recommender.provider.RecommendationProviderContainer;
2222
import netflix.nebula.dependency.recommender.provider.RecommendationResolver;
2323
import netflix.nebula.dependency.recommender.publisher.MavenBomXmlGenerator;
24+
import netflix.nebula.dependency.recommender.service.BomResolverService;
2425
import org.apache.commons.lang3.StringUtils;
2526
import org.codehaus.groovy.runtime.MethodClosure;
2627
import org.gradle.api.Action;
@@ -38,14 +39,17 @@
3839
import org.gradle.api.logging.Logger;
3940
import org.gradle.api.logging.Logging;
4041
import org.gradle.api.plugins.ExtraPropertiesExtension;
42+
import org.gradle.api.provider.Provider;
4143
import org.gradle.internal.deprecation.DeprecationLogger;
44+
import org.gradle.util.GradleVersion;
4245

4346
import java.lang.reflect.Method;
4447
import java.util.*;
4548

4649
public class DependencyRecommendationsPlugin implements Plugin<Project> {
4750
public static final String NEBULA_RECOMMENDER_BOM = "nebulaRecommenderBom";
4851
public static final boolean CORE_BOM_SUPPORT_ENABLED = Boolean.getBoolean("nebula.features.coreBomSupport");
52+
private static final GradleVersion GRADLE_9_0 = GradleVersion.version("9.0");
4953
private Logger logger = Logging.getLogger(DependencyRecommendationsPlugin.class);
5054
private RecommendationProviderContainer recommendationProviderContainer;
5155
//TODO: remove this exclusion once https://github.com/gradle/gradle/issues/6750 is resolved
@@ -75,10 +79,19 @@ private void applyRecommendationsDirectly(final Project project, final Configura
7579
project.afterEvaluate(new Action<Project>() {
7680
@Override
7781
public void execute(Project p) {
82+
// Eagerly resolve and cache all BOMs if using build service approach
83+
if (shouldUseBuildService(p)) {
84+
eagerlyResolveBoms(p, recommendationProviderContainer);
85+
}
86+
7887
p.getConfigurations().all(new ExtendRecommenderConfigurationAction(bomConfiguration, p, recommendationProviderContainer));
7988
p.subprojects(new Action<Project>() {
8089
@Override
8190
public void execute(Project sub) {
91+
// Also eagerly resolve BOMs for subprojects if using build service
92+
if (shouldUseBuildService(sub)) {
93+
eagerlyResolveBoms(sub, recommendationProviderContainer);
94+
}
8295
sub.getConfigurations().all(new ExtendRecommenderConfigurationAction(bomConfiguration, sub, recommendationProviderContainer));
8396
}
8497
});
@@ -87,6 +100,17 @@ public void execute(Project sub) {
87100
}
88101

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

103127
for (Dependency dependency : resolvableDependencies.getDependencies()) {
104-
applyRecommendationToDependency(rsFactory, dependency, new ArrayList<ProjectDependency>());
128+
applyRecommendationToDependency(rsFactory, dependency, new ArrayList<ProjectDependency>(), project);
105129

106130
// if project dependency, pull all first orders and apply recommendations if missing dependency versions
107131
// dependency.getProjectConfiguration().allDependencies iterate and inspect them as well
@@ -160,7 +184,7 @@ private boolean isExcludedConfiguration(String confName) {
160184
return false;
161185
}
162186

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

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.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)