Fix ConcurrentModificationException in DeclarativeRecipe#7066
Merged
natedanner merged 1 commit intomainfrom Mar 20, 2026
Merged
Fix ConcurrentModificationException in DeclarativeRecipe#7066natedanner merged 1 commit intomainfrom
natedanner merged 1 commit intomainfrom
Conversation
8427df5 to
415052a
Compare
f723811 to
4f1baa1
Compare
shanman190
approved these changes
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.
4f1baa1 to
24873ce
Compare
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.
Summary
DeclarativeRecipe.recipeListandpreconditionsare mutableArrayListfields that are populated duringinitialize()(viaclear()+add()) and concurrently iterated bygetDataTableDescriptors(),createRecipeDescriptor(), andgetRecipeList()describe()/getDescriptor()on the same recipe instance, this causesConcurrentModificationExceptionvolatileand assign immutable list snapshots frominitialize(). Readers always see a complete, immutable list — no CME possible, and no risk of duplicate entries from interleavedinitialize()callsWhy not align with
Recipe.getRecipeList()(fresh list per call)?The base
Recipe.getRecipeList()builds a new list every call viabuildRecipeList()— inherently thread-safe, no shared state. We considered makingDeclarativeRecipefollow this pattern, but it isn't feasible:DeclarativeReciperesolvesLazyLoadedRecipereferences by name duringinitialize(), which requires theavailableRecipescontext 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
recipeListandpreconditionsdeclared asvolatile, initialized toCollections.emptyList()initialize(List, List, Function, Set)refactored toinitialize(List, Function, Set)— builds a newArrayListinternally, returnsCollections.unmodifiableList(result)recipeList = initialize(...)Test plan
getDataTableDescriptorsThreadSafe— concurrent reader/writer threads that reproduces the CMEconcurrentInitializeDoesNotDuplicateRecipes— concurrentinitialize()calls don't produce duplicate entriesDeclarativeRecipeTesttests pass