Skip to content

Commit 188e93d

Browse files
committed
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.
1 parent 15ebbf2 commit 188e93d

1 file changed

Lines changed: 11 additions & 4 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,16 @@ private void initializeDeclarativeRecipe(DeclarativeRecipe declarativeRecipe, St
161161
}
162162
}
163163

164-
@SuppressWarnings("NotNullFieldNotInitialized")
165164
@JsonIgnore
166-
private transient Accumulator accumulator;
165+
private transient ThreadLocal<Accumulator> accumulator = new ThreadLocal<>();
167166

168167
@Override
169168
public Accumulator getInitialValue(ExecutionContext ctx) {
170169
Accumulator acc = new Accumulator();
171170
for (Recipe precondition : preconditions) {
172171
registerNestedScanningRecipes(precondition, acc, ctx);
173172
}
174-
accumulator = acc;
173+
accumulator.set(acc);
175174
return acc;
176175
}
177176

@@ -509,7 +508,8 @@ private TreeVisitor<?, ExecutionContext> orVisitors(Recipe recipe) {
509508
//noinspection rawtypes
510509
ScanningRecipe scanning = (ScanningRecipe) recipe;
511510
//noinspection unchecked
512-
conditions.add(scanning.getVisitor(accumulator.recipeToAccumulator.get(scanning)));
511+
Accumulator acc = accumulator.get();
512+
conditions.add(scanning.getVisitor(acc != null ? acc.recipeToAccumulator.get(scanning) : null));
513513
} else {
514514
conditions.add(recipe.getVisitor());
515515
}
@@ -602,6 +602,13 @@ private static class LazyLoadedRecipe extends Recipe {
602602
String description = "Recipe that is loaded lazily.";
603603
}
604604

605+
@Override
606+
public DeclarativeRecipe clone() {
607+
DeclarativeRecipe cloned = (DeclarativeRecipe) super.clone();
608+
cloned.accumulator = new ThreadLocal<>();
609+
return cloned;
610+
}
611+
605612
@Override
606613
protected RecipeDescriptor createRecipeDescriptor() {
607614
List<RecipeDescriptor> preconditionDescriptors = new ArrayList<>();

0 commit comments

Comments
 (0)