Vamsi-klu commented on code in PR #19088:
URL: https://github.com/apache/pinot/pull/19088#discussion_r3725701821


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java:
##########
@@ -820,182 +831,419 @@ private void validateLengthOfMVColumns(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) {
+    boolean dictHadError = updateDictionary(row);
+    boolean rowHadError = addNewRow(docId, row);
+    if (dictHadError || rowHadError) {
+      recordIncompleteRow();
+    }
+  }
+
+  /// @return {@code true} if any column required a default/fallback while 
updating dictionaries
+  private boolean updateDictionary(GenericRow row) {
+    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) {
+          // Prefer default-null dict entry so addNewRow can still complete 
the forward index for this docId
+          // (fail-soft; issue #16316). Meter and fall back to field-spec 
default.
+          recordIndexingError("DICTIONARY");
+          hadError = true;
+          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) {
+        // Do not abort the row mid-dictionary: remaining columns still get a 
chance, and addNewRow will fill
+        // defaults for this column if dict ids are unset (Integer.MIN_VALUE / 
null).
+        hadError = true;
+        recordIndexingError("DICTIONARY", e);
+        indexContainer._dictId = Integer.MIN_VALUE;
+        indexContainer._dictIds = null;
       }

Review Comment:
   Good catch, this is a real gap on the aggregation path. index() calls 
updateDictionary() and then getOrCreateDocId(), and the key is built straight 
from indexContainer._dictId for every dimension and time column, so the 
Integer.MIN_VALUE sentinel does flow into the rollup key. Two failed rows with 
different real values would collapse into one key, and the default recovery in 
addPhysicalColumn happens only after keying. I will move the fallback into the 
updateDictionary() catch block: index getDefaultNullValueForIndexing(fieldSpec) 
into the dictionary and set _dictId/_dictIds from that, keeping 
Integer.MIN_VALUE only if indexing the default also throws. The MIN_VALUE 
recovery in addPhysicalColumn stays as a last resort, and the row is still 
metered incomplete via dictHadError.



##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/IndexingFailureTest.java:
##########
@@ -133,8 +133,46 @@ public void testIndexingFailures()
     
assertEquals(_mutableSegment.getDataSource(JSON_COL).getJsonIndex().getMatchingDocIds("valid
 = 'json'"),
         ImmutableRoaringBitmap.bitmapOf(0, 2, 3));
     
assertTrue(_mutableSegment.getDataSource(STRING_COL).getNullValueVector().isNull(3));
-    // null string value skipped
+    // Fail-soft (#16316): null string is completed with the field default so 
forward lengths stay aligned.
+    GenericRow nullResult = _mutableSegment.getRecord(3, new GenericRow());
+    assertEquals(nullResult.getValue(STRING_COL), 
FieldSpec.DEFAULT_DIMENSION_NULL_VALUE_OF_STRING);
     verify(_serverMetrics, 
times(1)).addMeteredTableValue(matches("DICTIONARY-indexingError$"),
         eq(ServerMeter.INDEXING_FAILURES), eq(1L));

Review Comment:
   I checked this and the matcher does match the emitted key. Mockito's 
ArgumentMatchers.matches() delegates to org.mockito.internal.matchers.Matches, 
which calls pattern.matcher(actual).find(), a substring search rather than a 
whole-string match (verified against mockito-core 5.23.0, the version pinned in 
the root pom). So matches("DICTIONARY-indexingError$") finds the anchored 
suffix inside "testTable_REALTIME-DICTIONARY-indexingError". This assertion is 
also unchanged from master (it shows up as context in the diff, and the 
pre-existing checks at lines 90 and 104 use the same style), and 
IndexingFailureTest passes in CI on this PR. I will keep it as is.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java:
##########
@@ -820,182 +831,419 @@ private void validateLengthOfMVColumns(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) {
+    boolean dictHadError = updateDictionary(row);
+    boolean rowHadError = addNewRow(docId, row);
+    if (dictHadError || rowHadError) {
+      recordIncompleteRow();
+    }
+  }
+
+  /// @return {@code true} if any column required a default/fallback while 
updating dictionaries
+  private boolean updateDictionary(GenericRow row) {
+    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) {
+          // Prefer default-null dict entry so addNewRow can still complete 
the forward index for this docId
+          // (fail-soft; issue #16316). Meter and fall back to field-spec 
default.
+          recordIndexingError("DICTIONARY");
+          hadError = true;
+          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) {
+        // Do not abort the row mid-dictionary: remaining columns still get a 
chance, and addNewRow will fill
+        // defaults for this column if dict ids are unset (Integer.MIN_VALUE / 
null).
+        hadError = true;
+        recordIndexingError("DICTIONARY", e);
+        indexContainer._dictId = Integer.MIN_VALUE;
+        indexContainer._dictIds = null;
       }
