Skip to content

Replace ThreadLocal with cursor-based accumulator in DeclarativeRecipe#7225

Merged
knutwannheden merged 1 commit intomainfrom
fix/declarative-recipe-threadlocal
Apr 1, 2026
Merged

Replace ThreadLocal with cursor-based accumulator in DeclarativeRecipe#7225
knutwannheden merged 1 commit intomainfrom
fix/declarative-recipe-threadlocal

Conversation

@knutwannheden
Copy link
Copy Markdown
Contributor

@knutwannheden knutwannheden commented Apr 1, 2026

Motivation

When a ScanningRecipe is used as a precondition in a DeclarativeRecipe, the accumulator is stored in a ThreadLocal during the scan phase and read back in orVisitors() during the edit phase. This means the scan and edit phases must run on the same thread — otherwise the ThreadLocal is null, resulting in NPE for any ScanningRecipe precondition that uses its accumulator in getVisitor().

This affects ModuleHasDependency, RepositoryHasDependency, HasNoJakartaAnnotations, and similar recipes when used as preconditions. The error manifests as all files in a run failing (not intermittent), because once a thread switch occurs between scan and edit phases, the entire edit phase runs on the new thread where the ThreadLocal is empty.

ScanningRecipe already has a thread-safe mechanism for storing accumulators: getAccumulator(Cursor, ExecutionContext) stores them on the root cursor keyed by UUID (line 94-96). This survives across threads since the cursor is passed through RecipeRunCycle boundaries. DeclarativeRecipe should use this same mechanism instead of a ThreadLocal.

Summary

  • Remove ThreadLocal<Accumulator> from DeclarativeRecipe
  • PreconditionBellwether now lazily resolves precondition visitors in visit(), where both the cursor and ExecutionContext are available
  • orVisitors() takes Cursor + ExecutionContext and uses getAccumulator(rootCursor, ctx) — the same UUID-keyed cursor message mechanism that ScanningRecipe.getVisitor() already uses
  • Remove onComplete() (only cleared the ThreadLocal) and simplify clone()

No public API changes.

Test plan

…veRecipe

When a ScanningRecipe is used as a precondition in a DeclarativeRecipe,
the accumulator was stored in a ThreadLocal during the scan phase and
read back in orVisitors() during the edit phase. Worker yielding could
cause the edit phase to resume on a different thread, where the
ThreadLocal was null, resulting in NPE.

The fix uses the existing cursor-based accumulator storage mechanism
(ScanningRecipe.getAccumulator) which stores accumulators on the root
cursor keyed by UUID. This survives across threads since the cursor is
passed through RecipeRunCycle boundaries.

Changes:
- Remove ThreadLocal<Accumulator> field
- PreconditionBellwether now lazily resolves precondition visitors in
  visit() where cursor and ExecutionContext are available
- orVisitors() takes Cursor + ExecutionContext parameters and uses
  getAccumulator(rootCursor, ctx) instead of ThreadLocal
- Remove onComplete() that only cleared the ThreadLocal
- Simplify clone() which only copied the ThreadLocal

Fixes #7159
@github-project-automation github-project-automation bot moved this to In Progress in OpenRewrite Apr 1, 2026
@knutwannheden knutwannheden changed the title Replace ThreadLocal with cursor-based accumulator in DeclarativeRecipe Replace ThreadLocal with cursor-based accumulator in DeclarativeRecipe Apr 1, 2026
@github-project-automation github-project-automation bot moved this from In Progress to Ready to Review in OpenRewrite Apr 1, 2026
@knutwannheden knutwannheden merged commit e3a8798 into main Apr 1, 2026
1 check passed
@knutwannheden knutwannheden deleted the fix/declarative-recipe-threadlocal branch April 1, 2026 08:35
@github-project-automation github-project-automation bot moved this from Ready to Review to Done in OpenRewrite Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

DeclarativeRecipe ThreadLocal accumulator causes NPE for ScanningRecipe preconditions in multi-threaded execution

2 participants