spotless
spotless copied to clipboard
Concurrency issue when using GradleBuild task in multiproject setup
JavaCompile
tasks can fail randomly if the Spotless plugin is applied on subprojects AND a GradleBuild
task is executing on the same project in parallel.
Gradle version 7.3.3 Spotless Gradle plugin version 6.1.2 Gradle build scan: https://gradle.com/s/enf5wof4kfw7i Public repo: https://github.com/davidburstromspotify/spotless-issue-1087
I have a feeling the plugin causes some cross talk between the outer and inner build.
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':subproject5:compileJava'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:145)
at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:282)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:143)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:131)
...
Caused by: java.lang.NullPointerException
at java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:936)
at org.gradle.api.internal.tasks.compile.tooling.JavaCompileTaskSuccessResultPostProcessor.findTaskOperationId(JavaCompileTaskSuccessResultPostProcessor.java:65)
at org.gradle.api.internal.tasks.compile.tooling.JavaCompileTaskSuccessResultPostProcessor.findTaskOperationId(JavaCompileTaskSuccessResultPostProcessor.java:69)
at org.gradle.api.internal.tasks.compile.tooling.JavaCompileTaskSuccessResultPostProcessor.finished(JavaCompileTaskSuccessResultPostProcessor.java:58)
...
I think I know why this is happening. We create a single task in the root project, :spotlessInternalRegisterDependencies
. The problem is that in a composite build, there will be two different Spotless plugins, each with a classloader from their own build, each trying to register the same task.
The way that we resolved that was:
- try to register
:spotlessInternalRegisterDependencies
- if that fails, then register
:spotlessInternalRegisterDependencies${System.identityHashCode(RegisterDependenciesTask.class)}
https://github.com/diffplug/spotless/blob/804a73aefce1dbb02c92729cbd265848307aafc3/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java#L242-L252
The exception being caught above is thrown on this line:
https://github.com/diffplug/spotless/blob/804a73aefce1dbb02c92729cbd265848307aafc3/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java#L259
I think I have an easy solution (I feel like there's an air bubble of "Gradle demands to declare dependencies in the root project" and Spotless is pushing that bubble around, but we do seem to have concretely ironed out some cases, and here we can iron out another.
I was wrong. I added some very simple printlns: https://github.com/diffplug/spotless/commit/72919ceeb2d852add4cb38d504d3576b0adbc5eb
And this is the output I get over and over:
./gradlew build
> Configure project :
~~~ REGISTER spotlessInternalRegisterDependencies
> Configure project :spotless-issue-1087
~~~ REGISTER spotlessInternalRegisterDependencies
> Task :spotless-issue-1087:help
The important thing is that "FAILED TO REGISTER" is never printed, and it is strange that "REGISTER" succeeds twice. Not sure what to make of that...
You probably also already noticed this, but it's not even necessary to run any Spotless tasks:
./gradlew gradleBuild compileJava
is enough to provoke the issue. But as you point out, task registration will happen regardless.
https://scans.gradle.com/s/3cxzpd6plgo22/timeline
This seems like a Gradle bug to me. I wonder if something as simple as this could reproduce it:
public class BugPlugin implements Plugin<Project> {
static final String BUG_TASK = "bug";
TaskProvider<BugTask> bugTask;
@Override
public void apply(Project project) {
if (!project.rootProject.tasks.names.contains(BUG_TASK)) {
bugTask = rootProjectTasks.register(BUG_TASK, BugTask.class, BugTask::setup);
} else {
bugTask = rootProjectTasks.named(BUG_TASK, BugTask.class);
}
}
It could certainly be a Gradle bug, would be good to rule out Spotless from the equation.
I tried with
subprojects {
if (!project.rootProject.tasks.names.contains("dummy")) {
project.rootProject.tasks.register("dummy", DefaultTask::class.java)
} else {
println("contains dummy")
}
}
which should be the equivalent of applying the plugin, but it didn't provoke any issue.
Looking at the debug logs when using the Spotless plugin, it looks like Gradle registers one :spotlessInternalRegisterDependencies
and one :spotless-issue-1087:spotlessInternalRegisterDependencies
, which indicates they apply on different projects (the latter being considered the root project for the GradleBuild
invocation). What's the risk of registering the task twice in one Gradle invocation?
The fact that there's a reference to the Gradle
instance makes my spidey-sense tingle, though I'm not sure if or how that can introduce any unexpected cross-talk:
https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java#L65
Maybe instead of DefaultTask
use BugTask
, and have that BugTask
create a shared build since that's what Spotless does?
the latter being considered the root project for the GradleBuild invocation
I agree that seems to be the case, but I don't understand why that is happening / is possible. Seems like a very bad design to have "which project is the root" depend on the context of how it was called.
I also faced the same bug, I cannot help you much but it would be great if you could solve it !
fwiw, we are now seeing this as well in gradle 7.4.2 (I know, I know...)