Skip to content

Commit 4ce95e4

Browse files
authored
Fix InternalAutoDateHistogramTests (#54602)
The test had errors around time units that have different length - think leap years or months that aren't 30 days. This fixes those errors. In the proces I've changed a bunch of things to debug the problem: * Replace `currentTimeMillis` with a random time. Now the test fails randomly! Wonderful. Much better than on random days of the month. * Generate buckets "closer together" to test random reduction. Without this we were super frequently getting stuck in the "year of century" rounding because *some* of the of the buckets we built were far apart. This generates a much greater variety of tests. * Implement `toString` on `RoundingInfo` so I can debug without going crazy. * Switch keys in the bucket assertions from epoch millis to `Instant`s so we can read the failures. Closes #54540 Closes #39497
1 parent ac1b6eb commit 4ce95e4

File tree

2 files changed

+61
-45
lines changed

2 files changed

+61
-45
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -305,5 +305,10 @@ public boolean equals(Object obj) {
305305
&& Objects.equals(dateTimeUnit, other.dateTimeUnit)
306306
;
307307
}
308+
309+
@Override
310+
public String toString() {
311+
return "RoundingInfo[" + rounding + " " + Arrays.toString(innerIntervals) + "]";
312+
}
308313
}
309314
}

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

+56-45
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.elasticsearch.common.Rounding;
2323
import org.elasticsearch.common.io.stream.Writeable;
24+
import org.elasticsearch.common.time.DateFormatter;
2425
import org.elasticsearch.search.DocValueFormat;
2526
import org.elasticsearch.search.aggregations.InternalAggregations;
2627
import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation;
@@ -40,22 +41,24 @@
4041
import java.util.List;
4142
import java.util.Map;
4243
import java.util.TreeMap;
44+
import java.util.concurrent.TimeUnit;
4345

4446
import static java.util.Collections.emptyList;
45-
import static org.elasticsearch.common.unit.TimeValue.timeValueHours;
46-
import static org.elasticsearch.common.unit.TimeValue.timeValueMinutes;
47-
import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds;
4847
import static org.hamcrest.Matchers.equalTo;
4948

5049
public class InternalAutoDateHistogramTests extends InternalMultiBucketAggregationTestCase<InternalAutoDateHistogram> {
5150

5251
private DocValueFormat format;
5352
private RoundingInfo[] roundingInfos;
53+
private long defaultStart;
54+
private int roundingIndex;
5455

5556
@Override
5657
public void setUp() throws Exception {
5758
super.setUp();
5859
format = randomNumericDocValueFormat();
60+
defaultStart = randomLongBetween(0, DateFormatter.forPattern("date_optional_time").parseMillis("2050-01-01"));
61+
roundingIndex = between(0, AutoDateHistogramAggregationBuilder.buildRoundings(null, null).length - 1);
5962
}
6063

6164
@Override
@@ -68,17 +71,20 @@ protected InternalAutoDateHistogram createTestInstance(String name,
6871
int targetBuckets = randomIntBetween(1, nbBuckets * 2 + 1);
6972
List<InternalAutoDateHistogram.Bucket> buckets = new ArrayList<>(nbBuckets);
7073

71-
long startingDate = System.currentTimeMillis();
74+
long startingDate = defaultStart;
75+
if (rarely()) {
76+
startingDate += randomFrom(TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS).toMillis(between(1, 10000));
77+
}
7278

7379
long interval = randomIntBetween(1, 3);
74-
long intervalMillis = randomFrom(timeValueSeconds(interval), timeValueMinutes(interval), timeValueHours(interval)).getMillis();
80+
long intervalMillis = roundingInfos[roundingIndex].roughEstimateDurationMillis * interval;
7581

7682
for (int i = 0; i < nbBuckets; i++) {
7783
long key = startingDate + (intervalMillis * i);
7884
buckets.add(i, new InternalAutoDateHistogram.Bucket(key, randomIntBetween(1, 100), format, aggregations));
7985
}
8086
InternalAggregations subAggregations = new InternalAggregations(Collections.emptyList());
81-
BucketInfo bucketInfo = new BucketInfo(roundingInfos, randomIntBetween(0, roundingInfos.length - 1), subAggregations);
87+
BucketInfo bucketInfo = new BucketInfo(roundingInfos, roundingIndex, subAggregations);
8288
return new InternalAutoDateHistogram(name, buckets, targetBuckets, bucketInfo, format, emptyList(), metadata, 1);
8389
}
8490

@@ -108,11 +114,6 @@ public void testGetAppropriateRoundingUsesCorrectIntervals() {
108114
assertThat(result, equalTo(2));
109115
}
110116

111-
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/54540")
112-
public void testReduceRandom() {
113-
super.testReduceRandom();
114-
}
115-
116117
@Override
117118
protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAutoDateHistogram> inputs) {
118119

@@ -135,41 +136,49 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAut
135136
RoundingInfo roundingInfo = roundingInfos[roundingIndex];
136137

137138
long normalizedDuration = (highest - lowest) / roundingInfo.getRoughEstimateDurationMillis();
138-
long innerIntervalToUse = roundingInfo.innerIntervals[0];
139139
int innerIntervalIndex = 0;
140140

141-
// First, try to calculate the correct innerInterval using the normalizedDuration.
142-
// This handles cases where highest and lowest are further apart than the interval being used.
141+
/*
142+
* Guess the interval to use based on the roughly estimated
143+
* duration. It'll be accurate or it'll produce more buckets
144+
* than we need but it is quick.
145+
*/
143146
if (normalizedDuration != 0) {
144147
for (int j = roundingInfo.innerIntervals.length-1; j >= 0; j--) {
145148
int interval = roundingInfo.innerIntervals[j];
146149
if (normalizedDuration / interval < reduced.getBuckets().size()) {
147-
innerIntervalToUse = interval;
148150
innerIntervalIndex = j;
149151
}
150152
}
151153
}
152154

153-
long intervalInMillis = innerIntervalToUse * roundingInfo.getRoughEstimateDurationMillis();
154-
int bucketCount = getBucketCount(lowest, highest, roundingInfo, intervalInMillis);
155-
156-
//Next, if our bucketCount is still above what we need, we'll go back and determine the interval
157-
// based on a size calculation.
158-
if (bucketCount > reduced.getBuckets().size()) {
159-
for (int i = innerIntervalIndex; i < roundingInfo.innerIntervals.length; i++) {
160-
long newIntervalMillis = roundingInfo.innerIntervals[i] * roundingInfo.getRoughEstimateDurationMillis();
161-
if (getBucketCount(lowest, highest, roundingInfo, newIntervalMillis) <= reduced.getBuckets().size()) {
162-
innerIntervalToUse = roundingInfo.innerIntervals[i];
163-
intervalInMillis = innerIntervalToUse * roundingInfo.getRoughEstimateDurationMillis();
164-
}
155+
/*
156+
* Next pick smaller intervals until we find the one that makes the right
157+
* number of buckets.
158+
*/
159+
int innerIntervalToUse;
160+
do {
161+
innerIntervalToUse = roundingInfo.innerIntervals[innerIntervalIndex];
162+
int bucketCount = getBucketCount(lowest, highest, roundingInfo.rounding, innerIntervalToUse);
163+
if (bucketCount == reduced.getBuckets().size()) {
164+
break;
165165
}
166-
}
167-
168-
Map<Long, Long> expectedCounts = new TreeMap<>();
169-
for (long keyForBucket = roundingInfo.rounding.round(lowest);
170-
keyForBucket <= roundingInfo.rounding.round(highest);
171-
keyForBucket = keyForBucket + intervalInMillis) {
172-
expectedCounts.put(keyForBucket, 0L);
166+
if (bucketCount < reduced.getBuckets().size()) {
167+
innerIntervalToUse = roundingInfo.innerIntervals[Math.max(0, innerIntervalIndex - 1)];
168+
break;
169+
}
170+
} while (++innerIntervalIndex < roundingInfo.innerIntervals.length);
171+
172+
assertThat(reduced.getInterval().toString(), equalTo(innerIntervalToUse + roundingInfo.unitAbbreviation));
173+
Map<Instant, Long> expectedCounts = new TreeMap<>();
174+
long keyForBucket = roundingInfo.rounding.round(lowest);
175+
while (keyForBucket <= roundingInfo.rounding.round(highest)) {
176+
long nextKey = keyForBucket;
177+
for (int i = 0; i < innerIntervalToUse; i++) {
178+
nextKey = roundingInfo.rounding.nextRoundingValue(nextKey);
179+
}
180+
Instant key = Instant.ofEpochMilli(keyForBucket);
181+
expectedCounts.put(key, 0L);
173182

174183
// Iterate through the input buckets, and for each bucket, determine if it's inside
175184
// the range of the bucket in the outer loop. if it is, add the doc count to the total
@@ -179,26 +188,26 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAut
179188
for (Histogram.Bucket bucket : histogram.getBuckets()) {
180189
long roundedBucketKey = roundingInfo.rounding.round(((ZonedDateTime) bucket.getKey()).toInstant().toEpochMilli());
181190
long docCount = bucket.getDocCount();
182-
if (roundedBucketKey >= keyForBucket
183-
&& roundedBucketKey < keyForBucket + intervalInMillis) {
184-
expectedCounts.compute(keyForBucket,
185-
(key, oldValue) -> (oldValue == null ? 0 : oldValue) + docCount);
191+
if (roundedBucketKey >= keyForBucket && roundedBucketKey < nextKey) {
192+
expectedCounts.compute(key,
193+
(k, oldValue) -> (oldValue == null ? 0 : oldValue) + docCount);
186194
}
187195
}
188196
}
197+
keyForBucket = nextKey;
189198
}
190199

191200
// If there is only a single bucket, and we haven't added it above, add a bucket with no documents.
192201
// this step is necessary because of the roundedBucketKey < keyForBucket + intervalInMillis above.
193202
if (roundingInfo.rounding.round(lowest) == roundingInfo.rounding.round(highest) && expectedCounts.isEmpty()) {
194-
expectedCounts.put(roundingInfo.rounding.round(lowest), 0L);
203+
expectedCounts.put(Instant.ofEpochMilli(roundingInfo.rounding.round(lowest)), 0L);
195204
}
196205

197206

198207
// pick out the actual reduced values to the make the assertion more readable
199-
Map<Long, Long> actualCounts = new TreeMap<>();
208+
Map<Instant, Long> actualCounts = new TreeMap<>();
200209
for (Histogram.Bucket bucket : reduced.getBuckets()) {
201-
actualCounts.compute(((ZonedDateTime) bucket.getKey()).toInstant().toEpochMilli(),
210+
actualCounts.compute(((ZonedDateTime) bucket.getKey()).toInstant(),
202211
(key, oldValue) -> (oldValue == null ? 0 : oldValue) + bucket.getDocCount());
203212
}
204213
assertEquals(expectedCounts, actualCounts);
@@ -212,11 +221,13 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List<InternalAut
212221
assertThat(reduced.getInterval(), equalTo(expectedInterval));
213222
}
214223

215-
private int getBucketCount(long lowest, long highest, RoundingInfo roundingInfo, long intervalInMillis) {
224+
private int getBucketCount(long min, long max, Rounding rounding, int interval) {
216225
int bucketCount = 0;
217-
for (long keyForBucket = roundingInfo.rounding.round(lowest);
218-
keyForBucket <= roundingInfo.rounding.round(highest);
219-
keyForBucket = keyForBucket + intervalInMillis) {
226+
long key = rounding.round(min);
227+
while (key < max) {
228+
for (int i = 0; i < interval; i++) {
229+
key = rounding.nextRoundingValue(key);
230+
}
220231
bucketCount++;
221232
}
222233
return bucketCount;

0 commit comments

Comments
 (0)