Jackie-Jiang commented on code in PR #19088:
URL: https://github.com/apache/pinot/pull/19088#discussion_r4449000588
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/dedup/PartitionDedupMetadataManager.java:
##########
@@ -55,6 +55,14 @@ default void replaceSegment(IndexSegment oldSegment,
IndexSegment newSegment) {
/// Remove the expired primary keys from the metadata when TTL is enabled.
void removeExpiredPrimaryKeys();
+ /// Returns true if the primary key is already present and within TTL. Does
not insert the key.
+ ///
+ /// Call this before writing a new consuming row so a failed write cannot
leave a ghost key that drops a later retry.
+ /// Default is absent so existing test doubles keep the previous
always-insert behavior until they override it.
+ default boolean isRecordPresent(DedupRecordInfo dedupRecordInfo) {
+ return false;
Review Comment:
[P1] Preserve deduplication for existing metadata-manager plugins
A custom metadata manager implementing the existing interface inherits false
here. MutableSegmentImpl now publishes the row before calling
checkRecordPresentOrUpdate(), and ignores that method's duplicate result. Thus
a previously working plugin can return true for an existing key and still
retain every duplicate after this change. These are supported production
extensions via DedupConfig.metadataManagerClass, not just test doubles. Please
preserve the old contract for implementations without the new capability, or
provide a compatible migration. Add a regression using a legacy implementation
that only overrides checkRecordPresentOrUpdate(), and ingest the same key twice.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java:
##########
@@ -674,82 +697,105 @@ public boolean index(GenericRow row, @Nullable
StreamMessageMetadata metadata)
boolean canTakeMore;
// NOTE: out-of-order records can not be dropped or marked when
consistent upsert view is enabled.
- // Since Indexing the record and updation of _numDocsIndexed counter
happens before updating the upsert
- // metadata, we wouldn't be able to actually drop or mark those records
as dropped. This order is important for
- // consistent upsert view, otherwise the latest doc can be missed by
query due to 'docId < _numDocs' check
- // in query filter operators. Here the record becomes queryable before
validDocIds bitmaps are updated.
- if (_upsertConsistencyMode != UpsertConfig.ConsistencyMode.NONE) {
- updateDictionary(updatedRow);
- addNewRow(numDocsIndexed, updatedRow);
- numDocsIndexed++;
- canTakeMore = numDocsIndexed < _capacity;
- _numDocsIndexed = numDocsIndexed;
- // Index the record and update _numDocsIndexed counter before updating
the upsert metadata so that the record
- // becomes queryable before validDocIds bitmaps are updated. This
order is important for consistent upsert view,
- // otherwise the latest doc can be missed by query due to 'docId <
_numDocs' check in query filter operators.
- // NOTE: out-of-order records can not be dropped or marked when
consistent upsert view is enabled.
- _partitionUpsertMetadataManager.addRecord(this, recordInfo);
- } else {
- // if record doesn't need to be dropped, then persist in segment and
update metadata hashmap
- // we are doing metadata update first followed by segment data update
here, there can be a scenario where
- // segment indexing or addNewRow call errors out in those scenario,
there can be metadata inconsistency where
- // a key is pointing to some other key's docID
- // TODO fix this metadata mismatch scenario
- boolean isOutOfOrderRecord =
!_partitionUpsertMetadataManager.addRecord(this, recordInfo);
+ // Peek first so drop/mark can skip the write without mutating
validDocIds. Then finish the physical row
+ // before addRecord so metadata never points at a hole (issue #16316).
+ if (!_partitionUpsertMetadataManager.isAcceptingRecords()) {
+ updateIndexedAndIngestionTime(metadata);
+ return canAddMore();
+ }
+ if (_upsertConsistencyMode == UpsertConfig.ConsistencyMode.NONE) {
+ boolean isOutOfOrderRecord =
_partitionUpsertMetadataManager.isOutOfOrderRecord(recordInfo);
if (_upsertOutOfOrderRecordColumn != null) {
updatedRow.putValue(_upsertOutOfOrderRecordColumn,
BooleanUtils.toInt(isOutOfOrderRecord));
}
- if (!isOutOfOrderRecord || !_upsertDropOutOfOrderRecord) {
- updateDictionary(updatedRow);
- addNewRow(numDocsIndexed, updatedRow);
- // Update number of documents indexed before handling the upsert
metadata so that the record becomes queryable
- // once validated
- numDocsIndexed++;
+ if (isOutOfOrderRecord && _upsertDropOutOfOrderRecord) {
+ // Not addRecord(): the check above ran without the key's lock, and
if the key's segment was removed since
+ // then, addRecord() would treat it as new and mark the unwritten
doc `numDocsIndexed` valid.
+ _partitionUpsertMetadataManager.dropOutOfOrderRecord(this,
recordInfo);
+ updateIndexedAndIngestionTime(metadata);
+ return canAddMore();
}
- canTakeMore = numDocsIndexed < _capacity;
- _numDocsIndexed = numDocsIndexed;
}
+ indexPhysicalRow(numDocsIndexed, updatedRow);
+ numDocsIndexed++;
+ canTakeMore = numDocsIndexed < _capacity;
+ _numDocsIndexed = numDocsIndexed;
+ // Index the record and update _numDocsIndexed counter before updating
the upsert metadata so that the record
+ // becomes queryable before validDocIds bitmaps are updated. This order
is important for consistent upsert view,
+ // otherwise the latest doc can be missed by query due to 'docId <
_numDocs' check in query filter operators.
+ _partitionUpsertMetadataManager.addRecord(this, recordInfo);
Review Comment:
[P1] Keep repaired upsert fields consistent with the metadata being claimed
recordInfo was captured before indexPhysicalRow(), but that method can
replace primary-key, comparison, or delete fields with defaults. This call then
claims metadata for the original values while publishing a different physical
row. For example, a recoverable dictionary failure on comparison value 20 can
store its default while metadata still claims version 20, causing a later valid
version 10 to be rejected; rebuilding metadata from the segment yields
different ordering. A repaired primary key similarly disagrees with the map
key, and a repaired delete flag can disagree with the queryable bitmap. Please
handle metadata-defining field failures explicitly: avoid validating a
substituted row under the original metadata, or finalize the physical values
before constructing metadata with a defined recovery policy. Cover PK,
comparison, and delete-field fallback.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java:
##########
@@ -852,192 +898,527 @@ private void validateNumMultiValues(GenericRow row) {
}
}
- private void updateDictionary(GenericRow row) {
+ /// Runs dictionary + forward/secondary indexing for a new docId and meters
an incomplete row when either step had
+ /// to fall back to defaults (issue #16316).
+ private void indexPhysicalRow(int docId, GenericRow row) {
+ RowWriteState state = _rowWriteState;
+ boolean dictHadError = updateDictionary(row, state);
+ boolean rowHadError = addNewRow(docId, row, state);
+ if (dictHadError || rowHadError) {
+ recordIncompleteRow();
+ }
+ if (state._repairFailed) {
+ markUnrecoverable(state);
+ throwPendingRowIndexingExceptionIfStrict();
+ throw new IllegalStateException(
+ "Mutable segment " + _segmentName + " is terminal after an
unrecoverable indexing failure");
+ }
+ }
+
+ /// @return {@code true} if any column required a default/fallback while
updating dictionaries.
+ /// When `continueOnError` is false, dictionary failures are stashed and
rethrown after the row is published.
+ private boolean updateDictionary(GenericRow row, RowWriteState state) {
+ boolean hadError = false;
for (Map.Entry<String, IndexContainer> entry :
_indexContainerMap.entrySet()) {
IndexContainer indexContainer = entry.getValue();
MutableDictionary dictionary = indexContainer._dictionary;
if (dictionary == null) {
continue;
}
-
- Object value = row.getValue(entry.getKey());
- if (value == null) {
- recordIndexingError("DICTIONARY");
- } else {
+ String column = entry.getKey();
+ Object value = row.getValue(column);
+ try {
+ if (value == null) {
+ recordIndexingError("DICTIONARY");
+ hadError = true;
+ state.markIncomplete();
+ value = getDefaultNullValueForIndexing(indexContainer._fieldSpec);
+ row.putDefaultNullValue(column, value);
+ }
if (indexContainer._fieldSpec.isSingleValueField()) {
indexContainer._dictId = dictionary.index(value);
} else {
indexContainer._dictIds = dictionary.index((Object[]) value);
}
-
- // Update min/max value from dictionary
indexContainer._minValue = dictionary.getMinVal();
indexContainer._maxValue = dictionary.getMaxVal();
+ } catch (Exception e) {
+ recordOrDeferIndexingError("DICTIONARY", e);
+ hadError = true;
+ state.noteError(e);
+ indexContainer._dictId = Integer.MIN_VALUE;
+ indexContainer._dictIds = null;
+ try {
+ // Index the field default instead of leaving the sentinel: with
metrics aggregation the rollup key is built
+ // from the dict ids (see getOrCreateDocId), and Integer.MIN_VALUE
is not a real dict id.
+ Object defaultValue =
getDefaultNullValueForIndexing(indexContainer._fieldSpec);
+ if (indexContainer._fieldSpec.isSingleValueField()) {
+ indexContainer._dictId = dictionary.index(defaultValue);
+ } else {
+ indexContainer._dictIds = dictionary.index((Object[])
defaultValue);
+ }
+ row.putDefaultNullValue(column, defaultValue);
+ indexContainer._minValue = dictionary.getMinVal();
+ indexContainer._maxValue = dictionary.getMaxVal();
+ } catch (Exception fallbackError) {
+ _logger.error("Failed to index default null value into dictionary
for column: {}", column, fallbackError);
+ state.noteRepairFailure(fallbackError);
+ }
}
- updateIndexCapacityThresholdBreached(dictionary, entry.getKey());
+ updateIndexCapacityThresholdBreached(dictionary, column);
}
+ return hadError;
}
- private void addNewRow(int docId, GenericRow row) {
+ /// Indexes a new physical row. Always completes the row so seal/query
lengths stay aligned with [_numDocsIndexed]
+ /// (issue #16316). Forward indexes are written first from a finalized
value; secondary indexes then consume that
+ /// same value. On unrecoverable repair the segment is marked terminal.
+ ///
+ /// @return {@code true} if any column required a default/fallback while
indexing
+ private boolean addNewRow(int docId, GenericRow row, RowWriteState state) {
+ boolean rowHadError = false;
for (Map.Entry<String, IndexContainer> entry :
_indexContainerMap.entrySet()) {
String column = entry.getKey();
IndexContainer indexContainer = entry.getValue();
-
- // Handle ingestion aggregation
- ValueAggregator valueAggregator = indexContainer._valueAggregator;
- if (valueAggregator != null) {
- String sourceColumn = indexContainer._sourceColumn;
- // NOTE: value can be null if the column is not specified in the
schema.
- Object value = row.getValue(sourceColumn);
- // Handle COUNT(*)
- if (value == null &&
sourceColumn.equals(AggregationFunctionColumnPair.STAR)) {
- assert valueAggregator.getAggregationType() ==
AggregationFunctionType.COUNT;
- value = 1;
+ try {
+ if (indexContainer._valueAggregator != null) {
+ if (!addAggregatedColumn(docId, row, column, indexContainer, state))
{
+ rowHadError = true;
+ }
+ } else if (!addPhysicalColumn(docId, row, column, indexContainer,
state)) {
+ rowHadError = true;
}
-
- // Update numValues info
- indexContainer._valuesInfo.updateSVNumValues();
-
- MutableIndex forwardIndex =
indexContainer._mutableIndexes.get(StandardIndexes.forward());
- FieldSpec fieldSpec = indexContainer._fieldSpec;
-
- DataType dataType = fieldSpec.getDataType();
- value = valueAggregator.getInitialAggregatedValue(value);
- // BIG_DECIMAL is actually stored as byte[] and hence can be supported
here.
- switch (dataType.getStoredType()) {
- case INT:
- forwardIndex.add(((Number) value).intValue(), -1, docId);
- break;
- case LONG:
- forwardIndex.add(((Number) value).longValue(), -1, docId);
- break;
- case FLOAT:
- forwardIndex.add(((Number) value).floatValue(), -1, docId);
- break;
- case DOUBLE:
- forwardIndex.add(((Number) value).doubleValue(), -1, docId);
- break;
- case BIG_DECIMAL:
- case BYTES:
- forwardIndex.add(valueAggregator.serializeAggregatedValue(value),
-1, docId);
- break;
- default:
- throw new UnsupportedOperationException(
- "Unsupported data type: " + dataType + " for aggregation: " +
column);
+ } catch (Exception e) {
+ recordOrDeferIndexingError("ROW", e);
+ rowHadError = true;
+ state.noteError(e);
+ try {
+ indexDefaultNullColumn(docId, column, indexContainer, state);
+ } catch (Exception fallbackError) {
+ _logger.error("Failed to index default null for column: {} at docId:
{}", column, docId, fallbackError);
+ state.noteRepairFailure(fallbackError);
}
- continue;
}
+ }
- // Update the null value vector even if a null value is somehow produced
- if (indexContainer._nullValueVector != null && row.isNullValue(column)) {
- indexContainer._nullValueVector.setNull(docId);
+ if (_multiColumnValues != null) {
+ try {
+ // Multi-column text is a row-level secondary index. It is written
only after every column has a finalized
+ // value in `_multiColumnValues`. There is no per-doc rollback; a
failure leaves this doc absent from the
+ // text index while forward indexes remain complete. Counted once as a
secondary-index error.
+ _multiColumnTextIndex.add(_multiColumnValues);
+ } catch (Exception e) {
+ recordOrDeferIndexingError("MULTI_COLUMN_TEXT", e);
+ rowHadError = true;
+ state.noteError(e);
+ } finally {
+ Collections.fill(_multiColumnValues, null);
}
+ }
+ if (state._repairFailed) {
+ markUnrecoverable(state);
+ }
+ return rowHadError;
+ }
- Object value = row.getValue(column);
- if (value == null) {
- // the value should not be null unless something is broken upstream
but this will lead to inappropriate reuse
- // of the dictionary id if this somehow happens. An NPE here can
corrupt indexes leading to incorrect query
- // results, hence the extra care. A metric will already have been
emitted when trying to update the dictionary.
- continue;
+ /// Returns {@code true} when the aggregated column was written without
error.
+ private boolean addAggregatedColumn(int docId, GenericRow row, String
column, IndexContainer indexContainer,
+ RowWriteState state) {
+ ValueAggregator valueAggregator = indexContainer._valueAggregator;
+ String sourceColumn = indexContainer._sourceColumn;
+ Object value = row.getValue(sourceColumn);
+ if (value == null &&
sourceColumn.equals(AggregationFunctionColumnPair.STAR)) {
+ assert valueAggregator.getAggregationType() ==
AggregationFunctionType.COUNT;
+ value = 1;
+ }
+
+ MutableIndex forwardIndex =
indexContainer._mutableIndexes.get(StandardIndexes.forward());
+ FieldSpec fieldSpec = indexContainer._fieldSpec;
+ DataType dataType = fieldSpec.getDataType();
+ try {
+ value = valueAggregator.getInitialAggregatedValue(value);
+ addAggregatedForwardValue(forwardIndex, valueAggregator, dataType,
value, docId, column);
+ indexContainer._valuesInfo.updateSVNumValues();
+ state._columnsWithCanonicalForward.add(column);
+ return true;
+ } catch (Exception e) {
+ recordOrDeferIndexingError(StandardIndexes.forward(), e);
+ state.noteError(e);
+ try {
+ indexDefaultAggregatedValue(docId, column, indexContainer, state);
+ } catch (Exception fallbackError) {
+ state.noteRepairFailure(fallbackError);
}
+ return false;
+ }
+ }
- FieldSpec fieldSpec = indexContainer._fieldSpec;
- DataType dataType = fieldSpec.getDataType();
+ private void addAggregatedForwardValue(MutableIndex forwardIndex,
ValueAggregator valueAggregator, DataType dataType,
+ Object value, int docId, String column) {
+ maybeIntercept(column, StandardIndexes.forward(), value, docId);
+ switch (dataType.getStoredType()) {
+ case INT:
+ forwardIndex.add(((Number) value).intValue(), -1, docId);
+ break;
+ case LONG:
+ forwardIndex.add(((Number) value).longValue(), -1, docId);
+ break;
+ case FLOAT:
+ forwardIndex.add(((Number) value).floatValue(), -1, docId);
+ break;
+ case DOUBLE:
+ forwardIndex.add(((Number) value).doubleValue(), -1, docId);
+ break;
+ case BIG_DECIMAL:
+ case BYTES:
+ forwardIndex.add(valueAggregator.serializeAggregatedValue(value), -1,
docId);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported data type: " +
dataType + " for aggregation: " + column);
+ }
+ }
- if (fieldSpec.isSingleValueField()) {
- // Update numValues info
- indexContainer._valuesInfo.updateSVNumValues();
-
- // Route OPEN_STRUCT values to the dedicated mutable index.
OPEN_STRUCT has no forward
- // index / dictionary / min-max, so the standard per-IndexType loop
and the comparable
- // tracking below would be no-ops at best and crash at worst (Map is
not Comparable).
- if (dataType == DataType.OPEN_STRUCT) {
- MutableIndex openStructIndex =
indexContainer._mutableIndexes.get(StandardIndexes.openStruct());
- if (openStructIndex != null) {
- openStructIndex.add(value, -1, docId);
- }
- continue;
+ /// Returns {@code true} when the physical column was written from the row
value without error and without falling
+ /// back to the field default.
+ private boolean addPhysicalColumn(int docId, GenericRow row, String column,
IndexContainer indexContainer,
+ RowWriteState state) {
+ FieldSpec fieldSpec = indexContainer._fieldSpec;
+ DataType dataType = fieldSpec.getDataType();
+ boolean isNull = row.isNullValue(column);
+ Object value = row.getValue(column);
+ boolean defaultSubstituted = false;
+ if (value == null) {
+ recordIndexingError("NULL_VALUE");
+ value = getDefaultNullValueForIndexing(fieldSpec);
+ isNull = true;
+ defaultSubstituted = true;
+ state.markIncomplete();
+ }
+
+ if (fieldSpec.isSingleValueField()) {
+ if (dataType == DataType.OPEN_STRUCT) {
+ return addOpenStructColumn(docId, column, indexContainer, value,
defaultSubstituted, state);
+ }
+ int dictId = indexContainer._dictId;
+ if (indexContainer._dictionary != null && dictId == Integer.MIN_VALUE) {
+ try {
+ Object defaultValue = getDefaultNullValueForIndexing(fieldSpec);
+ dictId = indexContainer._dictionary.index(defaultValue);
+ indexContainer._dictId = dictId;
+ value = defaultValue;
+ isNull = true;
+ defaultSubstituted = true;
+ state.markIncomplete();
+ } catch (Exception e) {
+ recordOrDeferIndexingError("DICTIONARY", e);
+ state.noteError(e);
+ state.noteRepairFailure(e);
+ return false;
}
+ }
+ Object parsedJsonValue = defaultSubstituted ? null :
row.getParsedJsonValue(column);
+ boolean hadError = writeSingleValueIndexes(docId, column,
indexContainer, fieldSpec, dataType, value, dictId,
+ isNull, parsedJsonValue, state);
+ return !hadError && !defaultSubstituted;
+ }
- // Update indexes
- int dictId = indexContainer._dictId;
- for (Map.Entry<IndexType, MutableIndex> indexEntry :
indexContainer._mutableIndexes.entrySet()) {
- try {
- MutableIndex mutableIndex = indexEntry.getValue();
- if (mutableIndex instanceof MutableJsonIndexImpl) {
- // Feed the JSON index the parsed value cached by the
DataTypeTransformer (if any), so it flattens the
- // document directly instead of re-parsing the string this
column was serialized into for the forward
- // index; the index falls back to that string for documents it
cannot flatten directly. This is
- // intentionally scoped to Pinot's built-in JSON index
implementation.
- Object parsedValue = row.getParsedJsonValue(column);
- if (parsedValue != null && value instanceof String) {
- ((MutableJsonIndexImpl) mutableIndex).addParsed(parsedValue,
(String) value);
- updateIndexCapacityThresholdBreached(mutableIndex,
indexEntry.getKey(), column);
- continue;
- }
- }
- mutableIndex.add(value, dictId, docId);
- updateIndexCapacityThresholdBreached(mutableIndex,
indexEntry.getKey(), column);
- } catch (Exception e) {
- recordIndexingError(indexEntry.getKey(), e);
- }
+ Object[] values = value instanceof Object[] ? (Object[]) value : new
Object[]{value};
+ int[] dictIds = indexContainer._dictIds;
+ if (indexContainer._dictionary != null && dictIds == null) {
+ try {
+ Object[] defaultValues = (Object[])
getDefaultNullValueForIndexing(fieldSpec);
+ dictIds = indexContainer._dictionary.index(defaultValues);
+ indexContainer._dictIds = dictIds;
+ values = defaultValues;
+ isNull = true;
+ defaultSubstituted = true;
+ state.markIncomplete();
+ } catch (Exception e) {
+ recordOrDeferIndexingError("DICTIONARY", e);
+ state.noteError(e);
+ state.noteRepairFailure(e);
+ return false;
+ }
+ }
+ boolean hadError = writeMultiValueIndexes(docId, column, indexContainer,
dataType, values, dictIds, isNull, state);
+ return !hadError && !defaultSubstituted;
+ }
+
+ private boolean addOpenStructColumn(int docId, String column, IndexContainer
indexContainer, Object value,
+ boolean defaultSubstituted, RowWriteState state) {
+ MutableIndex openStructIndex =
indexContainer._mutableIndexes.get(StandardIndexes.openStruct());
+ if (openStructIndex != null) {
+ try {
+ maybeIntercept(column, StandardIndexes.openStruct(), value, docId);
+ openStructIndex.add(value, -1, docId);
+ state._columnsWithCanonicalForward.add(column);
+ } catch (Exception e) {
+ recordOrDeferIndexingError(StandardIndexes.openStruct(), e);
+ state.noteError(e);
+ try {
+ Object defaultValue =
getDefaultNullValueForIndexing(indexContainer._fieldSpec);
+ maybeIntercept(column, StandardIndexes.openStruct(), defaultValue,
docId);
+ openStructIndex.add(defaultValue, -1, docId);
+ state._columnsWithCanonicalForward.add(column);
+ } catch (Exception fallbackError) {
+ state.noteRepairFailure(fallbackError);
+ return false;
}
+ defaultSubstituted = true;
+ }
+ } else {
+ state._columnsWithCanonicalForward.add(column);
+ }
+ indexContainer._valuesInfo.updateSVNumValues();
+ return !defaultSubstituted;
+ }
- if (dictId < 0) {
- // Update min/max value from raw value
- // NOTE: Skip updating min/max value for aggregated metrics because
the value will change over time.
- if (!isAggregateMetricsEnabled() || fieldSpec.getFieldType() !=
FieldSpec.FieldType.METRIC) {
- Comparable comparable = toComparableValue(value, dataType, column);
- if (indexContainer._minValue == null) {
- indexContainer._minValue = comparable;
- indexContainer._maxValue = comparable;
- } else {
- if (comparable.compareTo(indexContainer._minValue) < 0) {
- indexContainer._minValue = comparable;
- }
- if (comparable.compareTo(indexContainer._maxValue) > 0) {
- indexContainer._maxValue = comparable;
- }
- }
+ /// Writes the canonical forward index first, then every secondary index
from that same finalized value.
+ private boolean writeSingleValueIndexes(int docId, String column,
IndexContainer indexContainer, FieldSpec fieldSpec,
+ DataType dataType, Object value, int dictId, boolean isNull, @Nullable
Object parsedJsonValue,
+ RowWriteState state) {
+ boolean hadError = false;
+ MutableIndex forwardIndex =
indexContainer._mutableIndexes.get(StandardIndexes.forward());
+ if (forwardIndex != null &&
!state._columnsWithCanonicalForward.contains(column)) {
+ try {
+ maybeIntercept(column, StandardIndexes.forward(), value, docId);
+ forwardIndex.add(value, dictId, docId);
+ updateIndexCapacityThresholdBreached(forwardIndex,
StandardIndexes.forward(), column);
+ state._columnsWithCanonicalForward.add(column);
+ } catch (Exception e) {
+ recordOrDeferIndexingError(StandardIndexes.forward(), e);
+ state.noteError(e);
+ hadError = true;
+ try {
+ Object defaultValue = getDefaultNullValueForIndexing(fieldSpec);
+ int defaultDictId = dictId;
+ if (indexContainer._dictionary != null) {
+ defaultDictId = indexContainer._dictionary.index(defaultValue);
+ indexContainer._dictId = defaultDictId;
}
+ value = defaultValue;
+ dictId = defaultDictId;
Review Comment:
[P1] Keep the registered aggregation key consistent with repaired dimensions
getOrCreateDocId() has already registered the original dimension dictionary
IDs before this forward-index fallback changes the dimension and _dictId to the
default. With aggregation enabled, a forward failure for dim=42, metric=1 can
publish dim=DEFAULT while _recordIdMap still maps the key for 42 to that
document. A later successful dim=42, metric=2 then aggregates into the
default-valued row, permanently assigning valid input to the wrong group.
Please ensure a repaired dimension cannot disagree with the registered key, or
treat key-column repair as unrecoverable. The existing dictionary-failure
aggregation test fails before key creation and does not cover this
forward-failure case.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java:
##########
@@ -1129,77 +1582,159 @@ private void aggregateMetrics(GenericRow row, int
docId) {
}
MutableForwardIndex forwardIndex =
(MutableForwardIndex)
indexContainer._mutableIndexes.get(StandardIndexes.forward());
- DataType dataType = metricFieldSpec.getDataType();
-
- switch (valueAggregator.getAggregatedValueType()) {
- case DOUBLE:
- double oldDoubleValue;
- double newDoubleValue;
- switch (dataType) {
- case INT:
- oldDoubleValue = forwardIndex.getInt(docId);
- newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, value);
- forwardIndex.setInt(docId, (int) newDoubleValue);
- break;
- case LONG:
- oldDoubleValue = forwardIndex.getLong(docId);
- newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, value);
- forwardIndex.setLong(docId, (long) newDoubleValue);
- break;
- case FLOAT:
- oldDoubleValue = forwardIndex.getFloat(docId);
- newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, value);
- forwardIndex.setFloat(docId, (float) newDoubleValue);
- break;
- case DOUBLE:
- oldDoubleValue = forwardIndex.getDouble(docId);
- newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, value);
- forwardIndex.setDouble(docId, newDoubleValue);
- break;
- default:
- throw new
UnsupportedOperationException(String.format("Aggregation type %s of %s not
supported for %s",
- valueAggregator.getAggregatedValueType(),
valueAggregator.getAggregationType(), dataType));
- }
- break;
- case LONG:
- long oldLongValue;
- long newLongValue;
- switch (dataType) {
- case INT:
- oldLongValue = forwardIndex.getInt(docId);
- newLongValue = (long)
valueAggregator.applyRawValue(oldLongValue, value);
- forwardIndex.setInt(docId, (int) newLongValue);
- break;
- case LONG:
- oldLongValue = forwardIndex.getLong(docId);
- newLongValue = (long)
valueAggregator.applyRawValue(oldLongValue, value);
- forwardIndex.setLong(docId, newLongValue);
- break;
- case FLOAT:
- oldLongValue = (long) forwardIndex.getFloat(docId);
- newLongValue = (long)
valueAggregator.applyRawValue(oldLongValue, value);
- forwardIndex.setFloat(docId, (float) newLongValue);
- break;
- case DOUBLE:
- oldLongValue = (long) forwardIndex.getDouble(docId);
- newLongValue = (long)
valueAggregator.applyRawValue(oldLongValue, value);
- forwardIndex.setDouble(docId, (double) newLongValue);
- break;
- default:
- throw new
UnsupportedOperationException(String.format("Aggregation type %s of %s not
supported for %s",
- valueAggregator.getAggregatedValueType(),
valueAggregator.getAggregationType(), dataType));
+ prepared.add(
+ prepareMetricWrite(column, valueAggregator, forwardIndex,
metricFieldSpec.getDataType(), value, docId));
+ }
+
+ List<PreparedMetricWrite> committed = new ArrayList<>();
+ try {
+ for (PreparedMetricWrite write : prepared) {
+ maybeIntercept(write._column, StandardIndexes.forward(),
write.interceptValue(), docId);
+ commitMetricWrite(write, docId);
+ committed.add(write);
+ }
+ } catch (Exception e) {
+ Exception restoreError = null;
+ for (int i = committed.size() - 1; i >= 0; i--) {
+ try {
+ maybeIntercept(committed.get(i)._column, StandardIndexes.forward(),
committed.get(i).restoreInterceptValue(),
+ docId);
+ restoreMetricWrite(committed.get(i), docId);
+ } catch (Exception restoreException) {
+ if (restoreError == null) {
+ restoreError = restoreException;
+ } else {
+ restoreError.addSuppressed(restoreException);
}
- break;
- case BYTES:
- Object oldValue =
valueAggregator.deserializeAggregatedValue(forwardIndex.getBytes(docId));
- Object newValue = valueAggregator.applyRawValue(oldValue, value);
- forwardIndex.setBytes(docId,
valueAggregator.serializeAggregatedValue(newValue));
- break;
- default:
- throw new UnsupportedOperationException(
- String.format("Aggregation type %s of %s not supported for %s",
valueAggregator.getAggregatedValueType(),
- valueAggregator.getAggregationType(), dataType));
+ }
+ }
+ if (restoreError != null) {
+ e.addSuppressed(restoreError);
+ _unrecoverableIndexingFailure = true;
+ }
+ throw wrapIndexingException("AGGREGATE_METRICS", e);
+ }
+ }
+
+ private PreparedMetricWrite prepareMetricWrite(String column,
ValueAggregator valueAggregator,
+ MutableForwardIndex forwardIndex, DataType dataType, Object rawValue,
int docId) {
+ switch (valueAggregator.getAggregatedValueType()) {
+ case DOUBLE: {
+ double oldDoubleValue;
+ double newDoubleValue;
+ MetricWriteKind kind;
+ switch (dataType) {
+ case INT:
+ oldDoubleValue = forwardIndex.getInt(docId);
+ newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, rawValue);
+ kind = MetricWriteKind.INT;
+ break;
+ case LONG:
+ oldDoubleValue = forwardIndex.getLong(docId);
+ newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, rawValue);
+ kind = MetricWriteKind.LONG;
+ break;
+ case FLOAT:
+ oldDoubleValue = forwardIndex.getFloat(docId);
+ newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, rawValue);
+ kind = MetricWriteKind.FLOAT;
+ break;
+ case DOUBLE:
+ oldDoubleValue = forwardIndex.getDouble(docId);
+ newDoubleValue = (double)
valueAggregator.applyRawValue(oldDoubleValue, rawValue);
+ kind = MetricWriteKind.DOUBLE;
+ break;
+ default:
+ throw new UnsupportedOperationException(String.format("Aggregation
type %s of %s not supported for %s",
+ valueAggregator.getAggregatedValueType(),
valueAggregator.getAggregationType(), dataType));
+ }
+ return PreparedMetricWrite.numeric(column, forwardIndex, kind,
oldDoubleValue, newDoubleValue);
}
+ case LONG: {
+ long oldLongValue;
+ long newLongValue;
+ MetricWriteKind kind;
+ switch (dataType) {
+ case INT:
+ oldLongValue = forwardIndex.getInt(docId);
+ newLongValue = (long) valueAggregator.applyRawValue(oldLongValue,
rawValue);
+ kind = MetricWriteKind.INT;
+ break;
+ case LONG:
+ oldLongValue = forwardIndex.getLong(docId);
+ newLongValue = (long) valueAggregator.applyRawValue(oldLongValue,
rawValue);
+ kind = MetricWriteKind.LONG;
+ break;
+ case FLOAT:
+ oldLongValue = (long) forwardIndex.getFloat(docId);
+ newLongValue = (long) valueAggregator.applyRawValue(oldLongValue,
rawValue);
+ kind = MetricWriteKind.FLOAT;
+ break;
+ case DOUBLE:
+ oldLongValue = (long) forwardIndex.getDouble(docId);
+ newLongValue = (long) valueAggregator.applyRawValue(oldLongValue,
rawValue);
+ kind = MetricWriteKind.DOUBLE;
+ break;
+ default:
+ throw new UnsupportedOperationException(String.format("Aggregation
type %s of %s not supported for %s",
+ valueAggregator.getAggregatedValueType(),
valueAggregator.getAggregationType(), dataType));
+ }
+ return PreparedMetricWrite.numeric(column, forwardIndex, kind,
oldLongValue, newLongValue);
+ }
+ case BYTES: {
+ Object oldValue =
valueAggregator.deserializeAggregatedValue(forwardIndex.getBytes(docId));
+ Object newValue = valueAggregator.applyRawValue(oldValue, rawValue);
+ return PreparedMetricWrite.bytes(column, forwardIndex,
valueAggregator.serializeAggregatedValue(oldValue),
+ valueAggregator.serializeAggregatedValue(newValue));
Review Comment:
[P1] Capture the original aggregate bytes before applying the update
applyRawValue() can mutate oldValue and return that same object:
AvgValueAggregator calls value.apply(), and HLL aggregators mutate their
sketch. Serializing oldValue afterward therefore saves the updated aggregate as
the rollback value. If this metric commits and a later metric write fails,
restoreMetricWrite() restores the new bytes, silently retaining only part of
the failed row. Please capture the original bytes before applyRawValue(), or
clone the old aggregate first. Extend rollback coverage with a mutable BYTES
aggregate such as AVG followed by a failing metric; the current numeric LONG
tests cannot expose this aliasing.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java:
##########
@@ -852,192 +898,527 @@ private void validateNumMultiValues(GenericRow row) {
}
}
- private void updateDictionary(GenericRow row) {
+ /// Runs dictionary + forward/secondary indexing for a new docId and meters
an incomplete row when either step had
+ /// to fall back to defaults (issue #16316).
+ private void indexPhysicalRow(int docId, GenericRow row) {
+ RowWriteState state = _rowWriteState;
+ boolean dictHadError = updateDictionary(row, state);
+ boolean rowHadError = addNewRow(docId, row, state);
+ if (dictHadError || rowHadError) {
+ recordIncompleteRow();
+ }
+ if (state._repairFailed) {
+ markUnrecoverable(state);
+ throwPendingRowIndexingExceptionIfStrict();
+ throw new IllegalStateException(
+ "Mutable segment " + _segmentName + " is terminal after an
unrecoverable indexing failure");
+ }
+ }
+
+ /// @return {@code true} if any column required a default/fallback while
updating dictionaries.
+ /// When `continueOnError` is false, dictionary failures are stashed and
rethrown after the row is published.
+ private boolean updateDictionary(GenericRow row, RowWriteState state) {
+ boolean hadError = false;
for (Map.Entry<String, IndexContainer> entry :
_indexContainerMap.entrySet()) {
IndexContainer indexContainer = entry.getValue();
MutableDictionary dictionary = indexContainer._dictionary;
if (dictionary == null) {
continue;
}
-
- Object value = row.getValue(entry.getKey());
- if (value == null) {
- recordIndexingError("DICTIONARY");
- } else {
+ String column = entry.getKey();
+ Object value = row.getValue(column);
+ try {
+ if (value == null) {
+ recordIndexingError("DICTIONARY");
+ hadError = true;
+ state.markIncomplete();
+ value = getDefaultNullValueForIndexing(indexContainer._fieldSpec);
+ row.putDefaultNullValue(column, value);
+ }
if (indexContainer._fieldSpec.isSingleValueField()) {
indexContainer._dictId = dictionary.index(value);
} else {
indexContainer._dictIds = dictionary.index((Object[]) value);
}
-
- // Update min/max value from dictionary
indexContainer._minValue = dictionary.getMinVal();
indexContainer._maxValue = dictionary.getMaxVal();
+ } catch (Exception e) {
+ recordOrDeferIndexingError("DICTIONARY", e);
+ hadError = true;
+ state.noteError(e);
+ indexContainer._dictId = Integer.MIN_VALUE;
+ indexContainer._dictIds = null;
+ try {
+ // Index the field default instead of leaving the sentinel: with
metrics aggregation the rollup key is built
+ // from the dict ids (see getOrCreateDocId), and Integer.MIN_VALUE
is not a real dict id.
+ Object defaultValue =
getDefaultNullValueForIndexing(indexContainer._fieldSpec);
+ if (indexContainer._fieldSpec.isSingleValueField()) {
+ indexContainer._dictId = dictionary.index(defaultValue);
+ } else {
+ indexContainer._dictIds = dictionary.index((Object[])
defaultValue);
+ }
+ row.putDefaultNullValue(column, defaultValue);
+ indexContainer._minValue = dictionary.getMinVal();
+ indexContainer._maxValue = dictionary.getMaxVal();
+ } catch (Exception fallbackError) {
+ _logger.error("Failed to index default null value into dictionary
for column: {}", column, fallbackError);
+ state.noteRepairFailure(fallbackError);
+ }
}
- updateIndexCapacityThresholdBreached(dictionary, entry.getKey());
+ updateIndexCapacityThresholdBreached(dictionary, column);
}
+ return hadError;
}
- private void addNewRow(int docId, GenericRow row) {
+ /// Indexes a new physical row. Always completes the row so seal/query
lengths stay aligned with [_numDocsIndexed]
+ /// (issue #16316). Forward indexes are written first from a finalized
value; secondary indexes then consume that
+ /// same value. On unrecoverable repair the segment is marked terminal.
+ ///
+ /// @return {@code true} if any column required a default/fallback while
indexing
+ private boolean addNewRow(int docId, GenericRow row, RowWriteState state) {
+ boolean rowHadError = false;
for (Map.Entry<String, IndexContainer> entry :
_indexContainerMap.entrySet()) {
String column = entry.getKey();
IndexContainer indexContainer = entry.getValue();
-
- // Handle ingestion aggregation
- ValueAggregator valueAggregator = indexContainer._valueAggregator;
- if (valueAggregator != null) {
- String sourceColumn = indexContainer._sourceColumn;
- // NOTE: value can be null if the column is not specified in the
schema.
- Object value = row.getValue(sourceColumn);
- // Handle COUNT(*)
- if (value == null &&
sourceColumn.equals(AggregationFunctionColumnPair.STAR)) {
- assert valueAggregator.getAggregationType() ==
AggregationFunctionType.COUNT;
- value = 1;
+ try {
+ if (indexContainer._valueAggregator != null) {
+ if (!addAggregatedColumn(docId, row, column, indexContainer, state))
{
+ rowHadError = true;
+ }
+ } else if (!addPhysicalColumn(docId, row, column, indexContainer,
state)) {
+ rowHadError = true;
}
-
- // Update numValues info
- indexContainer._valuesInfo.updateSVNumValues();
-
- MutableIndex forwardIndex =
indexContainer._mutableIndexes.get(StandardIndexes.forward());
- FieldSpec fieldSpec = indexContainer._fieldSpec;
-
- DataType dataType = fieldSpec.getDataType();
- value = valueAggregator.getInitialAggregatedValue(value);
- // BIG_DECIMAL is actually stored as byte[] and hence can be supported
here.
- switch (dataType.getStoredType()) {
- case INT:
- forwardIndex.add(((Number) value).intValue(), -1, docId);
- break;
- case LONG:
- forwardIndex.add(((Number) value).longValue(), -1, docId);
- break;
- case FLOAT:
- forwardIndex.add(((Number) value).floatValue(), -1, docId);
- break;
- case DOUBLE:
- forwardIndex.add(((Number) value).doubleValue(), -1, docId);
- break;
- case BIG_DECIMAL:
- case BYTES:
- forwardIndex.add(valueAggregator.serializeAggregatedValue(value),
-1, docId);
- break;
- default:
- throw new UnsupportedOperationException(
- "Unsupported data type: " + dataType + " for aggregation: " +
column);
+ } catch (Exception e) {
+ recordOrDeferIndexingError("ROW", e);
+ rowHadError = true;
+ state.noteError(e);
+ try {
+ indexDefaultNullColumn(docId, column, indexContainer, state);
+ } catch (Exception fallbackError) {
+ _logger.error("Failed to index default null for column: {} at docId:
{}", column, docId, fallbackError);
+ state.noteRepairFailure(fallbackError);
}
- continue;
}
+ }
- // Update the null value vector even if a null value is somehow produced
- if (indexContainer._nullValueVector != null && row.isNullValue(column)) {
- indexContainer._nullValueVector.setNull(docId);
+ if (_multiColumnValues != null) {
+ try {
+ // Multi-column text is a row-level secondary index. It is written
only after every column has a finalized
+ // value in `_multiColumnValues`. There is no per-doc rollback; a
failure leaves this doc absent from the
+ // text index while forward indexes remain complete. Counted once as a
secondary-index error.
+ _multiColumnTextIndex.add(_multiColumnValues);
+ } catch (Exception e) {
+ recordOrDeferIndexingError("MULTI_COLUMN_TEXT", e);
+ rowHadError = true;
+ state.noteError(e);
+ } finally {
+ Collections.fill(_multiColumnValues, null);
}
+ }
+ if (state._repairFailed) {
+ markUnrecoverable(state);
+ }
+ return rowHadError;
+ }
- Object value = row.getValue(column);
- if (value == null) {
- // the value should not be null unless something is broken upstream
but this will lead to inappropriate reuse
- // of the dictionary id if this somehow happens. An NPE here can
corrupt indexes leading to incorrect query
- // results, hence the extra care. A metric will already have been
emitted when trying to update the dictionary.
- continue;
+ /// Returns {@code true} when the aggregated column was written without
error.
+ private boolean addAggregatedColumn(int docId, GenericRow row, String
column, IndexContainer indexContainer,
+ RowWriteState state) {
+ ValueAggregator valueAggregator = indexContainer._valueAggregator;
+ String sourceColumn = indexContainer._sourceColumn;
+ Object value = row.getValue(sourceColumn);
+ if (value == null &&
sourceColumn.equals(AggregationFunctionColumnPair.STAR)) {
+ assert valueAggregator.getAggregationType() ==
AggregationFunctionType.COUNT;
+ value = 1;
+ }
+
+ MutableIndex forwardIndex =
indexContainer._mutableIndexes.get(StandardIndexes.forward());
+ FieldSpec fieldSpec = indexContainer._fieldSpec;
+ DataType dataType = fieldSpec.getDataType();
+ try {
+ value = valueAggregator.getInitialAggregatedValue(value);
+ addAggregatedForwardValue(forwardIndex, valueAggregator, dataType,
value, docId, column);
+ indexContainer._valuesInfo.updateSVNumValues();
+ state._columnsWithCanonicalForward.add(column);
+ return true;
+ } catch (Exception e) {
+ recordOrDeferIndexingError(StandardIndexes.forward(), e);
+ state.noteError(e);
+ try {
+ indexDefaultAggregatedValue(docId, column, indexContainer, state);
+ } catch (Exception fallbackError) {
+ state.noteRepairFailure(fallbackError);
}
+ return false;
+ }
+ }
- FieldSpec fieldSpec = indexContainer._fieldSpec;
- DataType dataType = fieldSpec.getDataType();
+ private void addAggregatedForwardValue(MutableIndex forwardIndex,
ValueAggregator valueAggregator, DataType dataType,
+ Object value, int docId, String column) {
+ maybeIntercept(column, StandardIndexes.forward(), value, docId);
+ switch (dataType.getStoredType()) {
+ case INT:
+ forwardIndex.add(((Number) value).intValue(), -1, docId);
+ break;
+ case LONG:
+ forwardIndex.add(((Number) value).longValue(), -1, docId);
+ break;
+ case FLOAT:
+ forwardIndex.add(((Number) value).floatValue(), -1, docId);
+ break;
+ case DOUBLE:
+ forwardIndex.add(((Number) value).doubleValue(), -1, docId);
+ break;
+ case BIG_DECIMAL:
+ case BYTES:
+ forwardIndex.add(valueAggregator.serializeAggregatedValue(value), -1,
docId);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported data type: " +
dataType + " for aggregation: " + column);
+ }
+ }
- if (fieldSpec.isSingleValueField()) {
- // Update numValues info
- indexContainer._valuesInfo.updateSVNumValues();
-
- // Route OPEN_STRUCT values to the dedicated mutable index.
OPEN_STRUCT has no forward
- // index / dictionary / min-max, so the standard per-IndexType loop
and the comparable
- // tracking below would be no-ops at best and crash at worst (Map is
not Comparable).
- if (dataType == DataType.OPEN_STRUCT) {
- MutableIndex openStructIndex =
indexContainer._mutableIndexes.get(StandardIndexes.openStruct());
- if (openStructIndex != null) {
- openStructIndex.add(value, -1, docId);
- }
- continue;
+ /// Returns {@code true} when the physical column was written from the row
value without error and without falling
+ /// back to the field default.
+ private boolean addPhysicalColumn(int docId, GenericRow row, String column,
IndexContainer indexContainer,
+ RowWriteState state) {
+ FieldSpec fieldSpec = indexContainer._fieldSpec;
+ DataType dataType = fieldSpec.getDataType();
+ boolean isNull = row.isNullValue(column);
+ Object value = row.getValue(column);
+ boolean defaultSubstituted = false;
+ if (value == null) {
+ recordIndexingError("NULL_VALUE");
+ value = getDefaultNullValueForIndexing(fieldSpec);
+ isNull = true;
+ defaultSubstituted = true;
+ state.markIncomplete();
+ }
+
+ if (fieldSpec.isSingleValueField()) {
+ if (dataType == DataType.OPEN_STRUCT) {
+ return addOpenStructColumn(docId, column, indexContainer, value,
defaultSubstituted, state);
+ }
+ int dictId = indexContainer._dictId;
+ if (indexContainer._dictionary != null && dictId == Integer.MIN_VALUE) {
+ try {
+ Object defaultValue = getDefaultNullValueForIndexing(fieldSpec);
+ dictId = indexContainer._dictionary.index(defaultValue);
+ indexContainer._dictId = dictId;
+ value = defaultValue;
+ isNull = true;
+ defaultSubstituted = true;
+ state.markIncomplete();
+ } catch (Exception e) {
+ recordOrDeferIndexingError("DICTIONARY", e);
+ state.noteError(e);
+ state.noteRepairFailure(e);
+ return false;
}
+ }
+ Object parsedJsonValue = defaultSubstituted ? null :
row.getParsedJsonValue(column);
+ boolean hadError = writeSingleValueIndexes(docId, column,
indexContainer, fieldSpec, dataType, value, dictId,
+ isNull, parsedJsonValue, state);
+ return !hadError && !defaultSubstituted;
+ }
- // Update indexes
- int dictId = indexContainer._dictId;
- for (Map.Entry<IndexType, MutableIndex> indexEntry :
indexContainer._mutableIndexes.entrySet()) {
- try {
- MutableIndex mutableIndex = indexEntry.getValue();
- if (mutableIndex instanceof MutableJsonIndexImpl) {
- // Feed the JSON index the parsed value cached by the
DataTypeTransformer (if any), so it flattens the
- // document directly instead of re-parsing the string this
column was serialized into for the forward
- // index; the index falls back to that string for documents it
cannot flatten directly. This is
- // intentionally scoped to Pinot's built-in JSON index
implementation.
- Object parsedValue = row.getParsedJsonValue(column);
- if (parsedValue != null && value instanceof String) {
- ((MutableJsonIndexImpl) mutableIndex).addParsed(parsedValue,
(String) value);
- updateIndexCapacityThresholdBreached(mutableIndex,
indexEntry.getKey(), column);
- continue;
- }
- }
- mutableIndex.add(value, dictId, docId);
- updateIndexCapacityThresholdBreached(mutableIndex,
indexEntry.getKey(), column);
- } catch (Exception e) {
- recordIndexingError(indexEntry.getKey(), e);
- }
+ Object[] values = value instanceof Object[] ? (Object[]) value : new
Object[]{value};
+ int[] dictIds = indexContainer._dictIds;
+ if (indexContainer._dictionary != null && dictIds == null) {
+ try {
+ Object[] defaultValues = (Object[])
getDefaultNullValueForIndexing(fieldSpec);
+ dictIds = indexContainer._dictionary.index(defaultValues);
+ indexContainer._dictIds = dictIds;
+ values = defaultValues;
+ isNull = true;
+ defaultSubstituted = true;
+ state.markIncomplete();
+ } catch (Exception e) {
+ recordOrDeferIndexingError("DICTIONARY", e);
+ state.noteError(e);
+ state.noteRepairFailure(e);
+ return false;
+ }
+ }
+ boolean hadError = writeMultiValueIndexes(docId, column, indexContainer,
dataType, values, dictIds, isNull, state);
+ return !hadError && !defaultSubstituted;
+ }
+
+ private boolean addOpenStructColumn(int docId, String column, IndexContainer
indexContainer, Object value,
+ boolean defaultSubstituted, RowWriteState state) {
+ MutableIndex openStructIndex =
indexContainer._mutableIndexes.get(StandardIndexes.openStruct());
+ if (openStructIndex != null) {
+ try {
+ maybeIntercept(column, StandardIndexes.openStruct(), value, docId);
+ openStructIndex.add(value, -1, docId);
+ state._columnsWithCanonicalForward.add(column);
+ } catch (Exception e) {
+ recordOrDeferIndexingError(StandardIndexes.openStruct(), e);
+ state.noteError(e);
+ try {
+ Object defaultValue =
getDefaultNullValueForIndexing(indexContainer._fieldSpec);
+ maybeIntercept(column, StandardIndexes.openStruct(), defaultValue,
docId);
+ openStructIndex.add(defaultValue, -1, docId);
+ state._columnsWithCanonicalForward.add(column);
+ } catch (Exception fallbackError) {
+ state.noteRepairFailure(fallbackError);
+ return false;
}
+ defaultSubstituted = true;
+ }
+ } else {
+ state._columnsWithCanonicalForward.add(column);
+ }
+ indexContainer._valuesInfo.updateSVNumValues();
+ return !defaultSubstituted;
+ }
- if (dictId < 0) {
- // Update min/max value from raw value
- // NOTE: Skip updating min/max value for aggregated metrics because
the value will change over time.
- if (!isAggregateMetricsEnabled() || fieldSpec.getFieldType() !=
FieldSpec.FieldType.METRIC) {
- Comparable comparable = toComparableValue(value, dataType, column);
- if (indexContainer._minValue == null) {
- indexContainer._minValue = comparable;
- indexContainer._maxValue = comparable;
- } else {
- if (comparable.compareTo(indexContainer._minValue) < 0) {
- indexContainer._minValue = comparable;
- }
- if (comparable.compareTo(indexContainer._maxValue) > 0) {
- indexContainer._maxValue = comparable;
- }
- }
+ /// Writes the canonical forward index first, then every secondary index
from that same finalized value.
+ private boolean writeSingleValueIndexes(int docId, String column,
IndexContainer indexContainer, FieldSpec fieldSpec,
+ DataType dataType, Object value, int dictId, boolean isNull, @Nullable
Object parsedJsonValue,
+ RowWriteState state) {
+ boolean hadError = false;
+ MutableIndex forwardIndex =
indexContainer._mutableIndexes.get(StandardIndexes.forward());
+ if (forwardIndex != null &&
!state._columnsWithCanonicalForward.contains(column)) {
+ try {
+ maybeIntercept(column, StandardIndexes.forward(), value, docId);
+ forwardIndex.add(value, dictId, docId);
+ updateIndexCapacityThresholdBreached(forwardIndex,
StandardIndexes.forward(), column);
+ state._columnsWithCanonicalForward.add(column);
+ } catch (Exception e) {
+ recordOrDeferIndexingError(StandardIndexes.forward(), e);
+ state.noteError(e);
+ hadError = true;
+ try {
+ Object defaultValue = getDefaultNullValueForIndexing(fieldSpec);
+ int defaultDictId = dictId;
+ if (indexContainer._dictionary != null) {
+ defaultDictId = indexContainer._dictionary.index(defaultValue);
+ indexContainer._dictId = defaultDictId;
Review Comment:
[P1] Refresh cached dictionary bounds after inserting the fallback value
_minValue and _maxValue were copied from the dictionary in
updateDictionary(), before this insertion. If the default is a new extreme,
those cached bounds remain stale. For example, with null handling disabled,
repairing the only dictionary INT row from 42 to Integer.MIN_VALUE leaves
datasource bounds [42,42]. ColumnValueSegmentPruner then incorrectly prunes the
consuming segment for WHERE col < 0 even though its repaired row matches.
Please refresh the cached bounds in fallback dictionary-insertion paths, and
cover the datasource bounds/range-pruning behavior with null handling disabled.
##########
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java:
##########
@@ -735,12 +736,28 @@ private boolean processStreamEvents(MessageBatch
messageBatch, long idlePipeSlee
_serverMetrics.addMeteredGlobalValue(ServerMeter.REALTIME_BYTES_CONSUMED,
recordSerializedValueLength);
}
} catch (Exception e) {
- _numRowsErrored++;
- _numBytesDropped += rowSizeInBytes;
String errorMessage =
"Caught exception while indexing the record at offset: " +
offset + " , row: " + transformedRow;
_segmentLogger.error(errorMessage, e);
_realtimeTableDataManager.addSegmentError(_segmentNameStr, new
SegmentErrorInfo(now(), errorMessage, e));
+ // continueOnError=false finishes a started row, then rethrows.
A published repair is consumed;
+ // only an unpublished failure is a drop. An unrecoverable
repair is terminal.
+ if (_realtimeSegment.getNumDocsIndexed() > docsBeforeIndex) {
+ indexedMessageCount++;
+ _lastRowMetadata = metadata;
+ _lastConsumedTimestampMs = System.currentTimeMillis();
+ realtimeRowsConsumedMeter =
+ _serverMetrics.addMeteredTableValue(_clientId,
ServerMeter.REALTIME_ROWS_CONSUMED, 1,
+ realtimeRowsConsumedMeter);
+
_serverMetrics.addMeteredGlobalValue(ServerMeter.REALTIME_ROWS_CONSUMED, 1L);
+ } else {
+ _numRowsErrored++;
+ _numBytesDropped += rowSizeInBytes;
+ }
+ canTakeMore = _realtimeSegment.canTakeMoreRows();
+ if (!canTakeMore) {
+ throw new RuntimeException(errorMessage, e);
Review Comment:
[P1] Distinguish normal capacity completion from an unrecoverable failure
With the default continueOnError=false, a recoverable indexing error on the
row that fills the segment publishes the repaired row and then rethrows.
canTakeMoreRows() is false simply because _numDocsIndexed == _capacity, so this
branch throws before _currentOffset and _numRowsIndexed advance.
PartitionConsumer then transitions to State.ERROR instead of completing the
segment normally. A malformed JSON secondary-index value on the
capacity-filling row is one reachable example. Please check unrecoverable state
separately and let successfully published rows advance their offsets and
complete normally. StatelessRealtimeSegmentWriter.indexTransformedRow has the
same conflation. A real-segment regression with capacity one and a recoverable
JSON error would cover the distinction missing from the mocked tests.
--
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]