This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new c5f81e3af15 Safely publish lazily-initialized caches shared across
threads (#19137)
c5f81e3af15 is described below
commit c5f81e3af15208fdd07d02af3ea6d9350c20eb76
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Jul 31 17:44:16 2026 -0700
Safely publish lazily-initialized caches shared across threads (#19137)
---
.../common/request/context/LiteralContext.java | 22 +++++++++------
.../request/context/predicate/BaseInPredicate.java | 29 +++++++++++++-------
.../context/predicate/RegexpLikePredicate.java | 27 +++++++++++++++----
.../pinot/common/utils/regex/JavaUtilPattern.java | 2 +-
.../pinot/common/utils/regex/Re2jPattern.java | 2 +-
.../pinot/segment/spi/index/AbstractIndexType.java | 31 ++++++++++++++++------
.../apache/pinot/spi/data/DateTimeFieldSpec.java | 9 +++++--
7 files changed, 87 insertions(+), 35 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/request/context/LiteralContext.java
b/pinot-common/src/main/java/org/apache/pinot/common/request/context/LiteralContext.java
index 0dc08747195..744cdca228f 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/request/context/LiteralContext.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/request/context/LiteralContext.java
@@ -44,14 +44,20 @@ public class LiteralContext {
/// This is used for type conversion, and is not included in {@link #equals}
and {@link #hashCode}.
private final PinotDataType _pinotDataType;
- private Boolean _booleanValue;
- private Integer _intValue;
- private Long _longValue;
- private Float _floatValue;
- private Double _doubleValue;
- private BigDecimal _bigDecimalValue;
- private String _stringValue;
- private byte[] _bytesValue;
+ /// Lazily converted caches of the value, one per stored type.
+ ///
+ /// `volatile` is required, not just for the null checks in the getters: a
literal belongs to the query's expression
+ /// tree, which is built once per query and then read concurrently by the
threads that build and run the per-segment
+ /// plans. Without it the values are published unsafely, and a racing thread
can read a non-null reference whose
+ /// contents are not yet visible to it.
+ private volatile Boolean _booleanValue;
+ private volatile Integer _intValue;
+ private volatile Long _longValue;
+ private volatile Float _floatValue;
+ private volatile Double _doubleValue;
+ private volatile BigDecimal _bigDecimalValue;
+ private volatile String _stringValue;
+ private volatile byte[] _bytesValue;
public LiteralContext(Literal literal) {
switch (literal.getSetField()) {
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/BaseInPredicate.java
b/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/BaseInPredicate.java
index 926dd42bda3..0bbe77c14fe 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/BaseInPredicate.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/BaseInPredicate.java
@@ -27,19 +27,28 @@ import org.apache.pinot.spi.utils.BytesUtils;
import org.apache.pinot.spi.utils.TimestampUtils;
-/// Base predicate for IN and NOT_IN.
+/// Base predicate for `IN` and `NOT_IN`.
+///
+/// Instances are read concurrently: a predicate belongs to the query's filter
tree, which is built once per query and
+/// then shared by the threads that build and run the per-segment plans. The
lazily parsed value caches are therefore
+/// published safely; see the fields.
public abstract class BaseInPredicate extends BasePredicate {
protected final List<String> _values;
- // Cache the parsed values
- private int[] _intValues;
- private long[] _longValues;
- private float[] _floatValues;
- private double[] _doubleValues;
- private BigDecimal[] _bigDecimalValues;
- private int[] _booleanValues;
- private long[] _timestampValues;
- private ByteArray[] _bytesValues;
+ /// Lazily parsed caches of the values, one per stored type.
+ ///
+ /// `volatile` is required, not just for the null checks in the getters: a
predicate is read concurrently (see the
+ /// class javadoc), and without it the arrays are published unsafely. A
racing thread can then read the non-null
+ /// array reference while the element writes are still invisible to it,
seeing `0` for the primitive arrays and
+ /// `null` for the object arrays — which silently matches the wrong rows
rather than failing.
+ private volatile int[] _intValues;
+ private volatile long[] _longValues;
+ private volatile float[] _floatValues;
+ private volatile double[] _doubleValues;
+ private volatile BigDecimal[] _bigDecimalValues;
+ private volatile int[] _booleanValues;
+ private volatile long[] _timestampValues;
+ private volatile ByteArray[] _bytesValues;
public BaseInPredicate(ExpressionContext lhs, List<String> values) {
super(lhs);
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/RegexpLikePredicate.java
b/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/RegexpLikePredicate.java
index d6167b2add1..e7ca7566f4b 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/RegexpLikePredicate.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/request/context/predicate/RegexpLikePredicate.java
@@ -24,11 +24,22 @@ import
org.apache.pinot.common.utils.RegexpPatternConverterUtils;
import org.apache.pinot.common.utils.regex.Pattern;
import org.apache.pinot.common.utils.regex.PatternFactory;
-/// Predicate for REGEXP_LIKE with optional match parameters
+/// Predicate for `REGEXP_LIKE` with optional match parameters.
+///
+/// Instances are read concurrently: a predicate belongs to the query's filter
tree, which is built once per query and
+/// then shared by the threads that build and run the per-segment plans. The
lazily compiled pattern is therefore
+/// published safely; see [#getPattern].
public class RegexpLikePredicate extends BasePredicate {
private final String _value;
private final boolean _caseInsensitive;
- private Pattern _pattern = null;
+
+ /// Lazily compiled cache of [#getPattern].
+ ///
+ /// `volatile` is required, not just for the null check in [#getPattern]:
without it the pattern is published
+ /// unsafely, and a racing thread can read the non-null reference while the
fields written by the pattern's
+ /// construction are still invisible to it. Using such a pattern to create a
matcher fails with a
+ /// `NullPointerException`.
+ private volatile Pattern _pattern;
public RegexpLikePredicate(ExpressionContext lhs, String value) {
super(lhs);
@@ -55,11 +66,17 @@ public class RegexpLikePredicate extends BasePredicate {
return _caseInsensitive;
}
+ /// Returns the compiled pattern, lazily compiling and caching it on first
access.
+ ///
+ /// Uses the racy-single-check idiom: two threads may each compile the
pattern, but both compile the same one, so
+ /// the duplicate work is harmless. Correctness relies on `_pattern` being
`volatile`; see the field for why.
public Pattern getPattern() {
- if (_pattern == null) {
- _pattern = PatternFactory.compile(_value, _caseInsensitive);
+ Pattern pattern = _pattern;
+ if (pattern == null) {
+ pattern = PatternFactory.compile(_value, _caseInsensitive);
+ _pattern = pattern;
}
- return _pattern;
+ return pattern;
}
@Override
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/JavaUtilPattern.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/JavaUtilPattern.java
index 88d61647240..48d68b6512a 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/JavaUtilPattern.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/JavaUtilPattern.java
@@ -21,7 +21,7 @@ package org.apache.pinot.common.utils.regex;
import java.util.regex.Pattern;
public class JavaUtilPattern implements
org.apache.pinot.common.utils.regex.Pattern {
- final Pattern _pattern;
+ private final Pattern _pattern;
public JavaUtilPattern(String regex) {
_pattern = Pattern.compile(regex, 0);
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/Re2jPattern.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/Re2jPattern.java
index 24e7e819e04..2f52d9131ae 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/Re2jPattern.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/regex/Re2jPattern.java
@@ -22,7 +22,7 @@ import com.google.re2j.Pattern;
public class Re2jPattern implements
org.apache.pinot.common.utils.regex.Pattern {
- Pattern _pattern;
+ private final Pattern _pattern;
public Re2jPattern(String regex) {
this(regex, false);
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/AbstractIndexType.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/AbstractIndexType.java
index ced61841acf..09ee848c360 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/AbstractIndexType.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/AbstractIndexType.java
@@ -39,8 +39,15 @@ public abstract class AbstractIndexType<C extends
IndexConfig, IR extends IndexR
implements IndexType<C, IR, IC> {
private final String _id;
- private ColumnConfigDeserializer<C> _deserializer;
- private IndexReaderFactory<IR> _readerFactory;
+
+ /// Lazily created caches of [#getConfig] and [#getReaderFactory].
+ ///
+ /// `volatile` is required, not just for the null checks in those methods:
an index type is a process-wide singleton
+ /// held by [IndexService] and is used concurrently by the threads that
load, reload and refresh segments. Without
+ /// `volatile` these values are published unsafely, so a racing thread can
read the non-null reference while the
+ /// contents are not yet visible to it.
+ private volatile ColumnConfigDeserializer<C> _deserializer;
+ private volatile IndexReaderFactory<IR> _readerFactory;
protected ColumnConfigDeserializer<C> createDeserializer() {
ColumnConfigDeserializer<C> fromIndexes =
@@ -70,22 +77,30 @@ public abstract class AbstractIndexType<C extends
IndexConfig, IR extends IndexR
@Override
public Map<String, C> getConfig(TableConfig tableConfig, Schema schema) {
- if (_deserializer == null) {
- _deserializer = createDeserializer();
+ ColumnConfigDeserializer<C> deserializer = _deserializer;
+ if (deserializer == null) {
+ deserializer = createDeserializer();
+ _deserializer = deserializer;
}
try {
- return _deserializer.deserialize(tableConfig, schema);
+ return deserializer.deserialize(tableConfig, schema);
} catch (MergedColumnConfigDeserializer.ConfigDeclaredTwiceException ex) {
throw new
MergedColumnConfigDeserializer.ConfigDeclaredTwiceException(ex.getColumn(),
this, ex);
}
}
+ /// Returns the reader factory, lazily creating and caching it on first
access.
+ ///
+ /// Uses the racy-single-check idiom: two threads may each create a factory,
but the factories are equivalent, so
+ /// the duplicate work is harmless. Correctness relies on `_readerFactory`
being `volatile`; see the field for why.
@Override
public IndexReaderFactory<IR> getReaderFactory() {
- if (_readerFactory == null) {
- _readerFactory = createReaderFactory();
+ IndexReaderFactory<IR> readerFactory = _readerFactory;
+ if (readerFactory == null) {
+ readerFactory = createReaderFactory();
+ _readerFactory = readerFactory;
}
- return _readerFactory;
+ return readerFactory;
}
public void convertToNewFormat(TableConfig tableConfig, Schema schema) {
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/data/DateTimeFieldSpec.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/DateTimeFieldSpec.java
index 959aecbda09..e284600ac0f 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/DateTimeFieldSpec.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/DateTimeFieldSpec.java
@@ -32,8 +32,13 @@ public final class DateTimeFieldSpec extends FieldSpec {
private String _format;
private String _granularity;
private Object _sampleValue;
- private transient DateTimeFormatSpec _formatSpec;
- private transient DateTimeGranularitySpec _granularitySpec;
+ /// Lazily derived caches of [#getFormatSpec] and [#getGranularitySpec].
+ ///
+ /// `volatile` is required, not just for the null checks in those getters: a
field spec belongs to a `Schema`, which
+ /// is cached and read concurrently by query threads. Without it these
values are published unsafely, so a racing
+ /// thread can read the non-null reference while the contents are not yet
visible to it.
+ private transient volatile DateTimeFormatSpec _formatSpec;
+ private transient volatile DateTimeGranularitySpec _granularitySpec;
public enum TimeFormat {
EPOCH, TIMESTAMP, SIMPLE_DATE_FORMAT
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]