raghavyadav01 commented on code in PR #19093:
URL: https://github.com/apache/pinot/pull/19093#discussion_r3960628759
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java:
##########
@@ -152,6 +156,62 @@ public JsonIndexReader getSparseJsonIndex() {
return _sparseDataSource != null ? _sparseDataSource.getJsonIndex() : null;
}
+ @SuppressWarnings("unchecked")
+ @Nullable
+ @Override
+ public Map<String, Object> getMapValue(int docId) {
Review Comment:
Can a minion task actually reach this? `PinotSegmentRecordReader` iterates
`getPhysicalColumnNames()`, which includes the `col$key` / `col$__sparse__`
children, and the immutable segment has no DataSource for those, so the reader
throws on the first child before it gets to the parent. Worth skipping
`isMaterializedChild()` columns in `addColumnReader` and adding a
sealed-segment round-trip test, otherwise this path is only covered by mocks.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java:
##########
@@ -63,12 +63,20 @@ public class MutableOpenStructIndex implements
OpenStructIndexReader<ForwardInde
private final PinotDataBufferMemoryManager _memoryManager;
private final int _capacity;
- // Volatile for lock-free reader access; writer always holds the
consuming-thread lock.
+ // Volatile copy-on-write: the writer (consuming thread) creates a fresh
HashMap copy and publishes
+ // atomically via volatile write (see allocateKeyColumn). Readers see a
consistent snapshot of the
+ // entire map. ConcurrentHashMap is NOT appropriate here — it would allow
readers to observe
+ // partially-updated state during a put. Single-writer is guaranteed by the
Pinot consuming thread
+ // model (one thread per partition).
private volatile Map<String, MutableKeyColumn> _keyColumns = new HashMap<>();
// Single-writer (see #index), but close() may run on a different thread, so
volatile for
// visibility; flushed to ServerMetrics on close() to avoid a metered-value
call on every
// ignored key of every consumed row.
private volatile long _ignoredKeyDropCount;
+ // Batched for the same reason as _ignoredKeyDropCount: keep a metered-value
call, which rebuilds
+ // the metric name and hits the registry, off the per-row consuming path.
Flushed in close().
+ private volatile long _typeCoercionFailureCount;
Review Comment:
Doesn't batching these to `close()` hide the signal for the whole
consuming-segment lifetime, and lose it if the server dies first? Could we
cache the `PinotMeter` and use the `addMeteredTableValue(..., reusedMeter)`
overload instead? That avoids the name rebuild but keeps the metric live.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/ImmutableOpenStructDataSource.java:
##########
@@ -152,6 +156,62 @@ public JsonIndexReader getSparseJsonIndex() {
return _sparseDataSource != null ? _sparseDataSource.getJsonIndex() : null;
}
+ @SuppressWarnings("unchecked")
+ @Nullable
+ @Override
+ public Map<String, Object> getMapValue(int docId) {
+ Map<String, Object> result = null;
+
+ for (Map.Entry<String, DataSource> entry : _perKeyDataSources.entrySet()) {
+ Object value = readValue(entry.getKey(), entry.getValue(), docId);
+ if (value != null) {
+ if (result == null) {
+ result = new HashMap<>();
+ }
+ result.put(entry.getKey(), value);
+ }
+ }
+
+ if (_sparseDataSource != null) {
+ Object sparseValue = readValue(_fieldSpec.getName(), _sparseDataSource,
docId);
+ if (sparseValue instanceof String) {
+ String json = (String) sparseValue;
+ if (!json.isEmpty()) {
+ try {
+ Map<String, Object> sparseMap = JsonUtils.stringToObject(json,
Map.class);
+ if (result == null) {
+ result = new HashMap<>();
+ }
+ result.putAll(sparseMap);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to parse sparse JSON at docId "
+ docId, e);
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /// Reads the value of `key` at `docId`, or `null` when the doc is null or
the column has no
+ /// forward index. Delegates the null-vector check and the dictionary/raw
per-type read dispatch to
+ /// [PinotSegmentColumnReader] rather than re-deriving them here, so this
path cannot drift from the
+ /// reader every other column read in the engine already goes through.
OPEN_STRUCT child columns are
+ /// always single-valued, hence the 0 maxNumValuesPerMVEntry.
+ @Nullable
+ private static Object readValue(String key, DataSource dataSource, int
docId) {
+ ForwardIndexReader<?> fwdReader = dataSource.getForwardIndex();
+ if (fwdReader == null) {
+ return null;
+ }
+ try (PinotSegmentColumnReader reader = new PinotSegmentColumnReader(key,
fwdReader, dataSource.getDictionary(),
Review Comment:
This builds a new `PinotSegmentColumnReader` (and so a new
`ForwardIndexReaderContext`) per key per doc. For the raw LZ4 sparse column
that's a direct buffer alloc and a full chunk re-decompression on every doc,
and both callers are whole-segment loops. Can we hold one reader per key for
the scan instead? Same concern I raised on #18643, deferred then because the
immutable side had no `getMapValue`.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java:
##########
@@ -119,7 +127,8 @@ public void index(int docId, @Nullable Object value) {
// Resolve stored type and coerce BEFORE allocating a column so a
first-row coercion failure
// does not allocate a column that was never usable.
DataType resolvedType = resolveStoredType(key, rawValue, null);
- Object coerced = tryCoerce(key, rawValue, resolvedType);
+ Object coerced = tryCoerce(key, rawValue,
+ ColumnDataType.fromDataTypeSV(resolvedType).toPinotDataType());
Review Comment:
Now that `fromDataTypeSV(...)` sits outside `tryCoerce`'s try/catch, a child
spec declared as LIST/STRUCT/UNKNOWN (child types aren't validated anywhere)
throws out of `index()` and errors the whole row, where before it was a counted
coercion failure. The splitter still has the same expression inside its try.
Keep it inside for the fresh-key path, or reject those child types in
`OpenStructIndexType.validate`?
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java:
##########
@@ -575,14 +572,20 @@ private void writeSparseJsonColumn(List<String>
sparseKeys)
try {
String json = JsonUtils.objectToString(sparseEntries);
jsonPerDoc[docId] = json;
- maxLen = Math.max(maxLen,
json.getBytes(StandardCharsets.UTF_8).length);
- nonNullCount++;
+ maxLen = Math.max(maxLen, Utf8.encodedLength(json));
} catch (IOException e) {
throw new RuntimeException("Failed to serialize sparse entries for
docId " + docId, e);
}
}
}
+ // Absent docs store "" in the raw forward index (see loop below) and are
flagged in the null vector, so feed
+ // the same placeholder through the stats collector and record it as the
default null value. Collected inside
+ // the write loop rather than in a pass of its own: it needs the exact
same per-doc branch.
+ DimensionFieldSpec sparseFieldSpec = new DimensionFieldSpec(sparseCol,
DataType.STRING, true);
+ String defaultValue = "";
Review Comment:
The comment says "" is recorded as the default null value, but
`sparseFieldSpec` still carries the STRING default `"null"`, and that's what
ends up in `DEFAULT_NULL_VALUE`. Dense keys keep disk and metadata aligned
here. Either `setDefaultNullValue("")` on the spec or reword the comment?
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitter.java:
##########
@@ -575,14 +572,20 @@ private void writeSparseJsonColumn(List<String>
sparseKeys)
try {
String json = JsonUtils.objectToString(sparseEntries);
jsonPerDoc[docId] = json;
- maxLen = Math.max(maxLen,
json.getBytes(StandardCharsets.UTF_8).length);
- nonNullCount++;
+ maxLen = Math.max(maxLen, Utf8.encodedLength(json));
} catch (IOException e) {
throw new RuntimeException("Failed to serialize sparse entries for
docId " + docId, e);
}
}
}
+ // Absent docs store "" in the raw forward index (see loop below) and are
flagged in the null vector, so feed
+ // the same placeholder through the stats collector and record it as the
default null value. Collected inside
+ // the write loop rather than in a pass of its own: it needs the exact
same per-doc branch.
+ DimensionFieldSpec sparseFieldSpec = new DimensionFieldSpec(sparseCol,
DataType.STRING, true);
+ String defaultValue = "";
+ AbstractColumnStatisticsCollector statsCollector =
StatsCollectorUtil.createStatsCollector(sparseFieldSpec, null);
Review Comment:
`StringColumnPreIndexStatsCollector` keeps a set entry for every distinct
blob (near one per doc here) and sorts them all at seal, which is O(n log n)
string compares on the commit path for cardinality/min/max a raw column never
reads. Since the commit path is what the stacked PR is trimming, would a
minimal `ColumnStatistics` built from the loop you already have (lengths, doc
counts) fed into the same `addColumnMetadataInfo` call work?
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java:
##########
@@ -144,6 +144,10 @@ public ImmutableSegmentImpl(
for (String parent : openStructParents) {
FieldSpec fieldSpec = schema != null ? schema.getFieldSpecFor(parent)
: null;
if (!(fieldSpec instanceof ComplexFieldSpec)) {
+ LOGGER.warn("Skipping OPEN_STRUCT parent column '{}': schema is {}
or fieldSpec is {} "
Review Comment:
Nit: worth adding the segment name here since this fires per segment load,
and splitting the two cases. "schema is present or fieldSpec is
DimensionFieldSpec" reads oddly when only one applies.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructIndex.java:
##########
@@ -290,15 +299,30 @@ public DataType getStoredType() {
@Override
public void close()
throws IOException {
- if (_ignoredKeyDropCount > 0) {
- ServerMetrics serverMetrics = ServerMetrics.get();
- if (serverMetrics != null) {
- serverMetrics.addMeteredTableValue(_tableNameWithType,
_openStructColumn,
- ServerMeter.OPEN_STRUCT_IGNORED_KEY_DROPS, _ignoredKeyDropCount);
- }
- }
+ flushMeters();
for (MutableKeyColumn keyCol : _keyColumns.values()) {
keyCol.close();
}
}
+
+ /// Emits the batched ingestion counters. Counters accumulate per row on the
consuming path and
+ /// are flushed once here, mirroring what [OpenStructColumnSplitter] does at
seal time.
+ private void flushMeters() {
Review Comment:
Two small things: the counters aren't zeroed after emitting, so a second
`close()` double-counts, and `flushMeters()` runs before the key-column close
loop with no `finally`. Could we zero after emit and flush in a `finally`?
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/creator/impl/openstruct/OpenStructColumnSplitterTest.java:
##########
@@ -219,7 +220,17 @@ public void testSparseJsonColumnWritten()
}
s.seal();
String sparseCol = OpenStructNaming.sparseColumnName("metrics");
- assertTrue(s.getMaterializedColumnMetadata().containsKey(sparseCol));
+ PropertiesConfiguration props =
s.getMaterializedColumnMetadata().get(sparseCol);
+ assertNotNull(props);
+
+ // Regression: the sparse column's metadata used to omit several
properties ColumnMetadataImpl.
+ // fromPropertiesConfiguration() reads via config.getInt() with no default
(e.g. CARDINALITY). This branch's
+ // reader still tolerates a missing
bitsPerElement/lengthOfEachEntry/maxNumberOfMultiValues via UNAVAILABLE
+ // defaults, but the sibling early-release branch's stricter reader throws
NoSuchElementException on the same
Review Comment:
"The sibling early-release branch" won't mean anything to readers here. Also
`CARDINALITY` is the only no-default read and the old block already wrote it,
so this test passes on the pre-PR metadata. Could we assert what's actually new
instead, e.g. encoding RAW, cardinality 2 (old code wrote 1), longest element
length?
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/openstruct/MutableOpenStructDataSource.java:
##########
@@ -116,8 +116,9 @@ public boolean isFullyMaterialized() {
@Override
public Map<String, DataSource> getDataSources() {
+ Map<String, MutableKeyColumn> snapshot = _index.getKeyColumns();
Review Comment:
Rebase note: this snapshot doesn't change behaviour (`getDataSource(key)`
re-reads the live map, and the published map is never mutated), and it
conflicts with #19439 which simplified this loop to `result.put(key,
getDataSource(key))`. Probably simplest to take master's version.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]