Skip to content

Cache entity codecs in a ConcurrentHashMap (plan item 2.8) - #4397

Merged
evanchooly merged 1 commit into
masterfrom
codec-provider-map
Oct 15, 2026
Merged

evanchooly merged 1 commit into
masterfrom
codec-provider-map

Conversation

@evanchooly

Copy link
Copy Markdown
Member

Plan item 2.8 (review D8). This completes the Phase 2 fixes. 2.5 ships with 3.8, as the plan says.

The bug

MorphiaCodecProvider.codecs was a plain HashMap, and get mutated it on every miss. With concurrent first use of an entity type, a HashMap can be corrupted. Even when it isn't, every thread that missed built its own MorphiaCodec and stored it, overwriting the others, so callers ended up holding different codec instances for the same type.

Change

codecs is a ConcurrentHashMap. On a miss, get builds the codec and stores it with putIfAbsent. If another thread stored one first, it returns that one, so every caller shares a single codec per type.

It doesn't use computeIfAbsent, as the plan says: building a codec looks up other codecs, and those lookups can land back in this provider, which isn't allowed inside a ConcurrentHashMap compute.

Tests

CodecHarnessTest.concurrentCodecLookupsShareOneCodec runs under both mappers. For 20 fresh providers, 8 threads released together call provider.get(HarnessScalars.class, registry), and the test asserts that all of them get the same codec. It failed before the fix: "REFLECTION: every lookup should get the one cached codec".

The core and Kotlin suites pass with -Dmorphia.mapper=critter and with reflection: 1483 + 5 tests, 0 failures.

Decisions to review

  1. Recursive self-lookups now keep the inner codec. If building a type's codec recursively looks up that same type, the inner lookup stores its codec first, and the outer one is discarded. Before, the outer one overwrote it. Both are fully built codecs for the same model.

MorphiaCodecProvider cached codecs in a plain HashMap that concurrent
lookups mutated, and each thread that missed built and stored its own
codec. It now uses a ConcurrentHashMap with putIfAbsent, so every caller
shares the first codec stored. Not computeIfAbsent, because building a
codec looks up other codecs that can land back in this provider.
Copilot AI balanced review requested due to automatic review settings October 15, 2026 03:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused change safely handles concurrent cache insertion without restricting recursive construction, and no blocking issues remain.

0 open findings

What changed in this PR

Makes Morphia’s entity codec cache thread-safe while preserving recursive codec construction.

Changes:

  • Uses ConcurrentHashMap and putIfAbsent to return one cached codec per type.
  • Adds concurrent lookup coverage for both mappers.
  • Marks plan item 2.8 complete.
File Description
design/​mapping-game-plan.md Marks the cache concurrency fix complete.
core/​src/​test/​java/​dev/​morphia/​test/​mapping/​codec/​harness/​CodecHarnessTest.java Tests shared codec identity across concurrent lookups.
core/​src/​main/​java/​dev/​morphia/​mapping/​codec/​MorphiaCodecProvider.java Atomically caches codecs and returns the winning instance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@evanchooly
evanchooly merged commit 30554c3 into master Oct 15, 2026
5 checks passed
@evanchooly
evanchooly deleted the codec-provider-map branch October 15, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants