xiangfu0 commented on code in PR #19795:
URL: https://github.com/apache/pinot/pull/19795#discussion_r4216245964


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TDigestUtils.java:
##########
@@ -207,331 +180,155 @@ public static TDigest deserialize(byte[] bytes) {
     return deserialize(ByteBuffer.wrap(bytes));
   }
 
-  /// Validates the structure and declared capacities of a serialized merging 
digest.
-  public static void validateSerialized(byte[] bytes) {
-    validateEncodedDigest(ByteBuffer.wrap(bytes));
+  /// Validates serialized lengths and centroid values without allocating 
centroid arrays.
+  public static double validateSerialized(byte[] bytes) {
+    return validateSerialized(ByteBuffer.wrap(bytes));
   }
 
-  /// Deserializes a finite-valued digest into a merging digest with the 
temporary-buffer footprint used by 3.2.
+  /// Validates one legacy digest without changing `input` and returns its 
total centroid weight.
   ///
-  /// Unlike the standard verbose decoder, this method does not retain the 
substantially larger default 3.3
-  /// temporary arrays. It also repairs legacy weighted boundary centroids 
before the first 3.3 merge.
-  public static TDigest deserializeFiniteWithLegacyBuffer(byte[] bytes) {
-    ByteBuffer input = ByteBuffer.wrap(bytes);
-    validateEncodedDigest(input.duplicate());
-
-    int encoding = input.getInt();
-    double min = input.getDouble();
-    double max = input.getDouble();
-    double compression;
-    int centroidCount;
-    boolean verbose;
-    if (encoding == VERBOSE_ENCODING) {
-      compression = input.getDouble();
-      centroidCount = input.getInt();
-      verbose = true;
-    } else if (encoding == SMALL_ENCODING) {
-      compression = input.getFloat();
-      input.getShort();
-      input.getShort();
-      centroidCount = input.getShort();
-      verbose = false;
-    } else {
-      throw new IllegalStateException("Invalid format for serialized 
histogram");
-    }
-
-    if (centroidCount == 0) {
-      return createMergingDigestWithLegacyBuffer(compression);
-    }
-    double[] means = new double[centroidCount + 2];
-    double[] weights = new double[centroidCount + 2];
-    for (int i = 0; i < centroidCount; i++) {
-      double weight = verbose ? input.getDouble() : input.getFloat();
-      double mean = verbose ? input.getDouble() : input.getFloat();
-      if (!(weight > 0.0) || !Double.isFinite(weight)) {
-        throw new IllegalArgumentException("Invalid TDigest centroid weight: " 
+ weight);
-      }
-      if (!Double.isFinite(mean)) {
-        throw new IllegalArgumentException("Expected a finite TDigest centroid 
mean: " + mean);
-      }
-      weights[i] = weight;
-      means[i] = mean;
-    }
+  /// Trailing bytes remain available to the caller for intermediate values 
containing several objects.
+  public static double validateSerialized(ByteBuffer input) {
+    return validateSerialized(input, true);
+  }
 
-    int normalizedCount = weights[0] != 1.0 || weights[centroidCount - 1] != 
1.0
-        ? normalizeBoundaries(means, weights, centroidCount, min, max) : 
centroidCount;
-    long addCount = 0L;
-    for (int i = 0; i < normalizedCount; i++) {
-      double weight = weights[i];
-      if (weight != Math.rint(weight) || weight >= 0x1p63) {
-        return copyIntoLegacyBuffer(
-            deserialize(toVerboseBytes(min, max, compression, means, weights, 
normalizedCount)), normalizedCount);
-      }
-      long integralWeight = (long) weight;
-      addCount = Math.addExact(addCount, 1L + (integralWeight - 1L) / 
Integer.MAX_VALUE);
-    }
+  private static double validateSerialized(ByteBuffer input, boolean 
checkCapacity) {
+    return inspectSerialized(input, checkCapacity).totalWeight();
+  }
 
-    int minimumMainCapacity = Math.addExact(Math.toIntExact(addCount), 2);
-    TDigest digest = createMergingDigestWithLegacyBuffer(compression, 
minimumMainCapacity);
-    for (int i = 0; i < normalizedCount; i++) {
-      long remainingWeight = (long) weights[i];
-      while (remainingWeight > 0L) {
-        int weight = (int) Math.min(remainingWeight, Integer.MAX_VALUE);
-        digest.add(means[i], weight);
-        remainingWeight -= weight;
-      }
-    }
-    return digest;
+  /// Validated metadata for one legacy byte payload. The centroid bytes 
remain owned by the caller.
+  ///
+  /// Legacy writers can emit poisoned numerical state after overflow or 
broken fractional boundary repair.
+  /// Such state is retained with degraded quantiles, rather than being passed 
to the sorted K1 kernel.
+  public record SerializedTDigestMetadata(int encoding, double min, double 
max, double compression,
+      int centroidCount, int mainCapacity, int bufferCapacity, int 
centroidOffset, int centroidSize,
+      int encodedLength, double totalWeight, boolean hasNonFiniteMeans, 
boolean needsLegacyFallback,
+      boolean unorderedMeans, boolean fractionalWeights) {
   }
 
-  /// Deserializes a digest and advances `input` past its encoded bytes.
-  public static TDigest deserialize(ByteBuffer input) {
-    ByteBuffer encoded = input.slice();
-    validateEncodedDigest(encoded.duplicate());
+  /// Inspects one payload without mutating `input`, scanning the centroid 
values once.
+  public static SerializedTDigestMetadata inspectSerialized(ByteBuffer input) {
+    return inspectSerialized(input, true);
+  }
 
+  /// Inspects a payload, optionally enforcing the legacy decoder's declared 
centroid capacity.
+  public static SerializedTDigestMetadata inspectSerialized(ByteBuffer input, 
boolean checkCapacity) {
+    ByteBuffer encoded = input.slice().order(ByteOrder.BIG_ENDIAN);
     int encoding = encoded.getInt();
+    if (encoding != VERBOSE_ENCODING && encoding != SMALL_ENCODING) {
+      throw new IllegalStateException("Invalid format for serialized 
histogram");
+    }
     double min = encoded.getDouble();
     double max = encoded.getDouble();
     double compression;
     int mainCapacity;
-    int bufferCapacity;
+    int bufferCapacity = 0;
     int centroidCount;
     int centroidSize;
     if (encoding == VERBOSE_ENCODING) {
       compression = encoded.getDouble();
+      validateCompression(compression);
+      mainCapacity = Math.max(getDefaultCentroidCapacity(compression), 
getLegacyDefaultCentroidCapacity(compression));
       centroidCount = encoded.getInt();
-      mainCapacity = getDefaultCentroidCapacity(compression);
-      bufferCapacity = 5 * mainCapacity;
       centroidSize = VERBOSE_CENTROID_SIZE;
-    } else if (encoding == SMALL_ENCODING) {
+    } else {
       compression = encoded.getFloat();
+      validateCompression(compression);
       mainCapacity = encoded.getShort();
       bufferCapacity = encoded.getShort();
-      centroidCount = encoded.getShort();
-      centroidSize = SMALL_CENTROID_SIZE;
-    } else {
-      // MergingDigest.fromBytes() has already produced the canonical 
exception for this case.
-      throw new IllegalStateException("Invalid format for serialized 
histogram");
-    }
-    int centroidOffset = encoded.position();
-    if (centroidCount == 0) {
-      return configureScaleFunction(MergingDigest.fromBytes(input));
-    }
-
-    double firstWeight = readWeight(encoded, centroidOffset, centroidSize);
-    double lastWeight = readWeight(encoded, centroidOffset + (centroidCount - 
1) * centroidSize, centroidSize);
-    int defaultMainCapacity = getDefaultCentroidCapacity(compression);
-    boolean needsBoundaryRepair = firstWeight != 1.0 || lastWeight != 1.0;
-    boolean exceedsDefaultVerboseCapacity = encoding == VERBOSE_ENCODING && 
centroidCount > defaultMainCapacity;
-    if (!needsBoundaryRepair && !exceedsDefaultVerboseCapacity) {
-      return configureScaleFunction(MergingDigest.fromBytes(input));
-    }
-
-    double[] means = new double[centroidCount + 2];
-    double[] weights = new double[centroidCount + 2];
-    encoded.position(centroidOffset);
-    for (int i = 0; i < centroidCount; i++) {
-      if (centroidSize == VERBOSE_CENTROID_SIZE) {
-        weights[i] = encoded.getDouble();
-        means[i] = encoded.getDouble();
-      } else {
-        weights[i] = encoded.getFloat();
-        means[i] = encoded.getFloat();
-      }
-    }
-    int encodedLength = encoded.position();
-    int normalizedCount = needsBoundaryRepair
-        ? normalizeBoundaries(means, weights, centroidCount, min, max) : 
centroidCount;
-    byte[] normalizedBytes = toVerboseBytes(min, max, compression, means, 
weights, normalizedCount);
-    MergingDigest result;
-    if (normalizedCount <= defaultMainCapacity) {
-      result = MergingDigest.fromBytes(ByteBuffer.wrap(normalizedBytes));
-    } else {
-      int resolvedMainCapacity = mainCapacity == -1 ? defaultMainCapacity : 
mainCapacity;
-      int resolvedBufferCapacity = bufferCapacity == -1
-          ? DEFAULT_MERGE_BUFFER_MULTIPLIER * resolvedMainCapacity : 
bufferCapacity;
-      int smallMainCapacity = Math.max(resolvedMainCapacity, normalizedCount);
-      long smallBufferCapacity = Math.max(Math.max((long) 
resolvedBufferCapacity, smallMainCapacity + 1L),
-          2L * smallMainCapacity + 1L);
-      if ((double) (float) compression == compression && normalizedCount <= 
Short.MAX_VALUE
-          && smallMainCapacity <= Short.MAX_VALUE && smallBufferCapacity <= 
Short.MAX_VALUE
-          && areFloatExact(means, weights, normalizedCount)) {
-        ByteBuffer normalized = ByteBuffer.allocate(SMALL_HEADER_SIZE + 
SMALL_CENTROID_SIZE * normalizedCount);
-        normalized.putInt(SMALL_ENCODING);
-        normalized.putDouble(min);
-        normalized.putDouble(max);
-        normalized.putFloat((float) compression);
-        normalized.putShort((short) smallMainCapacity);
-        normalized.putShort((short) smallBufferCapacity);
-        normalized.putShort((short) normalizedCount);
-        for (int i = 0; i < normalizedCount; i++) {
-          normalized.putFloat((float) weights[i]);
-          normalized.putFloat((float) means[i]);
-        }
-        normalized.flip();
-        result = MergingDigest.fromBytes(normalized);
-      } else {
-        result = MergingDigest.fromBytes(
-            ByteBuffer.wrap(reduceVerboseCentroids(normalizedBytes, 
defaultMainCapacity)));
-      }
-    }
-    input.position(Math.addExact(input.position(), encodedLength));
-    return configureScaleFunction(result);
-  }
-
-  private static TDigest configureScaleFunction(MergingDigest digest) {
-    digest.setScaleFunction(ScaleFunction.K_1);
-    return digest;
-  }
-
-  private static TDigest copyIntoLegacyBuffer(TDigest source, int 
centroidCount) {
-    TDigest digest = createMergingDigestWithLegacyBuffer(source.compression(), 
Math.addExact(centroidCount, 2));
-    digest.add(java.util.List.of(source));
-    return digest;
-  }
-
-  private static boolean areFloatExact(double[] means, double[] weights, int 
count) {
-    for (int i = 0; i < count; i++) {
-      if ((double) (float) weights[i] != weights[i] || (double) (float) 
means[i] != means[i]) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private static byte[] toVerboseBytes(double min, double max, double 
compression, double[] means, double[] weights,
-      int count) {
-    ByteBuffer normalized = ByteBuffer.allocate(VERBOSE_HEADER_SIZE + 
VERBOSE_CENTROID_SIZE * count);
-    normalized.putInt(VERBOSE_ENCODING);
-    normalized.putDouble(min);
-    normalized.putDouble(max);
-    normalized.putDouble(compression);
-    normalized.putInt(count);
-    for (int i = 0; i < count; i++) {
-      normalized.putDouble(weights[i]);
-      normalized.putDouble(means[i]);
-    }
-    return normalized.array();
-  }
-
-  private static double readWeight(ByteBuffer encoded, int offset, int 
centroidSize) {
-    return centroidSize == VERBOSE_CENTROID_SIZE ? encoded.getDouble(offset) : 
encoded.getFloat(offset);
-  }
-
-  private static void validateEncodedDigest(ByteBuffer input) {
-    int encoding = input.getInt();
-    if (encoding != VERBOSE_ENCODING && encoding != SMALL_ENCODING) {
-      throw new IllegalStateException("Invalid format for serialized 
histogram");
-    }
-    input.getDouble();
-    input.getDouble();
-    double compression;
-    int mainCapacity;
-    int centroidCount;
-    int centroidSize;
-    if (encoding == VERBOSE_ENCODING) {
-      compression = input.getDouble();
-      mainCapacity = getDefaultCentroidCapacity(compression);
-      centroidCount = input.getInt();
-      centroidSize = VERBOSE_CENTROID_SIZE;
-    } else {
-      compression = input.getFloat();
-      mainCapacity = input.getShort();
-      int bufferCapacity = input.getShort();
       checkSmallArrayCapacity(mainCapacity);
       checkSmallArrayCapacity(bufferCapacity);
       if (mainCapacity == -1) {
         mainCapacity = getDefaultCentroidCapacity(compression);
       }
-      centroidCount = input.getShort();
+      centroidCount = encoded.getShort();
       centroidSize = SMALL_CENTROID_SIZE;
     }
     if (centroidCount < 0) {
       throw new IllegalArgumentException("Invalid negative TDigest centroid 
count: " + centroidCount);
     }
-    if (centroidCount > input.remaining() / centroidSize) {
+    if (centroidCount > encoded.remaining() / centroidSize) {
       throw new BufferUnderflowException();
     }
-    int readableMainCapacity = mainCapacity;
-    if (encoding == VERBOSE_ENCODING && compression > 0.0 && 
Double.isFinite(compression)) {
-      readableMainCapacity = Math.max(mainCapacity, 
getLegacyDefaultCentroidCapacity(compression));
+    if (checkCapacity && centroidCount > mainCapacity) {
+      throw new IllegalArgumentException("TDigest centroid count exceeds 
capacity: " + centroidCount);
     }
-    if (centroidCount > readableMainCapacity) {
-      throw new ArrayIndexOutOfBoundsException(
-          "Index " + readableMainCapacity + " out of bounds for length " + 
readableMainCapacity);
+    if (Double.isNaN(min) || Double.isNaN(max) || (centroidCount > 0 && min > 
max)) {
+      throw new IllegalArgumentException("Invalid TDigest extrema: " + min + 
", " + max);
     }
-  }
-
-  private static void checkSmallArrayCapacity(int capacity) {
-    if (capacity < -1) {
-      throw new NegativeArraySizeException(Integer.toString(capacity));
-    }
-  }
-
-  private static int normalizeBoundaries(double[] means, double[] weights, int 
count, double min, double max) {
-    if (count == 1) {
-      double weight = weights[0];
-      if (weight <= 1.0) {
-        return count;
+    int centroidOffset = encoded.position();
+    double totalWeight = 0.0;
+    double previousMean = Double.NEGATIVE_INFINITY;
+    boolean hasNonFiniteMeans = false;
+    boolean needsLegacyFallback = false;
+    boolean unorderedMeans = false;
+    boolean fractionalWeights = false;
+    boolean withinBounds = true;
+    for (int i = 0; i < centroidCount; i++) {
+      double weight = centroidSize == VERBOSE_CENTROID_SIZE ? 
encoded.getDouble() : encoded.getFloat();
+      double mean = centroidSize == VERBOSE_CENTROID_SIZE ? 
encoded.getDouble() : encoded.getFloat();
+      if (!Double.isFinite(weight)) {
+        throw new IllegalArgumentException("Invalid TDigest centroid weight: " 
+ weight);
       }
-      double mean = means[0];
-      means[0] = min;
-      weights[0] = 1.0;
-      if (weight == 2.0) {
-        means[1] = max;
-        weights[1] = 1.0;
-        return 2;
+      fractionalWeights |= weight != Math.rint(weight);
+      // A previous Pinot boundary repair emitted a negative residual for 
singleton weights between one and two.
+      // Old tdunning merges could also overflow finite means to NaN or 
infinity. Preserve those stored bytes,
+      // but never normalize or sort them into a plausible distribution.
+      needsLegacyFallback |= weight <= 0.0 || Double.isNaN(mean)
+          || !Double.isFinite(mean) && (mean < min || mean > max);
+      hasNonFiniteMeans |= !Double.isFinite(mean);
+      unorderedMeans |= mean < previousMean;
+      if (!Double.isNaN(mean)) {
+        // Compact means are floats but extrema are doubles. Permit only the 
rounding of an actual endpoint,
+        // rather than admitting arbitrary out-of-range means that boundary 
normalization would rewrite.
+        boolean lowerRounded = encoding == SMALL_ENCODING && mean == (double) 
(float) min;
+        boolean upperRounded = encoding == SMALL_ENCODING && mean == (double) 
(float) max;
+        withinBounds &= (mean >= min || lowerRounded) && (mean <= max || 
upperRounded);
+        previousMean = mean;
+      }
+      totalWeight += weight;
+      if (!Double.isFinite(totalWeight)) {
+        throw new IllegalArgumentException("TDigest total weight exceeds the 
supported range");
       }
-      means[1] = residualMean(mean, weight, min, max, weight - 2.0, min, max);
-      weights[1] = weight - 2.0;
-      means[2] = max;
-      weights[2] = 1.0;
-      return 3;
-    }
-
-    if (weights[0] > 1.0) {
-      System.arraycopy(means, 1, means, 2, count - 1);
-      System.arraycopy(weights, 1, weights, 2, count - 1);
-      double weight = weights[0];
-      double mean = means[0];
-      means[0] = min;
-      weights[0] = 1.0;
-      means[1] = residualMean(mean, weight, min, 0.0, weight - 1.0, min, 
means[2]);
-      weights[1] = weight - 1.0;
-      count++;
     }
-    int lastIndex = count - 1;
-    if (weights[lastIndex] > 1.0) {
-      double weight = weights[lastIndex];
-      double mean = means[lastIndex];
-      means[lastIndex] = residualMean(mean, weight, max, 0.0, weight - 1.0, 
means[lastIndex - 1], max);
-      weights[lastIndex] = weight - 1.0;
-      means[count] = max;
-      weights[count] = 1.0;
-      count++;
+    if (!needsLegacyFallback && !withinBounds) {
+      throw new IllegalArgumentException("TDigest centroid mean is outside its 
extrema: " + min + ", " + max);

Review Comment:
   **[correctness] Legacy verbose bytes with a mean marginally outside [min, 
max] are now permanently unreadable.**
   
   `if (!needsLegacyFallback && !withinBounds) { throw new 
IllegalArgumentException("TDigest centroid mean is outside its extrema...") }` 
has no tolerance for the VERBOSE encoding (the float-rounded-endpoint exemption 
at L284-286 is gated on `encoding == SMALL_ENCODING`). t-digest 3.2's 
`byteSize()/asBytes()` re-serializes a digest read from the compact encoding 
without merging (`mergeNewValues()` is a no-op when `unmergedWeight == 0`), 
emitting verbose bytes whose boundary mean is the float-rounded extremum, e.g. 
`min=0.7` with first mean `(float) 0.7 = 0.699999988 < 0.7`. The old reader 
(`MergingDigest.fromBytes`) accepted such bytes; reproduced that the new 
`TDigestUtils.deserialize` throws on them. A 1-ulp overshoot above `max` (e.g. 
from the previous unclamped `mergeMeans`) fails the same way. Any stored BYTES 
column or rolling-upgrade intermediate containing such bytes makes percentile 
queries and merge-rollup fail permanently — and note the sibling drift symptom 
`unorderedMe
 ans` is repaired (sorted in `decode()`) rather than rejected, so rejecting 
boundary drift here is inconsistent. Consider clamping/repairing finite means 
to the header extrema (or extending the rounding exemption to VERBOSE) instead 
of throwing.



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