Make DeclarativeRecipe.accumulator thread-safe with ThreadLocal#6810
Merged
Make DeclarativeRecipe.accumulator thread-safe with ThreadLocal#6810
Conversation
DeclarativeRecipe.accumulator was a plain field that gets set during the scan phase via getInitialValue(). When the same DeclarativeRecipe instance is shared across concurrent recipe runs (e.g. via a recipe cache), one run's scan phase overwrites the accumulator set by another run. This causes the second run to use stale or null accumulator state during the edit phase, resulting in precondition scanning recipes producing incorrect results (typically 0 file changes). This changes the accumulator field to a ThreadLocal so each run thread gets its own independent accumulator. The clone() method is also overridden to create a fresh ThreadLocal for cloned instances.
zieka
reviewed
Feb 24, 2026
Contributor
Author
it is now |
sambsnyd
approved these changes
Feb 24, 2026
zieka
approved these changes
Feb 24, 2026
macsux
pushed a commit
that referenced
this pull request
Feb 27, 2026
* Make DeclarativeRecipe.accumulator thread-safe with ThreadLocal DeclarativeRecipe.accumulator was a plain field that gets set during the scan phase via getInitialValue(). When the same DeclarativeRecipe instance is shared across concurrent recipe runs (e.g. via a recipe cache), one run's scan phase overwrites the accumulator set by another run. This causes the second run to use stale or null accumulator state during the edit phase, resulting in precondition scanning recipes producing incorrect results (typically 0 file changes). This changes the accumulator field to a ThreadLocal so each run thread gets its own independent accumulator. The clone() method is also overridden to create a fresh ThreadLocal for cloned instances. * Clean up ThreadLocal in onComplete() to prevent memory leaks
pstreef
added a commit
to pstreef/rewrite
that referenced
this pull request
Mar 10, 2026
…veRecipe PR openrewrite#6810 changed DeclarativeRecipe.accumulator from a plain field to ThreadLocal to fix concurrent recipe runs overwriting each other's accumulator. However, ThreadLocal values are lost when execution resumes on a different thread after yielding. The accumulator is already stored on the rootCursor by ScanningRecipe.getAccumulator(), which is per-run and survives thread switches. This change removes the redundant ThreadLocal storage and instead resolves the accumulator from the rootCursor at visit time. The PreconditionBellwether now defers precondition resolution to visit time (when the rootCursor is available) rather than eagerly resolving in a field initializer.
pstreef
added a commit
to pstreef/rewrite
that referenced
this pull request
Mar 11, 2026
…veRecipe PR openrewrite#6810 changed DeclarativeRecipe.accumulator from a plain field to ThreadLocal to fix concurrent recipe runs overwriting each other's accumulator. However, ThreadLocal values are lost when execution resumes on a different thread after yielding. The accumulator is already stored on the rootCursor by ScanningRecipe.getAccumulator(), which is per-run and survives thread switches. This change removes the redundant ThreadLocal storage and instead resolves the accumulator from the rootCursor at visit time. The PreconditionBellwether now defers precondition resolution to visit time (when the rootCursor is available) rather than eagerly resolving in a field initializer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's Changed
DeclarativeRecipe.accumulatoris a mutable field that gets set during the scan phase viagetInitialValue(). When the sameDeclarativeRecipeinstance is shared across concurrent recipe runs (e.g. via a recipe cache in the Moderne worker), one run's scan phase overwrites the accumulator set by another run. This causes the second run to use stale or null accumulator state during the edit phase, resulting in precondition scanning recipes producing incorrect results — typically 0 file changes for the losing run.Changes
accumulatorfield fromAccumulatortoThreadLocal<Accumulator>so each run thread gets its own independent accumulatorgetInitialValue()to useaccumulator.set(acc)orVisitors()to useaccumulator.get()with a null guardclone()to create a freshThreadLocalfor cloned instancesHow to Reproduce
DeclarativeRecipeinstance for the same recipe IDUpgradeSpringBoot_3_4) concurrently via two paths — one direct catalog run, one wrapped in a builder/YAML declarative recipeTest Plan
rewrite-core:testpasses