Skip to content

Safely publish lazily-initialized caches shared across threads - #19137

Merged
Jackie-Jiang merged 1 commit into
apache:masterfrom
Jackie-Jiang:safe-publish-lazy-caches
Aug 1, 2026
Merged

Safely publish lazily-initialized caches shared across threads#19137
Jackie-Jiang merged 1 commit into
apache:masterfrom
Jackie-Jiang:safe-publish-lazy-caches

Conversation

@Jackie-Jiang

Copy link
Copy Markdown
Contributor

Summary

Several lazily-initialized caches on objects that are read by multiple threads are written to non-volatile fields, so their contents are published unsafely. On weakly-ordered hardware (e.g. AArch64/Graviton) a racing thread can observe the non-null reference while the writes that filled it are still invisible to it.

This is the same defect class as #19117 (DataSchema.getStoredColumnDataTypes), found by auditing for the rest of the pattern. Every getter changed here already used the racy-single-check idiom — read the field into a local, check it, publish it — so only the volatile was missing.

Changes, most to least impactful

BaseInPredicate — the eight lazily parsed value arrays. A predicate belongs to the query's filter tree, which is shared across the threads CombinePlanNode uses to build per-segment plans in parallel; the arrays are reached from InPredicateEvaluatorFactory, NotInPredicateEvaluatorFactory and PredicateUtils. Arrays get no final-field protection, so a racing thread can read the array reference with its elements still at 0 — silently matching the wrong rows rather than failing. IN is common enough that this is the main motivation for the change.

RegexpLikePredicate and Re2jPattern — the compiled pattern, shared the same way. This is only exposed when the RE2J engine is configured: JavaUtilPattern holds its delegate in a final field, so JLS 17.5 covers it, whereas Re2jPattern did not. Re2jPattern._pattern is now final as well, which closes the hazard at its source for every consumer rather than only for this caller.

LiteralContext — the eight lazily converted values, in the same shared expression tree.

AbstractIndexType — an index type is a process-wide singleton held by IndexService, used concurrently by the threads that load, reload and refresh segments.

DateTimeFieldSpec — the format and granularity specs hang off a cached Schema that query threads read concurrently.

Why volatile uniformly rather than case by case

Some of these values are safe today without volatile, because the cached object happens to have only final fields — JLS 17.5 covers those, and everything reachable from them, even through a data race. That is a poor thing to depend on: it makes one class's thread-safety hinge on the field modifiers of a class in another module, with nothing at either site recording the dependency, and it breaks silently when a field is added or a constructor is bypassed. The two Pattern implementations here already disagreed on exactly that point. So the fields are made volatile uniformly, and the finality argument is used only to judge which of these is urgent — not whether to fix it.

Also

JavaUtilPattern._pattern and Re2jPattern._pattern are narrowed to private. Nothing outside those classes referenced them — both callers go through getPattern() — and the two classes are otherwise mirror images, so this keeps them consistent.

No tests are included: an unsafe-publication race is not reliably reproducible in a unit test without a jcstress-style harness, which this repo does not currently have. This matches how #19117 was handled.

Several lazily-initialized caches are read concurrently but written to non-volatile fields, so their
contents are published unsafely. On weakly-ordered hardware (e.g. AArch64) a racing thread can observe
the non-null reference while the writes that filled it are still invisible to it.

- `BaseInPredicate`: the eight parsed-value arrays. A predicate belongs to the query's filter tree,
  which is shared while `CombinePlanNode` builds the per-segment plans in parallel; the arrays are
  reached from `InPredicateEvaluatorFactory` / `NotInPredicateEvaluatorFactory` / `PredicateUtils`.
  Arrays get no final-field protection, so a stale read yields `0` for the primitive arrays and
  silently matches the wrong rows rather than failing.
- `LiteralContext`: the eight lazily converted values, shared the same way.
- `RegexpLikePredicate`: the compiled pattern. Only exposed when the RE2J engine is configured, since
  `Re2jPattern` held its delegate in a non-final field while `JavaUtilPattern` did not; that field is
  now final too, which fixes the hazard at its source.
- `AbstractIndexType`: an index type is a process-wide singleton held by `IndexService`, used
  concurrently by the threads that load, reload and refresh segments.
- `DateTimeFieldSpec`: the format and granularity specs hang off a cached `Schema` that query threads
  read concurrently.

Every one of these getters already used the racy-single-check idiom; only the `volatile` was missing.
The fields are made `volatile` uniformly rather than case by case: whether a given cached value happens
to be safe today depends on the field modifiers of a class in another module, which is not an invariant
worth depending on.

Also narrows the two `Pattern` wrappers' delegate fields to `private`, which nothing outside those
classes referenced.
@Jackie-Jiang Jackie-Jiang added the bug Something is not working as expected label Jul 31, 2026
@codecov-commenter

codecov-commenter commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.61%. Comparing base (a3471b0) to head (e889c27).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19137      +/-   ##
============================================
- Coverage     65.61%   65.61%   -0.01%     
  Complexity     1423     1423              
============================================
  Files          3439     3439              
  Lines        218184   218189       +5     
  Branches      34660    34663       +3     
============================================
- Hits         143161   143155       -6     
- Misses        63465    63480      +15     
+ Partials      11558    11554       -4     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 65.61% <100.00%> (-0.01%) ⬇️
temurin 65.61% <100.00%> (-0.01%) ⬇️
unittests 65.60% <100.00%> (-0.01%) ⬇️
unittests1 56.99% <100.00%> (-0.01%) ⬇️
unittests2 37.96% <26.66%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jackie-Jiang
Jackie-Jiang merged commit c5f81e3 into apache:master Aug 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something is not working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants