xiangfu0 commented on code in PR #19795:
URL: https://github.com/apache/pinot/pull/19795#discussion_r4253367162
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/tdigest/TDigestAccumulator.java:
##########
@@ -186,243 +330,292 @@ public void add(double value) {
if (Double.isNaN(value)) {
throw new IllegalArgumentException("Cannot add NaN to t-digest");
}
- materializePendingSerializedTDigest();
+ requireMutable();
+ prepareForMutation();
ensureRawBuffer();
- if (_numRawValues == _rawValues.length
- || _numRawValues + _numIncomingCentroids == getPendingInputLimit()) {
- flush();
- }
+ flushIfBuffersFull(true);
+ _originalFractionalBytes = null;
_rawValues[_numRawValues++] = value;
}
@Override
- public void add(double value, int weight) {
+ public void add(double value, double weight) {
if (weight == 1) {
add(value);
return;
}
if (Double.isNaN(value)) {
throw new IllegalArgumentException("Cannot add NaN to t-digest");
}
- materializePendingSerializedTDigest();
+ if (!(weight > 0.0) || !Double.isFinite(weight)) {
+ throw new IllegalArgumentException("TDigest weight must be positive: " +
weight);
+ }
+ requireMutable();
+ prepareForMutation();
bufferIncomingCentroid(value, weight);
+ _originalFractionalBytes = null;
+ _hasFractionalWeights |= weight != Math.rint(weight);
+ recordFreshFractionalMean(value, weight);
_min = Math.min(_min, value);
_max = Math.max(_max, value);
}
@Override
public void add(TDigest other) {
+ if (other.hasValidStatistics() && other.getTotalWeight() == 0.0) {
+ return;
+ }
+ requireMutable();
if (other instanceof TDigestAccumulator) {
addAccumulator((TDigestAccumulator) other);
return;
}
- if (other instanceof MergingDigest && other.size() > Integer.MAX_VALUE) {
- // Centroid.count() exposes an int even though MergingDigest stores
double weights internally. Preserve large
- // pre-aggregated weights by ingesting the serialized double-precision
representation instead.
- addSerializedTDigest(TDigestUtils.serialize(other));
+ if (other instanceof NonFiniteAwareTDigest) {
+ TDigestAccumulator finiteDigest = ((NonFiniteAwareTDigest)
other).finiteDigestIfNoTails();
+ if (finiteDigest != null) {
+ addAccumulator(finiteDigest);
+ return;
+ }
+ }
+ if (!other.hasValidStatistics()) {
+ materializePendingSerializedTDigest();
+ SerializedTDigestInput input = getSerializedTDigestInput();
+ input.reset(ByteBuffer.wrap(other.serialize()), false);
+ addSerializedTDigest(input);
return;
}
-
+ double otherWeight = other.getTotalWeight();
+ checkTotalWeight(getTotalWeight() + otherWeight);
Collection<Centroid> centroids = other.centroids();
- materializePendingSerializedTDigest();
+ boolean wrapped = other instanceof NonFiniteAwareTDigest;
+ int capacity = wrapped ? Math.addExact(centroids.size(), 2) :
centroids.size();
+ double[] means = new double[capacity];
+ double[] weights = new double[capacity];
+ int count = 0;
for (Centroid centroid : centroids) {
- bufferIncomingCentroid(centroid.mean(), centroid.count());
+ double weight = centroid.weight();
+ if (weight == 0.0) {
+ continue;
+ }
+ if (!(weight > 0.0) || !Double.isFinite(weight) ||
Double.isNaN(centroid.mean())) {
+ throw new IllegalArgumentException("Invalid TDigest centroid");
+ }
+ means[count] = centroid.mean();
+ weights[count++] = weight;
}
- _min = Math.min(_min, other.getMin());
- _max = Math.max(_max, other.getMax());
- }
-
- @Override
- public void add(List<? extends TDigest> others) {
- for (TDigest other : others) {
- add(other);
+ if (wrapped) {
+ count = normalizeSerializedBoundaries(means, weights, count,
other.getMin(), other.getMax());
}
+ // Validate all generic centroids before changing the target, then use the
same whole-source primitive path.
+ addCentroids(means, weights, count, other.getMin(), other.getMax(), false,
wrapped);
+ inheritHistoricalFractionalBoundaries(other);
}
private void addAccumulator(TDigestAccumulator other) {
+ if (other == this) {
+ compress();
+ prepareForMutation();
+ if (_numCentroids == 0) {
+ return;
+ }
+ // Snapshot only self-merges; fresh fractional mass need not be
representable by a legacy wire payload.
+ addHistoricalCentroids(Arrays.copyOf(_centroidMeans, _numCentroids),
+ Arrays.copyOf(_centroidWeights, _numCentroids), _numCentroids, _min,
_max, false);
+ return;
+ }
+ if (_deferBoundaryNormalization && !other._deferBoundaryNormalization) {
+ other.prepareForMutation();
+ }
+ if (other._originalBoundaryBytes != null) {
+ if (getTotalWeight() != 0.0) {
+ throw unrepresentableBoundaryMutation();
+ }
+ addSerializedTDigest(other._originalBoundaryBytes);
+ return;
+ }
+ if (other._originalFractionalBytes != null && getTotalWeight() == 0.0) {
+ materializePendingSerializedTDigest();
+ SerializedTDigestInput input = getSerializedTDigestInput();
+ input.reset(other._originalFractionalBytes);
+ input._retainedBytes = other._originalFractionalBytes;
+ addSerializedTDigest(input);
+ return;
+ }
if (other._pendingSerializedTDigest != null) {
- addSerializedTDigest(other._pendingSerializedTDigest);
+ // Materialize before resetting the reusable reader: materialization
uses that same reader.
+ materializePendingSerializedTDigest();
+ SerializedTDigestInput input = getSerializedTDigestInput();
+ input.reset(other._pendingSerializedTDigest,
other._pendingSerializedMetadata);
+ input._retainedBytes = other._pendingSerializedTDigest;
+ addSerializedTDigest(input);
return;
}
-
+ // MergingDigest compressed merge sources in place. Aggregation owns these
mutable inputs exclusively.
other.compress();
- materializePendingSerializedTDigest();
- preserveSerializedCapacity(other._serializedMainCapacity,
other._serializedBufferCapacity);
- bufferIncomingCentroids(other._centroidMeans, other._centroidWeights,
other._numCentroids);
- _min = Math.min(_min, other._min);
- _max = Math.max(_max, other._max);
+ other.prepareForMutation();
+ prepareForMutation();
+ checkTotalWeight(_totalWeight + _incomingWeight + _numRawValues +
other._totalWeight);
+ _hasFractionalWeights |= other._hasFractionalWeights;
+ inheritHistoricalFractionalBoundaries(other);
+ bufferIncomingCentroids(other._centroidMeans, other._centroidWeights,
other._numCentroids, other._totalWeight,
+ other._min, other._max);
+ if (other._totalWeight > 0.0) {
+ _originalFractionalBytes = null;
+ }
}
public void addSerializedTDigest(byte[] bytes) {
- if (!_hasSerializedInput && _numCentroids == 0 && _totalWeight == 0.0 &&
_numRawValues == 0
- && _numIncomingCentroids == 0) {
- byte[] retainedBytes = bytes.clone();
- _pendingSerializedTotalWeight = getSerializedTotalWeight(retainedBytes);
- _pendingSerializedTDigest = retainedBytes;
- _hasSerializedInput = true;
+ if (_legacyDegraded || _originalBoundaryBytes != null) {
+ SerializedTDigestInput input = getSerializedTDigestInput();
+ input.reset(bytes);
+ addSerializedTDigest(input);
return;
}
+ requireMutable();
materializePendingSerializedTDigest();
+ SerializedTDigestInput input = getSerializedTDigestInput();
+ input.reset(bytes);
+ addSerializedTDigest(input);
+ }
+
+ private SerializedTDigestInput getSerializedTDigestInput() {
if (_serializedTDigestInput == null) {
_serializedTDigestInput = new SerializedTDigestInput();
}
- _serializedTDigestInput.reset(bytes);
- _serializedTDigestInput.decode();
- mergeSerializedTDigest(_serializedTDigestInput);
- _hasSerializedInput = true;
+ return _serializedTDigestInput;
}
public void addSerializedTDigest(SerializedTDigestInput input) {
- if (!_hasSerializedInput && _numCentroids == 0 && _totalWeight == 0.0 &&
_numRawValues == 0
- && _numIncomingCentroids == 0) {
- _pendingSerializedTDigest = input.retainBytes();
- _pendingSerializedTotalWeight = Double.NaN;
- _hasSerializedInput = true;
+ if (ignoreEmptyInputForDegradedState(input)) {
return;
}
- materializePendingSerializedTDigest();
- input.decode();
- mergeSerializedTDigest(input);
- _hasSerializedInput = true;
+ requireMutable();
+ if (prepareSerializedTDigest(input)) {
+ mergeSerializedTDigest(input, false);
+ }
}
public void addSerializedTDigestDirect(SerializedTDigestInput input) {
- if (!_hasSerializedInput && _numCentroids == 0 && _totalWeight == 0.0 &&
_numRawValues == 0
- && _numIncomingCentroids == 0) {
- _pendingSerializedTDigest = input.retainBytes();
- _pendingSerializedTotalWeight = Double.NaN;
- _hasSerializedInput = true;
+ if (ignoreEmptyInputForDegradedState(input)) {
return;
}
- materializePendingSerializedTDigest();
- input.decode();
- preserveSerializedCapacity(input._mainCapacity, input._bufferCapacity);
- boolean initialize = _numCentroids == 0 && _totalWeight == 0.0 &&
_numRawValues == 0
- && _numIncomingCentroids == 0;
- if (initialize) {
- if (input._totalWeight != 0.0) {
- ensureCentroidCapacity(input._numCentroids);
- System.arraycopy(input._means, 0, _centroidMeans, 0,
input._numCentroids);
- System.arraycopy(input._weights, 0, _centroidWeights, 0,
input._numCentroids);
- _numCentroids = input._numCentroids;
- _totalWeight = input._totalWeight;
- _min = input._min;
- _max = input._max;
- }
- } else {
- flush();
- boolean runBackwards = (_mergeCount++ & 1) != 0;
- if (mergeSorted(input._means, input._weights, input._numCentroids,
input._totalWeight,
- getIncrementalCompression(),
- runBackwards)) {
- _min = Math.min(_min, input._min);
- _max = Math.max(_max, input._max);
- }
+ requireMutable();
+ if (prepareSerializedTDigest(input)) {
+ mergeSerializedTDigest(input, true);
}
- _hasSerializedInput = true;
- }
-
- private void mergeSerializedTDigest(byte[] bytes) {
- ByteBuffer input = ByteBuffer.wrap(bytes);
- int encoding = input.getInt();
- if (encoding != VERBOSE_ENCODING && encoding != SMALL_ENCODING) {
- throw new IllegalStateException("Invalid format for serialized
histogram");
- }
- double min = input.getDouble();
- double max = input.getDouble();
- int numCentroids;
- int mainCapacity = 0;
- int bufferCapacity = 0;
- boolean initialize = _numCentroids == 0 && _totalWeight == 0.0 &&
_numRawValues == 0
- && _numIncomingCentroids == 0;
- double[] incomingMeans;
- double[] incomingWeights;
- if (encoding == VERBOSE_ENCODING) {
- double serializedCompression = input.getDouble();
- numCentroids = input.getInt();
- checkCentroidCount(numCentroids);
- checkCentroidsAvailable(input, numCentroids, VERBOSE_CENTROID_SIZE);
- checkVerboseCentroidCapacity(serializedCompression, numCentroids);
- if (initialize) {
- ensureCentroidCapacity(Math.addExact(numCentroids, 2));
- incomingMeans = _centroidMeans;
- incomingWeights = _centroidWeights;
- } else {
- incomingMeans = new double[Math.addExact(numCentroids, 2)];
- incomingWeights = new double[Math.addExact(numCentroids, 2)];
- }
- for (int i = 0; i < numCentroids; i++) {
- incomingWeights[i] = input.getDouble();
- incomingMeans[i] = input.getDouble();
+ }
+
+ private boolean ignoreEmptyInputForDegradedState(SerializedTDigestInput
input) {
+ if (!_legacyDegraded && _originalBoundaryBytes == null
+ && (_pendingSerializedMetadata == null ||
!_pendingSerializedMetadata.weightedBoundaries())) {
+ return false;
+ }
+ input.inspectMetadata();
+ return !input._metadata._needsLegacyFallback &&
input._metadata._totalWeight == 0.0;
+ }
+
+ private boolean prepareSerializedTDigest(SerializedTDigestInput input) {
+ if (_pendingSerializedTDigest == null && hasNoInputs()) {
+ input.inspectMetadata();
+ _hasFractionalWeights |= input._metadata._fractionalWeights;
+ _pendingSerializedTDigest = input.retainBytes();
+ _pendingSerializedMetadata = input.getMetadata();
+ if (input.recordHistoricalFractionalBoundaries(this)) {
+ // Alias the retained snapshot; historical fractional bytes remain
byte-exact after read-only materialization.
+ _originalFractionalBytes = _pendingSerializedTDigest;
}
- } else {
- double serializedCompression = input.getFloat();
- mainCapacity = input.getShort();
- bufferCapacity = input.getShort();
- checkSmallArrayCapacities(mainCapacity, bufferCapacity);
- mainCapacity = resolveSmallMainCapacity(serializedCompression,
mainCapacity);
- numCentroids = input.getShort();
- checkCentroidCount(numCentroids);
- checkCentroidsAvailable(input, numCentroids, SMALL_CENTROID_SIZE);
- checkSmallCentroidCapacity(mainCapacity, numCentroids);
- if (initialize) {
- ensureCentroidCapacity(Math.addExact(numCentroids, 2));
- incomingMeans = _centroidMeans;
- incomingWeights = _centroidWeights;
- } else {
- incomingMeans = new double[Math.addExact(numCentroids, 2)];
- incomingWeights = new double[Math.addExact(numCentroids, 2)];
+ if (input._metadata._needsLegacyFallback) {
+ markLegacyDegraded();
}
- for (int i = 0; i < numCentroids; i++) {
- incomingWeights[i] = input.getFloat();
- incomingMeans[i] = input.getFloat();
+ return false;
+ }
+ materializePendingSerializedTDigest();
+ input.decode();
+ if (input._metadata._needsLegacyFallback) {
+ if (hasNoInputs()) {
+ _pendingSerializedTDigest = input.retainBytes();
+ _pendingSerializedMetadata = input.getMetadata();
+ markLegacyDegraded();
+ return false;
}
+ throw corruptedMutation();
Review Comment:
Addressed in 3b8517bbdb. The fail-loud policy for an unknown nonempty
distribution is intentional: unchanged bytes stay readable and byte-exact,
while unsafe merges fail rather than discard unknown mass. The linked migration
notes now explicitly describe old reducers completing with NaN/garbage versus
this failure policy, including query, rollup and star-tree impact. Existing
opaque-payload and mutation-rejection regressions remain.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/tdigest/TDigestAccumulator.java:
##########
@@ -1287,12 +1682,29 @@ private void ensureOutputCapacity(int capacity) {
}
}
- private static double getTotalWeight(double[] weights, int count) {
- double totalWeight = 0.0;
- for (int i = 0; i < count; i++) {
- totalWeight += weights[i];
+ static void checkTotalWeight(double totalWeight) {
+ if (!(totalWeight >= 0.0) || !Double.isFinite(totalWeight)) {
+ throw new IllegalArgumentException("Invalid TDigest total weight: " +
totalWeight);
}
- return totalWeight;
+ }
+
+ void requireMutable() {
+ if (_legacyDegraded) {
+ throw corruptedMutation();
Review Comment:
Addressed in 3b8517bbdb. Reproduced the finite-only +/-1.7e308 overflow
against genuine 3.3. Upgrade notes now call out corrupt intermediates produced
by old live servers, finishing producer upgrades, and rebuilding poisoned
stored digests. The legacy outcome depended on merge order: some old paths
already rejected NaN sources; the notes say old reducers could complete with
NaN or garbage.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/tdigest/TDigestAccumulator.java:
##########
@@ -536,59 +755,96 @@ public double quantile(double quantile) {
if (Double.isNaN(quantile) || quantile < 0.0 || quantile > 1.0) {
throw new IllegalArgumentException("q should be in [0,1], got " +
quantile);
}
+ if (_legacyDegraded) {
+ return Double.NaN;
+ }
materializePendingSerializedTDigest();
flush();
- if (_numCentroids == 0) {
+ return quantile(quantile, _numCentroids, _totalWeight, _min, _max,
_centroidMeans, _centroidWeights, null);
+ }
+
+ /// Interpolates the same distribution from primitive centroids or validated
encoded centroids, without a copy.
+ static double quantile(double quantile, int count, double totalWeight,
double min, double max, double[] means,
+ double[] weights, SerializedTDigestState encoded) {
+ if (count == 0) {
return Double.NaN;
}
- if (_numCentroids == 1) {
- return _centroidMeans[0];
+ if (count == 1) {
+ return mean(means, encoded, 0);
+ }
+ if (quantile == 0.0) {
+ return min;
+ }
+ if (quantile == 1.0) {
+ return max;
}
- double index = quantile * _totalWeight;
- if (index < 1.0) {
- return _min;
+ double index = quantile * totalWeight;
+ double firstWeight = weight(weights, encoded, 0);
+ if (firstWeight < 1.0 && index < firstWeight / 2.0) {
+ return weightedAverage(min, firstWeight / 2.0 - index, mean(means,
encoded, 0), index);
}
- if (_centroidWeights[0] > 1.0 && index < _centroidWeights[0] / 2.0) {
- return _min + (index - 1.0) / (_centroidWeights[0] / 2.0 - 1.0) *
(_centroidMeans[0] - _min);
+ if (firstWeight >= 1.0 && index < 1.0) {
+ return min;
}
- int lastIndex = _numCentroids - 1;
- if (index > _totalWeight - 1.0) {
- return _max;
+ if (weight(weights, encoded, 0) > 1.0 && index < weight(weights, encoded,
0) / 2.0) {
+ return min + (index - 1.0) / (weight(weights, encoded, 0) / 2.0 - 1.0) *
(mean(means, encoded, 0) - min);
+ }
+ int lastIndex = count - 1;
+ double lastWeight = weight(weights, encoded, lastIndex);
+ if (lastWeight < 1.0 && index >= totalWeight - lastWeight / 2.0) {
+ return weightedAverage(mean(means, encoded, lastIndex), totalWeight -
index, max,
+ index - (totalWeight - lastWeight / 2.0));
+ }
+ if (lastWeight >= 1.0 && index > totalWeight - 1.0) {
+ return max;
}
- if (_centroidWeights[lastIndex] > 1.0
- && _totalWeight - index <= _centroidWeights[lastIndex] / 2.0) {
- return _max - (_totalWeight - index - 1.0) /
(_centroidWeights[lastIndex] / 2.0 - 1.0)
- * (_max - _centroidMeans[lastIndex]);
+ if (weight(weights, encoded, lastIndex) > 1.0
+ && totalWeight - index <= weight(weights, encoded, lastIndex) / 2.0) {
+ return max - (totalWeight - index - 1.0) / (weight(weights, encoded,
lastIndex) / 2.0 - 1.0)
+ * (max - mean(means, encoded, lastIndex));
}
- double weightSoFar = _centroidWeights[0] / 2.0;
+ double weightSoFar = weight(weights, encoded, 0) / 2.0;
for (int i = 0; i < lastIndex; i++) {
- double halfWeight = (_centroidWeights[i] + _centroidWeights[i + 1]) /
2.0;
+ double halfWeight = (weight(weights, encoded, i) + weight(weights,
encoded, i + 1)) / 2.0;
if (weightSoFar + halfWeight > index) {
- double leftUnitWeight = 0.0;
- if (_centroidWeights[i] == 1.0) {
- if (index - weightSoFar < 0.5) {
- return _centroidMeans[i];
- }
- leftUnitWeight = 0.5;
+ double leftExcludedWeight = getPointMassHalfWeight(means, weights,
encoded, count, i);
+ if (index - weightSoFar < leftExcludedWeight) {
+ return mean(means, encoded, i);
}
- double rightUnitWeight = 0.0;
- if (_centroidWeights[i + 1] == 1.0) {
- if (weightSoFar + halfWeight - index <= 0.5) {
- return _centroidMeans[i + 1];
- }
- rightUnitWeight = 0.5;
+ double rightExcludedWeight = getPointMassHalfWeight(means, weights,
encoded, count, i + 1);
+ if (weightSoFar + halfWeight - index <= rightExcludedWeight) {
+ return mean(means, encoded, i + 1);
}
- double leftWeight = index - weightSoFar - leftUnitWeight;
- double rightWeight = weightSoFar + halfWeight - index -
rightUnitWeight;
- return weightedAverage(_centroidMeans[i], rightWeight,
_centroidMeans[i + 1], leftWeight);
+ double leftWeight = index - weightSoFar - leftExcludedWeight;
+ double rightWeight = weightSoFar + halfWeight - index -
rightExcludedWeight;
+ return weightedAverage(mean(means, encoded, i), rightWeight,
mean(means, encoded, i + 1), leftWeight);
}
weightSoFar += halfWeight;
}
- double weightToMax = index - _totalWeight - _centroidWeights[lastIndex] /
2.0;
- double weightFromLastCentroid = _centroidWeights[lastIndex] / 2.0 -
weightToMax;
- return weightedAverage(_centroidMeans[lastIndex], weightToMax, _max,
weightFromLastCentroid);
+ // Rounded cumulative weights can leave a small gap before the directly
computed last centroid center.
+ double weightToMax = Math.max(0.0, index - (totalWeight - lastWeight /
2.0));
+ double weightFromLastCentroid = lastWeight / 2.0 - weightToMax;
+ return weightedAverage(mean(means, encoded, lastIndex),
weightFromLastCentroid, max, weightToMax);
+ }
+
+ private static double getPointMassHalfWeight(double[] means, double[]
weights, SerializedTDigestState encoded,
Review Comment:
Addressed in 3b8517bbdb. Kept the intentional point-mass correction and made
identical-byte/old-new reducer differences explicit in the notes. The existing
plateau and duplicate-heavy regressions cover this beyond the rank envelope;
restoring singleton-only exclusion fails both tests and 1/270 frozen accuracy
cases. The local genuine 3.3 same-byte probe also reproduces your weighted seed
45 result.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -272,10 +273,10 @@ public TDigest extractGroupByResult(GroupByResultHolder
groupByResultHolder, int
@Override
public TDigest merge(TDigest intermediateResult1, TDigest
intermediateResult2) {
- if (intermediateResult1.size() == 0L) {
+ if (intermediateResult1.isEmpty()) {
Review Comment:
Addressed in 3b8517bbdb. Fixed this at codec inspection: zero centroids or
exclusively zero encoded weights are empty despite unused NaN/reversed extrema.
Positive/negative cancellation and non-finite weights still remain opaque.
Existing regressions now cover both encodings and reducer operand orders, byte
aggregation/group-by, and NULL-enabled/disabled extraction; the new zero-mass
cases fail before the fix and pass afterward.
--
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]