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


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/PercentileTDigestAccumulator.java:
##########
@@ -616,10 +610,8 @@ private static double weightedAverage(double firstValue, 
double firstWeight, dou
   public Collection<Centroid> centroids() {

Review Comment:
   [code-review] `centroids()` (and `centroidCount()` below) are missing the 
`_legacyDegraded` handling that add/compress/cdf/quantile/serialize all have. 
`mergeLegacyDegradedInput` never clears `_numCentroids`/`_centroidMeans`, and 
in degraded mode `compress()` returns early, so:
   
   - An accumulator holding N centroids (mass W1) that absorbs a poisoned 
legacy input (mass W2) reports `size() == W1+W2` and serializes a single 
NaN-mean centroid of weight W1+W2, but `centroids()` returns the N stale 
pre-degradation centroids summing to W1 and `centroidCount()` returns N.
   - A digest degraded from pending bytes returns an empty `centroids()` 
collection while `size() > 0`.
   
   Reproduced: values {1.0, 2.0} + poisoned serialized digest (weights [1, 
−0.5, 1]) → `size()` 3 (totalWeight 3.5) but `centroids()` = [1.0@1, 2.0@1]. 
Any consumer iterating the centroid view silently loses the degraded mass and 
disagrees with both `size()` and the wire bytes. Suggest returning the degraded 
single-centroid view (consistent with `serialize()`) or clearing the stale 
arrays on degradation.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TDigestUtils.java:
##########
@@ -207,331 +181,194 @@ 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;
-    }
-
-    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);
-    }
-
-    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;
+  /// Trailing bytes remain available to the caller for intermediate values 
containing several objects.
+  public static double validateSerialized(ByteBuffer input) {
+    return validateSerialized(input, true);
   }
 
-  /// Deserializes a digest and advances `input` past its encoded bytes.
-  public static TDigest deserialize(ByteBuffer input) {
-    ByteBuffer encoded = input.slice();
-    validateEncodedDigest(encoded.duplicate());
-
-    int encoding = encoded.getInt();
-    double min = encoded.getDouble();
-    double max = encoded.getDouble();
-    double compression;
-    int mainCapacity;
-    int bufferCapacity;
-    int centroidCount;
-    int centroidSize;
-    if (encoding == VERBOSE_ENCODING) {
-      compression = encoded.getDouble();
-      centroidCount = encoded.getInt();
-      mainCapacity = getDefaultCentroidCapacity(compression);
-      bufferCapacity = 5 * mainCapacity;
-      centroidSize = VERBOSE_CENTROID_SIZE;
-    } else if (encoding == SMALL_ENCODING) {
-      compression = encoded.getFloat();
-      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 double validateSerialized(ByteBuffer input, boolean 
checkCapacity) {
+    return inspectSerialized(input, checkCapacity).totalWeight();
   }
 
-  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;
+  /// Header and numerical metadata for one legacy byte payload. The centroid 
bytes remain owned by the caller.
+  /// Header-only inspection leaves total weight as NaN until centroid 
inspection validates the numerical fields.
+  ///
+  /// 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) {
   }
 
-  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;
+  /// Inspects one payload without mutating `input`, scanning the centroid 
values once.
+  public static SerializedTDigestMetadata inspectSerialized(ByteBuffer input) {
+    return inspectSerialized(input, 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();
+  /// Inspects a payload, optionally enforcing the legacy decoder's declared 
centroid capacity.
+  public static SerializedTDigestMetadata inspectSerialized(ByteBuffer input, 
boolean checkCapacity) {
+    return inspectSerialized(input, readSerializedHeader(input, 
checkCapacity), null, null);
   }
 
-  private static double readWeight(ByteBuffer encoded, int offset, int 
centroidSize) {
-    return centroidSize == VERBOSE_CENTROID_SIZE ? encoded.getDouble(offset) : 
encoded.getFloat(offset);
+  /// Reads and validates the fixed-size header without scanning or allocating 
centroid arrays.
+  public static SerializedTDigestMetadata readSerializedHeader(ByteBuffer 
input) {
+    return readSerializedHeader(input, true);
   }
 
-  private static void validateEncodedDigest(ByteBuffer input) {
-    int encoding = input.getInt();
+  /// Reads a fixed-size header, optionally enforcing the declared centroid 
capacity.
+  ///
+  /// Numerical flags and total weight are computed by the subsequent 
inspection or decoding pass.
+  public static SerializedTDigestMetadata readSerializedHeader(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");
     }
-    input.getDouble();
-    input.getDouble();
+    double min = encoded.getDouble();
+    double max = encoded.getDouble();
     double compression;
     int mainCapacity;
+    int bufferCapacity = 0;
     int centroidCount;
     int centroidSize;
     if (encoding == VERBOSE_ENCODING) {
-      compression = input.getDouble();
-      mainCapacity = getDefaultCentroidCapacity(compression);
-      centroidCount = input.getInt();
+      compression = normalizeCompression(encoded.getDouble());
+      mainCapacity = Math.max(getDefaultCentroidCapacity(compression), 
getLegacyDefaultCentroidCapacity(compression));
+      centroidCount = encoded.getInt();
       centroidSize = VERBOSE_CENTROID_SIZE;
     } else {
-      compression = input.getFloat();
-      mainCapacity = input.getShort();
-      int bufferCapacity = input.getShort();
+      compression = normalizeCompression(encoded.getFloat());
+      mainCapacity = encoded.getShort();
+      bufferCapacity = encoded.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);
-    }
-  }
-
-  private static void checkSmallArrayCapacity(int capacity) {
-    if (capacity < -1) {
-      throw new NegativeArraySizeException(Integer.toString(capacity));
+    if (Double.isNaN(min) || Double.isNaN(max) || (centroidCount > 0 && min > 
max)) {
+      throw new IllegalArgumentException("Invalid TDigest extrema: " + min + 
", " + max);
     }
+    int centroidOffset = encoded.position();
+    int encodedLength = centroidOffset + centroidCount * centroidSize;
+    return new SerializedTDigestMetadata(encoding, min, max, compression, 
centroidCount, mainCapacity,
+        bufferCapacity, centroidOffset, centroidSize, encodedLength, 
Double.NaN, false, false, false, false);
   }
 
-  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;
+  /// Validates centroids in one pass and optionally decodes their means and 
weights into reusable arrays.
+  ///
+  /// The header must describe this exact input, and each supplied array must 
hold all encoded centroids. Endpoint
+  /// rounding inherited from compact re-encoding or old merge arithmetic is 
restored to the double header bounds.
+  public static SerializedTDigestMetadata inspectSerialized(ByteBuffer input, 
SerializedTDigestMetadata header,
+      double[] means, double[] weights) {
+    ByteBuffer encoded = input.slice().order(ByteOrder.BIG_ENDIAN);
+    encoded.position(header.centroidOffset());
+    int centroidCount = header.centroidCount();
+    int centroidSize = header.centroidSize();
+    double min = header.min();
+    double max = header.max();
+    double totalWeight = 0.0;
+    double previousMean = Double.NEGATIVE_INFINITY;
+    boolean hasNonFiniteMeans = false;
+    boolean needsLegacyFallback = false;
+    boolean unorderedMeans = false;
+    boolean fractionalWeights = false;
+    boolean withinBounds = true;
+    double roundedMin = (double) (float) min;
+    double roundedMax = (double) (float) max;
+    double lowerAdjacent = Math.nextDown(min);
+    double upperAdjacent = Math.nextUp(max);
+    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)) {
+        // A compact digest can be re-emitted as verbose without recovering 
its double endpoint means. Old
+        // merge arithmetic can also drift by one ULP. Permit only these 
identifiable endpoint roundings.
+        boolean lowerRounded = mean == roundedMin || mean == lowerAdjacent;
+        boolean upperRounded = mean == roundedMax || mean == upperAdjacent;
+        withinBounds &= (mean >= min || lowerRounded) && (mean <= max || 
upperRounded);
+        previousMean = mean;
+      }
+      if (means != null) {
+        means[i] = Math.max(min, Math.min(mean, max));
+      }
+      if (weights != null) {
+        weights[i] = weight;
+      }
+      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:
   [code-review] A finite VERBOSE centroid mean one ulp outside `[min, max]` 
now hard-fails deserialization here, while the float-rounding allowance above 
(`lowerRounded`/`upperRounded`) is granted only to SMALL encoding. Legacy 
writers used unclamped incremental mean updates (tdunning 3.3 `mean[i] + (m − 
mean[i]) * w / W`; the pre-PR Pinot `mergeMeans` had no clamp), which can 
overshoot the recorded extrema by an ulp on an interior/unordered centroid — 
such bytes were accepted by every old reader, and the new code even tolerates 
the analogous ordering slop (`unorderedMeans`). A demonstrated overshoot case: 
mean merged with a legacy fractional residual weight (`1.0000001f − 1.0`) 
against a ~1e13 centroid lands exactly 1 ulp above max. Likelihood from real 
writers is low but nonzero, and there is no fallback route — the segment 
becomes unreadable. Consider clamping near-extrema verbose means (like 
`decode()` does for SMALL) or routing ulp-level violations to `needsLegacyFallba
 ck` instead of throwing.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/PercentileTDigestAccumulator.java:
##########
@@ -240,167 +271,89 @@ public void add(List<? extends TDigest> others) {
   }
 
   private void addAccumulator(PercentileTDigestAccumulator other) {
+    _hasFractionalWeights |= other._hasFractionalWeights;
+    if (other == this) {
+      addSerializedTDigest(serialize());
+      return;
+    }
     if (other._pendingSerializedTDigest != null) {
-      addSerializedTDigest(other._pendingSerializedTDigest);
+      SerializedTDigestInput input = new SerializedTDigestInput();
+      input.reset(other._pendingSerializedTDigest, 
other._pendingSerializedMetadata);
+      input._retainedBytes = other._pendingSerializedTDigest;
+      addSerializedTDigest(input);
+      return;
+    }
+    if (other._legacyDegraded) {
+      addSerializedTDigest(other.serialize());
       return;
     }
 
     other.compress();

Review Comment:
   [code-review] `other.compress()` mutates the *source* of the merge: it 
lossily recompresses (or materializes) the digest being read. Via 
`PercentileTDigestValueAggregator.copy()` / `applyAggregatedValue`, every 
star-tree parent aggregation and every `cloneAggregatedValue` call therefore 
modifies the child/original digest — a nominally read-only clone changes the 
source's subsequent quantiles and serialized bytes depending on whether it was 
also merged upward. tdunning's `add(other)` never recompressed its argument (it 
only flushed). Same pattern in the generic `add(TDigest other)` path above. 
Mass is preserved, so the impact is accuracy/determinism rather than a crash, 
but merge sources should not be destructively compressed; reading the buffered 
state (or flushing without the public recompress) would avoid it.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/PercentileTDigestAccumulator.java:
##########
@@ -696,34 +680,61 @@ public double getMin() {
   @Override
   public double getMax() {
     if (_pendingSerializedTDigest != null) {
-      return 
ByteBuffer.wrap(_pendingSerializedTDigest).getDouble(Integer.BYTES + 
Double.BYTES);
+      return _pendingSerializedMetadata.max();
     }
     flush();
     return _max;
   }
 
-  byte[] serialize() {
+  public byte[] serialize() {
+    if (_legacyDegraded) {
+      if (_pendingSerializedTDigest != null) {
+        return _pendingSerializedTDigest.clone();
+      }
+      if (_totalWeight == _legacySerializedTotalWeight && _min == 
_legacySerializedMin
+          && _max == _legacySerializedMax) {
+        return _legacySerializedBytes.clone();

Review Comment:
   [code-review] This stale-cache check makes legacy-degraded merge output 
order-dependent: a second poisoned input whose total weight is 0 (e.g. centroid 
weights +3 and −3 — admissible, since validation only requires finite weights 
and `needsLegacyFallback` is set by `weight <= 0`) with min/max inside the 
cached range leaves the `(_totalWeight,_min,_max)` tuple unchanged, so 
`serialize()` returns the FIRST input's original bytes verbatim; the reverse 
merge order emits `toLegacyDegradedBytes()`. Both decode as degraded/NaN 
downstream, so no semantic loss, but intermediate bytes become non-commutative 
across reducer orderings (breaks bytes-equality checks and rollup idempotency 
comparisons). Falling through to `toLegacyDegradedBytes()` once any second 
input has been merged would make the output canonical.



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