2020package org .elasticsearch .search .aggregations .bucket .histogram ;
2121
2222import org .elasticsearch .common .io .stream .Writeable ;
23+ import org .elasticsearch .common .rounding .DateTimeUnit ;
2324import org .elasticsearch .search .DocValueFormat ;
2425import org .elasticsearch .search .aggregations .InternalAggregations ;
2526import org .elasticsearch .search .aggregations .ParsedMultiBucketAggregation ;
2829import org .elasticsearch .search .aggregations .pipeline .PipelineAggregator ;
2930import org .elasticsearch .test .InternalMultiBucketAggregationTestCase ;
3031import 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 ;
3237import java .util .ArrayList ;
3338import java .util .Collections ;
3439import java .util .HashMap ;
3944import static org .elasticsearch .common .unit .TimeValue .timeValueHours ;
4045import static org .elasticsearch .common .unit .TimeValue .timeValueMinutes ;
4146import 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
4350public 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