Skip to content

Replace DeclarativeRecipe ThreadLocal accumulator with cursor-based lookup#6936

Closed
pstreef wants to merge 2 commits intoopenrewrite:mainfrom
pstreef:fix/declarative-recipe-threadlocal-removal
Closed

Replace DeclarativeRecipe ThreadLocal accumulator with cursor-based lookup#6936
pstreef wants to merge 2 commits intoopenrewrite:mainfrom
pstreef:fix/declarative-recipe-threadlocal-removal

Conversation

@pstreef
Copy link
Copy Markdown
Contributor

@pstreef pstreef commented Mar 11, 2026

Problem

The orVisitors() method reads the accumulator from the ThreadLocal to resolve scanning precondition visitors. After a thread switch, accumulator.get() returns null, causing scanning preconditions to receive null accumulators. This affects any DeclarativeRecipe that uses a ScanningRecipe as a precondition.

Solution

The accumulator is already stored on the rootCursor by ScanningRecipe.getAccumulator(), which is per-run and survives thread switches. The ThreadLocal was redundant secondary storage.

  • Remove ThreadLocal<Accumulator> entirely
  • Defer precondition resolution to visit time when the rootCursor is available (via setCursor() set by RecipeRunCycle)
  • orVisitors() reads the accumulator from the cursor via getAccumulator(rootCursor, ctx) instead of ThreadLocal
  • Remove onComplete() ThreadLocal cleanup and simplify clone()

pstreef added 2 commits March 11, 2026 09:41
…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.
Each getVisitor() call creates a new single-threaded instance, so
caching the resolved visitor within it is safe and avoids redundant
precondition tree walks on every isAcceptable/visit call.
@pstreef
Copy link
Copy Markdown
Contributor Author

pstreef commented Mar 11, 2026

Closing this — after deeper investigation of the worker's yield mechanics, the ThreadLocal approach from #6810 is correct.

The rootCursor is preserved across yield boundaries within the same cycle, and scan/edit happen within the same RecipeRunCycle batch on the same thread. The ThreadLocal is set during scan and read during edit of the same batch. On resume after yield, a new batch does its own scan phase which sets the ThreadLocal fresh.

The actual fix for the yield-related accumulator loss was #6913 (delegating getAccumulator in BellwetherDecoratedScanningRecipe to the wrapped recipe's stable UUID).

@pstreef pstreef closed this Mar 11, 2026
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant