Skip to content

Fix ConcurrentModificationException in DeclarativeRecipe#7066

Merged
natedanner merged 1 commit intomainfrom
natedanner/fix-cme-data-tables
Mar 20, 2026
Merged

Fix ConcurrentModificationException in DeclarativeRecipe#7066
natedanner merged 1 commit intomainfrom
natedanner/fix-cme-data-tables

Conversation

@natedanner
Copy link
Contributor

@natedanner natedanner commented Mar 19, 2026

Summary

  • DeclarativeRecipe.recipeList and preconditions are mutable ArrayList fields that are populated during initialize() (via clear() + add()) and concurrently iterated by getDataTableDescriptors(), createRecipeDescriptor(), and getRecipeList()
  • Under concurrent access from virtual threads calling describe()/getDescriptor() on the same recipe instance, this causes ConcurrentModificationException
  • Fix: make both fields volatile and assign immutable list snapshots from initialize(). Readers always see a complete, immutable list — no CME possible, and no risk of duplicate entries from interleaved initialize() calls

Why not align with Recipe.getRecipeList() (fresh list per call)?

The base Recipe.getRecipeList() builds a new list every call via buildRecipeList() — inherently thread-safe, no shared state. We considered making DeclarativeRecipe follow this pattern, but it isn't feasible: DeclarativeRecipe resolves LazyLoadedRecipe references by name during initialize(), which requires the availableRecipes context that is only available at startup. The recipe list is data-driven (YAML), not code-driven, so it must be built once and cached. The volatile + immutable swap gives the same thread-safety guarantees while preserving this lifecycle.

Changes

  • recipeList and preconditions declared as volatile, initialized to Collections.emptyList()
  • initialize(List, List, Function, Set) refactored to initialize(List, Function, Set) — builds a new ArrayList internally, returns Collections.unmodifiableList(result)
  • Callers assign the returned list directly: recipeList = initialize(...)
  • No changes needed to iteration sites — they now iterate immutable snapshots

Test plan

  • Added getDataTableDescriptorsThreadSafe — concurrent reader/writer threads that reproduces the CME
  • Added concurrentInitializeDoesNotDuplicateRecipes — concurrent initialize() calls don't produce duplicate entries
  • Verified both tests fail without the fix, pass with it
  • All existing DeclarativeRecipeTest tests pass

@github-project-automation github-project-automation bot moved this to In Progress in OpenRewrite Mar 19, 2026
@natedanner natedanner force-pushed the natedanner/fix-cme-data-tables branch from 8427df5 to 415052a Compare March 19, 2026 22:56
@natedanner natedanner marked this pull request as ready for review March 19, 2026 22:59
@natedanner natedanner requested a review from shanman190 March 19, 2026 22:59
@natedanner natedanner force-pushed the natedanner/fix-cme-data-tables branch 2 times, most recently from f723811 to 4f1baa1 Compare March 20, 2026 17:27
@github-project-automation github-project-automation bot moved this from In Progress to Ready to Review in OpenRewrite Mar 20, 2026
Use volatile fields with immutable list snapshots for recipeList and
preconditions. The initialize() method now builds a new list and assigns
it atomically via volatile write, so concurrent readers always see a
complete, immutable snapshot — no ConcurrentModificationException and
no risk of duplicate entries from interleaved initialize() calls.
@natedanner natedanner force-pushed the natedanner/fix-cme-data-tables branch from 4f1baa1 to 24873ce Compare March 20, 2026 17:50
@natedanner natedanner merged commit d683c5f into main Mar 20, 2026
1 check failed
@natedanner natedanner deleted the natedanner/fix-cme-data-tables branch March 20, 2026 18:13
@github-project-automation github-project-automation bot moved this from Ready to Review to Done in OpenRewrite Mar 20, 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.

2 participants