Skip to content

Fixes a race in FormatterStepSerializationRoundtrip.HackClone.writeObject#2994

Merged
nedtwigg merged 3 commits into
diffplug:mainfrom
bric3:hackclone-race
Jul 22, 2026
Merged

Fixes a race in FormatterStepSerializationRoundtrip.HackClone.writeObject#2994
nedtwigg merged 3 commits into
diffplug:mainfrom
bric3:hackclone-race

Conversation

@bric3

@bric3 bric3 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

There's a race in FormatterStepSerializationRoundtrip.HackClone.writeObject when certain conditioin are met, this PR to fixes it by publishing an object.

Previously, HackClone assigned its cleaned clone before populating the clone’s equality or roundtrip state. If another Gradle worker serialized the same clone during that window, the nested FormatterStepSerializationRoundtrip.writeObject guard could see an half-initialized cleaned and then fail.

This now builds the cleaned clone locally, initializes the requested state, and only then publishes it.

Adds a Gradle regression intgeration test that exercises the task input fingerprinting. In a real project the issue was caused by a configuration like this:

plugins {
  id("com.diffplug.spotless") version "8.4.0"
}

allprojects {
  spotless {
    groovyGradle {
        toggleOffOn() // actual FencedStep, that wraps the actual formatter
        target '**/*.gradle' // target that match no files init the project, cause early task input fingerprinting
        greclipse() // lazy formatter that happens to be slow to init
    }
  }
}

And

  • the project had no more files matching the target.
  • the Gradle parallel mode is enabled

The added integration test, mimick the issue in a more deterministic way, by using the primitives to create the steps.


After creating the PR, please add a commit that adds a bullet-point under the [Unreleased] section of CHANGES.md, plugin-gradle/CHANGES.md, and plugin-maven/CHANGES.md which includes:

  • a summary of the change
  • either
    • a link to the issue you are resolving (for small changes)
    • a link to the PR you just created (for big changes likely to have discussion)

If your change only affects a build plugin, and not the lib, then you only need to update the plugin-foo/CHANGES.md for that plugin.

If your change affects lib in an end-user-visible way (fixing a bug, updating a version) then you need to update CHANGES.md for both the lib and all build plugins. Users of a build plugin shouldn't have to refer to lib to see changes that affect them.

This makes it easier for the maintainers to quickly release your changes :)

@nedtwigg

Copy link
Copy Markdown
Member

Excellent find @bric3, thanks very much. This will likely resolve a lot of futzy issues. Out of curiosity - did you solve this purely by hand, or did an agent play a role? I'd be curious to understand the workflow only because it's such a subtle issue.

@nedtwigg
nedtwigg merged commit 3924217 into diffplug:main Jul 22, 2026
19 checks passed
@bric3

bric3 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Finding the suspect code was somewhat easy enough to connect the dots. I followed the stack trace and by looking at the code I noticed the issue having the initilizer null was a a clear pattern for me.

stack
Caused by: java.lang.IllegalStateException: If the initializer was null, then one of roundtripStateInternal or equalityStateInternal should be non-null, and neither was
        at com.diffplug.spotless.FormatterStepSerializationRoundtrip.writeObject(FormatterStepSerializationRoundtrip.java:74)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at java.base/java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1072)

But confirming and reproducing it was done by an agent. I also intervened a bit at the end to correct a few minor stuff in the "build.gradle fixture".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants