Skip to content

Commit b4c8a3e

Browse files
Paul Sanwaldjasontedor
authored andcommitted
backport of #32723 (#32953)
Update test logic to correctly bucket intervals.
1 parent 08447de commit b4c8a3e

File tree

3 files changed

+97
-24
lines changed

3 files changed

+97
-24
lines changed

server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregationBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public int getNumBuckets() {
170170
return new AutoDateHistogramAggregatorFactory(name, config, numBuckets, roundings, context, parent, subFactoriesBuilder, metaData);
171171
}
172172

173-
private static Rounding createRounding(DateTimeUnit interval, DateTimeZone timeZone) {
173+
static Rounding createRounding(DateTimeUnit interval, DateTimeZone timeZone) {
174174
Rounding.Builder tzRoundingBuilder = Rounding.builder(interval);
175175
if (timeZone != null) {
176176
tzRoundingBuilder.timeZone(timeZone);

server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ private BucketReduceResult addEmptyBuckets(BucketReduceResult currentResult, Red
418418
return currentResult;
419419
}
420420
int roundingIdx = getAppropriateRounding(list.get(0).key, list.get(list.size() - 1).key, currentResult.roundingIdx,
421-
bucketInfo.roundingInfos);
421+
bucketInfo.roundingInfos, targetBuckets);
422422
RoundingInfo roundingInfo = bucketInfo.roundingInfos[roundingIdx];
423423
Rounding rounding = roundingInfo.rounding;
424424
// merge buckets using the new rounding
@@ -447,8 +447,8 @@ private BucketReduceResult addEmptyBuckets(BucketReduceResult currentResult, Red
447447
return new BucketReduceResult(list, roundingInfo, roundingIdx);
448448
}
449449

450-
private int getAppropriateRounding(long minKey, long maxKey, int roundingIdx,
451-
RoundingInfo[] roundings) {
450+
static int getAppropriateRounding(long minKey, long maxKey, int roundingIdx,
451+
RoundingInfo[] roundings, int targetBuckets) {
452452
if (roundingIdx == roundings.length - 1) {
453453
return roundingIdx;
454454
}
@@ -480,7 +480,7 @@ private int getAppropriateRounding(long minKey, long maxKey, int roundingIdx,
480480
currentKey = currentRounding.nextRoundingValue(currentKey);
481481
}
482482
currentRoundingIdx++;
483-
} while (requiredBuckets > (targetBuckets * roundings[roundingIdx].getMaximumInnerInterval())
483+
} while (requiredBuckets > (targetBuckets * roundings[currentRoundingIdx - 1].getMaximumInnerInterval())
484484
&& currentRoundingIdx < roundings.length);
485485
// The loop will increase past the correct rounding index here so we
486486
// need to subtract one to get the rounding index we need

server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.search.aggregations.bucket.histogram;
2121

2222
import org.elasticsearch.common.io.stream.Writeable;
23+
import org.elasticsearch.common.rounding.DateTimeUnit;
2324
import org.elasticsearch.search.DocValueFormat;
2425
import org.elasticsearch.search.aggregations.InternalAggregations;
2526
import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation;
@@ -28,7 +29,11 @@
2829
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
2930
import org.elasticsearch.test.InternalMultiBucketAggregationTestCase;
3031
import org.joda.time.DateTime;
32+
import org.joda.time.DateTimeZone;
3133

34+
import java.time.Instant;
35+
import java.time.OffsetDateTime;
36+
import java.time.ZoneOffset;
3237
import java.util.ArrayList;
3338
import java.util.Collections;
3439
import java.util.HashMap;
@@ -39,6 +44,8 @@
3944
import static org.elasticsearch.common.unit.TimeValue.timeValueHours;
4045
import static org.elasticsearch.common.unit.TimeValue.timeValueMinutes;
4146
import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds;
47+
import static org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder.createRounding;
48+
import static org.hamcrest.Matchers.equalTo;
4249

4350
public class InternalAutoDateHistogramTests extends InternalMultiBucketAggregationTestCase<InternalAutoDateHistogram> {
4451

@@ -56,11 +63,12 @@ protected InternalAutoDateHistogram createTestInstance(String name,
5663
List<PipelineAggregator> pipelineAggregators,
5764
Map<String, Object> metaData,
5865
InternalAggregations aggregations) {
59-
66+
6067
roundingInfos = AutoDateHistogramAggregationBuilder.buildRoundings(null);
6168
int nbBuckets = randomNumberOfBuckets();
6269
int targetBuckets = randomIntBetween(1, nbBuckets * 2 + 1);
6370
List<InternalAutoDateHistogram.Bucket> buckets = new ArrayList<>(nbBuckets);
71+
6472
long startingDate = System.currentTimeMillis();
6573

6674
long interval = randomIntBetween(1, 3);
@@ -72,23 +80,41 @@ protected InternalAutoDateHistogram createTestInstance(String name,
7280
}
7381
InternalAggregations subAggregations = new InternalAggregations(Collections.emptyList());
7482
BucketInfo bucketInfo = new BucketInfo(roundingInfos, randomIntBetween(0, roundingInfos.length - 1), subAggregations);
83+
return new InternalAutoDateHistogram(name, buckets, targetBuckets, bucketInfo, format, pipelineAggregators, metaData);
84+
}
7585

86+
/*
87+
This test was added to reproduce a bug where getAppropriateRounding was only ever using the first innerIntervals
88+
passed in, instead of using the interval associated with the loop.
89+
*/
90+
public void testGetAppropriateRoundingUsesCorrectIntervals() {
91+
RoundingInfo[] roundings = new RoundingInfo[6];
92+
DateTimeZone timeZone = DateTimeZone.UTC;
93+
// Since we pass 0 as the starting index to getAppropriateRounding, we'll also use
94+
// an innerInterval that is quite large, such that targetBuckets * roundings[i].getMaximumInnerInterval()
95+
// will be larger than the estimate.
96+
roundings[0] = new RoundingInfo(createRounding(DateTimeUnit.SECOND_OF_MINUTE, timeZone),
97+
1000L, 1000);
98+
roundings[1] = new RoundingInfo(createRounding(DateTimeUnit.MINUTES_OF_HOUR, timeZone),
99+
60 * 1000L, 1, 5, 10, 30);
100+
roundings[2] = new RoundingInfo(createRounding(DateTimeUnit.HOUR_OF_DAY, timeZone),
101+
60 * 60 * 1000L, 1, 3, 12);
76102

77-
return new InternalAutoDateHistogram(name, buckets, targetBuckets, bucketInfo, format, pipelineAggregators, metaData);
103+
OffsetDateTime timestamp = Instant.parse("2018-01-01T00:00:01.000Z").atOffset(ZoneOffset.UTC);
104+
// We want to pass a roundingIdx of zero, because in order to reproduce this bug, we need the function
105+
// to increment the rounding (because the bug was that the function would not use the innerIntervals
106+
// from the new rounding.
107+
int result = InternalAutoDateHistogram.getAppropriateRounding(timestamp.toEpochSecond()*1000,
108+
timestamp.plusDays(1).toEpochSecond()*1000, 0, roundings, 25);
109+
assertThat(result, equalTo(2));
78110
}
79111

80112
@Override
81113
protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAutoDateHistogram> inputs) {
82-
int roundingIdx = 0;
83-
for (InternalAutoDateHistogram histogram : inputs) {
84-
if (histogram.getBucketInfo().roundingIdx > roundingIdx) {
85-
roundingIdx = histogram.getBucketInfo().roundingIdx;
86-
}
87-
}
88-
RoundingInfo roundingInfo = roundingInfos[roundingIdx];
89114

90115
long lowest = Long.MAX_VALUE;
91116
long highest = 0;
117+
92118
for (InternalAutoDateHistogram histogram : inputs) {
93119
for (Histogram.Bucket bucket : histogram.getBuckets()) {
94120
long bucketKey = ((DateTime) bucket.getKey()).getMillis();
@@ -100,35 +126,72 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAut
100126
}
101127
}
102128
}
129+
130+
int roundingIndex = reduced.getBucketInfo().roundingIdx;
131+
RoundingInfo roundingInfo = roundingInfos[roundingIndex];
132+
103133
long normalizedDuration = (highest - lowest) / roundingInfo.getRoughEstimateDurationMillis();
104-
long innerIntervalToUse = 0;
105-
for (int interval : roundingInfo.innerIntervals) {
106-
if (normalizedDuration / interval < maxNumberOfBuckets()) {
107-
innerIntervalToUse = interval;
134+
long innerIntervalToUse = roundingInfo.innerIntervals[0];
135+
int innerIntervalIndex = 0;
136+
137+
// First, try to calculate the correct innerInterval using the normalizedDuration.
138+
// This handles cases where highest and lowest are further apart than the interval being used.
139+
if (normalizedDuration != 0) {
140+
for (int j = roundingInfo.innerIntervals.length-1; j >= 0; j--) {
141+
int interval = roundingInfo.innerIntervals[j];
142+
if (normalizedDuration / interval < reduced.getBuckets().size()) {
143+
innerIntervalToUse = interval;
144+
innerIntervalIndex = j;
145+
}
108146
}
109147
}
148+
149+
long intervalInMillis = innerIntervalToUse * roundingInfo.getRoughEstimateDurationMillis();
150+
int bucketCount = getBucketCount(lowest, highest, roundingInfo, intervalInMillis);
151+
152+
//Next, if our bucketCount is still above what we need, we'll go back and determine the interval
153+
// based on a size calculation.
154+
if (bucketCount > reduced.getBuckets().size()) {
155+
for (int i = innerIntervalIndex; i < roundingInfo.innerIntervals.length; i++) {
156+
long newIntervalMillis = roundingInfo.innerIntervals[i] * roundingInfo.getRoughEstimateDurationMillis();
157+
if (getBucketCount(lowest, highest, roundingInfo, newIntervalMillis) <= reduced.getBuckets().size()) {
158+
innerIntervalToUse = roundingInfo.innerIntervals[i];
159+
intervalInMillis = innerIntervalToUse * roundingInfo.getRoughEstimateDurationMillis();
160+
}
161+
}
162+
}
163+
110164
Map<Long, Long> expectedCounts = new TreeMap<>();
111-
long intervalInMillis = innerIntervalToUse*roundingInfo.getRoughEstimateDurationMillis();
112165
for (long keyForBucket = roundingInfo.rounding.round(lowest);
113-
keyForBucket <= highest;
166+
keyForBucket <= roundingInfo.rounding.round(highest);
114167
keyForBucket = keyForBucket + intervalInMillis) {
115168
expectedCounts.put(keyForBucket, 0L);
116169

170+
// Iterate through the input buckets, and for each bucket, determine if it's inside
171+
// the range of the bucket in the outer loop. if it is, add the doc count to the total
172+
// for that bucket.
173+
117174
for (InternalAutoDateHistogram histogram : inputs) {
118175
for (Histogram.Bucket bucket : histogram.getBuckets()) {
119-
long bucketKey = ((DateTime) bucket.getKey()).getMillis();
120-
long roundedBucketKey = roundingInfo.rounding.round(bucketKey);
176+
long roundedBucketKey = roundingInfo.rounding.round(((DateTime) bucket.getKey()).getMillis());
177+
long docCount = bucket.getDocCount();
121178
if (roundedBucketKey >= keyForBucket
122179
&& roundedBucketKey < keyForBucket + intervalInMillis) {
123-
long count = bucket.getDocCount();
124180
expectedCounts.compute(keyForBucket,
125-
(key, oldValue) -> (oldValue == null ? 0 : oldValue) + count);
181+
(key, oldValue) -> (oldValue == null ? 0 : oldValue) + docCount);
126182
}
127183
}
128184
}
129185
}
130186

187+
// If there is only a single bucket, and we haven't added it above, add a bucket with no documents.
188+
// this step is necessary because of the roundedBucketKey < keyForBucket + intervalInMillis above.
189+
if (roundingInfo.rounding.round(lowest) == roundingInfo.rounding.round(highest) && expectedCounts.isEmpty()) {
190+
expectedCounts.put(roundingInfo.rounding.round(lowest), 0L);
191+
}
131192

193+
194+
// pick out the actual reduced values to the make the assertion more readable
132195
Map<Long, Long> actualCounts = new TreeMap<>();
133196
for (Histogram.Bucket bucket : reduced.getBuckets()) {
134197
actualCounts.compute(((DateTime) bucket.getKey()).getMillis(),
@@ -137,6 +200,16 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAut
137200
assertEquals(expectedCounts, actualCounts);
138201
}
139202

203+
private int getBucketCount(long lowest, long highest, RoundingInfo roundingInfo, long intervalInMillis) {
204+
int bucketCount = 0;
205+
for (long keyForBucket = roundingInfo.rounding.round(lowest);
206+
keyForBucket <= roundingInfo.rounding.round(highest);
207+
keyForBucket = keyForBucket + intervalInMillis) {
208+
bucketCount++;
209+
}
210+
return bucketCount;
211+
}
212+
140213
@Override
141214
protected Writeable.Reader<InternalAutoDateHistogram> instanceReader() {
142215
return InternalAutoDateHistogram::new;

0 commit comments

Comments
 (0)