-      updateIndexCapacityThresholdBreached(dictionary, entry.getKey());
+      updateIndexCapacityThresholdBreached(dictionary, column);
     }
+    return hadError;
   }
 
-  private void addNewRow(int docId, GenericRow row) {
+  /// Indexes a new physical row. Fail-soft (issue #16316): every physical 
column must end up with a forward-index
+  /// (or OPEN_STRUCT) entry for [docId] so seal/query lengths stay aligned 
with [_numDocsIndexed]. On forward-index
+  /// failure the column is completed with the field default/null instead of 
being left blank. Secondary index
+  /// failures are still metered and swallowed. Aggregation path failures also 
fall back to a default initial value.
+  ///
+  /// @return {@code true} if any column required a default/fallback while 
indexing
+  private boolean addNewRow(int docId, GenericRow row) {
+    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)) {
+            rowHadError = true;
+          }
+        } else if (!addPhysicalColumn(docId, row, column, indexContainer)) {
+          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) {
+        // Last-resort complete-the-row so a single bad column cannot leave a 
half-written docId.
+        rowHadError = true;
+        recordIndexingError("ROW", e);
+        try {
+          indexDefaultNullColumn(docId, indexContainer);
+        } catch (Exception fallbackError) {
+          _logger.error("Failed to index default null for column: {} at docId: 
{}", column, docId, 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 {
+        _multiColumnTextIndex.add(_multiColumnValues);
+      } catch (Exception e) {
+        rowHadError = true;
+        recordIndexingError("MULTI_COLUMN_TEXT", e);
+      } finally {
+        Collections.fill(_multiColumnValues, null);
       }
+    }
+    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) {
+    ValueAggregator valueAggregator = indexContainer._valueAggregator;
+    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;
+    }
 
-      FieldSpec fieldSpec = indexContainer._fieldSpec;
-      DataType dataType = fieldSpec.getDataType();
+    MutableIndex forwardIndex = 
indexContainer._mutableIndexes.get(StandardIndexes.forward());
+    FieldSpec fieldSpec = indexContainer._fieldSpec;
+    DataType dataType = fieldSpec.getDataType();
+    try {
+      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);
+      }
+      indexContainer._valuesInfo.updateSVNumValues();
+      return true;
+    } catch (Exception e) {
+      recordIndexingError(StandardIndexes.forward(), e);
+      indexDefaultAggregatedValue(docId, indexContainer);
+      return false;
+    }
+  }
 
-      if (fieldSpec.isSingleValueField()) {
-        // Update numValues info
+  /// Returns {@code true} when the physical column was written without error.
+  private boolean addPhysicalColumn(int docId, GenericRow row, String column, 
IndexContainer indexContainer) {
+    FieldSpec fieldSpec = indexContainer._fieldSpec;
+    DataType dataType = fieldSpec.getDataType();
+    boolean isNull = row.isNullValue(column);
+    Object value = row.getValue(column);
+    if (value == null) {
+      // Should not happen after NullValueTransformer, but complete the row 
with defaults rather than leaving a hole.
+      recordIndexingError("NULL_VALUE");
+      value = getDefaultNullValueForIndexing(fieldSpec);
+      isNull = true;
+    }

Review Comment:
   Agreed. The null-value fallback at the top of addPhysicalColumn() 
substitutes the field default and meters NULL_VALUE, but the hadError flags are 
declared after that block, so the method can still return true and addNewRow() 
never meters the row as incomplete. The OPEN_STRUCT branch's early return true 
has the same gap. I will track the fallback in a flag initialized before the 
null check and fold it into all three return paths (single-value, multi-value, 
and OPEN_STRUCT) so any default substitution propagates to rowHadError and 
triggers recordIncompleteRow() once for the row. This only changes behavior for 
raw/no-dictionary columns, since updateDictionary() already flags the null case 
for dictionary columns.



-- 
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]

Reply via email to