Skip to content

fix(core): make Util.memoize thread-safe - #1406

Merged
benbellick merged 1 commit into
substrait-io:mainfrom
patientstreetlight:fix/thread-safe-memoize
Oct 9, 2026
Merged

benbellick merged 1 commit into
substrait-io:mainfrom
patientstreetlight:fix/thread-safe-memoize

Conversation

@patientstreetlight

Copy link
Copy Markdown
Contributor

Util.memoize stored its cached value and "computed" flag in plain
fields, so a thread racing the first call could observe the flag set
without the value and get null back, and concurrent callers could each
re-run the delegate. The memoizer backs function keys and anchors, the
ExtensionCollection lookup maps, and relation record types, so a shared
ExtensionCollection failed intermittently on first concurrent use with
"NullPointerException: key" or "Duplicate key null".

The flag is now volatile and initialization uses double-checked locking,
so the delegate runs exactly once and every caller sees the fully
computed value. The memoizer's delegate and AbstractRel's memoizer field
are now final so they are safely published along with their owner.

Closes #1389

Util.memoize stored its cached value and "computed" flag in plain
fields, so a thread racing the first call could observe the flag set
without the value and get null back, and concurrent callers could each
re-run the delegate. The memoizer backs function keys and anchors, the
ExtensionCollection lookup maps, and relation record types, so a shared
ExtensionCollection failed intermittently on first concurrent use with
"NullPointerException: key" or "Duplicate key null".

The flag is now volatile and initialization uses double-checked locking,
so the delegate runs exactly once and every caller sees the fully
computed value. The memoizer's delegate and AbstractRel's memoizer field
are now final so they are safely published along with their owner.

Closes substrait-io#1389
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: substrait-io/substrait-java/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 18c8a0f0-3a60-4a80-a6bd-ad49fd5501cd

📥 Commits

Reviewing files that changed from the base of the PR and between 73232c3 and 9d0859c.


📒 Files selected for processing (3)
  • core/src/main/java/io/substrait/relation/AbstractRel.java
  • core/src/main/java/io/substrait/util/Util.java
  • core/src/test/java/io/substrait/util/UtilTest.java

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.



📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Memoized values are now initialized safely when accessed concurrently, preventing duplicate supplier calls and ensuring callers receive the same cached result.
    • If a supplier throws an exception, a later call can retry initialization. Successful null results are also cached.

Walkthrough

Util.memoize now synchronizes initialization and safely publishes the first successful result. Added tests cover repeated calls, null results, and concurrent first access. AbstractRel.recordType is now final.

Changes

Memoized supplier

Layer / File(s) Summary
Memoizer initialization and validation
core/src/main/java/io/substrait/util/Util.java, core/src/main/java/io/substrait/relation/AbstractRel.java, core/src/test/java/io/substrait/util/UtilTest.java
Util.memoize documents thread-safe caching after a successful delegate call. Initialization is synchronized and rechecks the retrieved flag. Exceptions propagate without setting the flag, so a later call can retry. Tests verify single invocation for repeated and null results, and one shared result across eight concurrent callers. AbstractRel.recordType is declared final.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix · Severity of issue fixed: Medium


Merge Risk: ⚪ Minimal · up to 9d085

No actionable issue remains; the change is ready to merge after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title is concise, specific, and uses valid Conventional Commit format. It accurately identifies the main change: making Util.memoize thread-safe.
Description check Passed The description explains the concurrency failure, the affected memoizer behavior, the implemented fix, and the linked issue. It provides the rationale required by the template and is directly related …
Linked Issues check Passed Issue #1389 requires thread-safe memoization with safe publication and coordinated first initialization. Util.Memoizer now writes value before volatile retrieved, uses double-checked locking, an…
Out of Scope Changes check Passed The changes stay within issue #1389. The Util.memoize implementation and documentation address concurrent initialization and publication. The new UtilTest tests the required behavior. The `Abstrac…


  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@benbellick
benbellick merged commit 9042fea into substrait-io:main Oct 9, 2026
14 checks passed
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.

Util.memoize can return null under concurrent access

2 participants