Skip to content

Commit 1c0d339

Browse files
authored
[Rollup] Validate timezone in range queries (#30338)
When validating the search request, we make sure any date_histogram aggregations have timezones that match the jobs. But we didn't do any such validation on range queries. While it wouldn't produce incorrect results, it would be confusing to the user as no documents would match the aggregation (because we add a filter clause on the timezone for the agg). Now the user gets an exception up front, and some helpful text about why the range query didnt match, and which timezones are acceptable
1 parent 21bc87a commit 1c0d339

File tree

3 files changed

+68
-25
lines changed

3 files changed

+68
-25
lines changed

docs/CHANGELOG.asciidoc

+8
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ ones that the user is authorized to access in case field level security is enabl
107107
Fixed prerelease version of elasticsearch in the `deb` package to sort before GA versions
108108
({pull}29000[#29000])
109109

110+
Rollup::
111+
* Validate timezone in range queries to ensure they match the selected job when
112+
searching ({pull}30338[#30338])
113+
110114
[float]
111115
=== Regressions
112116
Fail snapshot operations early when creating or deleting a snapshot on a repository that has been
@@ -167,6 +171,10 @@ Machine Learning::
167171

168172
* Account for gaps in data counts after job is reopened ({pull}30294[#30294])
169173

174+
Rollup::
175+
* Validate timezone in range queries to ensure they match the selected job when
176+
searching ({pull}30338[#30338])
177+
170178
//[float]
171179
//=== Regressions
172180

x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@
5656
import org.elasticsearch.xpack.core.rollup.RollupField;
5757
import org.elasticsearch.xpack.core.rollup.action.RollupJobCaps;
5858
import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction;
59+
import org.elasticsearch.xpack.core.rollup.job.DateHistoGroupConfig;
5960
import org.elasticsearch.xpack.rollup.Rollup;
6061
import org.elasticsearch.xpack.rollup.RollupJobIdentifierUtils;
6162
import org.elasticsearch.xpack.rollup.RollupRequestTranslator;
6263
import org.elasticsearch.xpack.rollup.RollupResponseTranslator;
64+
import org.joda.time.DateTimeZone;
6365

6466
import java.io.IOException;
6567
import java.util.ArrayList;
@@ -277,6 +279,7 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set<RollupJobCaps> jobCap
277279
? ((RangeQueryBuilder)builder).fieldName()
278280
: ((TermQueryBuilder)builder).fieldName();
279281

282+
List<String> incorrectTimeZones = new ArrayList<>();
280283
List<String> rewrittenFieldName = jobCaps.stream()
281284
// We only care about job caps that have the query's target field
282285
.filter(caps -> caps.getFieldCaps().keySet().contains(fieldName))
@@ -286,6 +289,24 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set<RollupJobCaps> jobCap
286289
// For now, we only allow filtering on grouping fields
287290
.filter(agg -> {
288291
String type = (String)agg.get(RollupField.AGG);
292+
293+
// If the cap is for a date_histo, and the query is a range, the timezones need to match
294+
if (type.equals(DateHistogramAggregationBuilder.NAME) && builder instanceof RangeQueryBuilder) {
295+
String timeZone = ((RangeQueryBuilder)builder).timeZone();
296+
297+
// Many range queries don't include the timezone because the default is UTC, but the query
298+
// builder will return null so we need to set it here
299+
if (timeZone == null) {
300+
timeZone = DateTimeZone.UTC.toString();
301+
}
302+
boolean matchingTZ = ((String)agg.get(DateHistoGroupConfig.TIME_ZONE.getPreferredName()))
303+
.equalsIgnoreCase(timeZone);
304+
if (matchingTZ == false) {
305+
incorrectTimeZones.add((String)agg.get(DateHistoGroupConfig.TIME_ZONE.getPreferredName()));
306+
}
307+
return matchingTZ;
308+
}
309+
// Otherwise just make sure it's one of the three groups
289310
return type.equals(TermsAggregationBuilder.NAME)
290311
|| type.equals(DateHistogramAggregationBuilder.NAME)
291312
|| type.equals(HistogramAggregationBuilder.NAME);
@@ -304,8 +325,14 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set<RollupJobCaps> jobCap
304325
.collect(ArrayList::new, List::addAll, List::addAll);
305326

306327
if (rewrittenFieldName.isEmpty()) {
307-
throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName()
328+
if (incorrectTimeZones.isEmpty()) {
329+
throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName()
308330
+ "] query is not available in selected rollup indices, cannot query.");
331+
} else {
332+
throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName()
333+
+ "] query was found in rollup indices, but requested timezone is not compatible. Options include: "
334+
+ incorrectTimeZones);
335+
}
309336
}
310337

311338
if (rewrittenFieldName.size() > 1) {

x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java

+32-24
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,38 @@ public void testRange() {
121121
RollupJobCaps cap = new RollupJobCaps(job.build());
122122
Set<RollupJobCaps> caps = new HashSet<>();
123123
caps.add(cap);
124-
QueryBuilder rewritten = null;
125-
try {
126-
rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1), caps);
127-
} catch (Exception e) {
128-
fail("Should not have thrown exception when parsing query.");
129-
}
124+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("UTC"), caps);
130125
assertThat(rewritten, instanceOf(RangeQueryBuilder.class));
131126
assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp"));
132127
}
133128

129+
public void testRangeNullTimeZone() {
130+
RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo");
131+
GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig();
132+
group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build());
133+
job.setGroupConfig(group.build());
134+
RollupJobCaps cap = new RollupJobCaps(job.build());
135+
Set<RollupJobCaps> caps = new HashSet<>();
136+
caps.add(cap);
137+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1), caps);
138+
assertThat(rewritten, instanceOf(RangeQueryBuilder.class));
139+
assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp"));
140+
}
141+
142+
public void testRangeWrongTZ() {
143+
RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo");
144+
GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig();
145+
group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build());
146+
job.setGroupConfig(group.build());
147+
RollupJobCaps cap = new RollupJobCaps(job.build());
148+
Set<RollupJobCaps> caps = new HashSet<>();
149+
caps.add(cap);
150+
Exception e = expectThrows(IllegalArgumentException.class,
151+
() -> TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("EST"), caps));
152+
assertThat(e.getMessage(), equalTo("Field [foo] in [range] query was found in rollup indices, but requested timezone is not " +
153+
"compatible. Options include: [UTC]"));
154+
}
155+
134156
public void testTerms() {
135157
RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo");
136158
GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig();
@@ -139,12 +161,7 @@ public void testTerms() {
139161
RollupJobCaps cap = new RollupJobCaps(job.build());
140162
Set<RollupJobCaps> caps = new HashSet<>();
141163
caps.add(cap);
142-
QueryBuilder rewritten = null;
143-
try {
144-
rewritten = TransportRollupSearchAction.rewriteQuery(new TermQueryBuilder("foo", "bar"), caps);
145-
} catch (Exception e) {
146-
fail("Should not have thrown exception when parsing query.");
147-
}
164+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new TermQueryBuilder("foo", "bar"), caps);
148165
assertThat(rewritten, instanceOf(TermQueryBuilder.class));
149166
assertThat(((TermQueryBuilder)rewritten).fieldName(), equalTo("foo.terms.value"));
150167
}
@@ -160,12 +177,7 @@ public void testCompounds() {
160177

161178
BoolQueryBuilder builder = new BoolQueryBuilder();
162179
builder.must(getQueryBuilder(2));
163-
QueryBuilder rewritten = null;
164-
try {
165-
rewritten = TransportRollupSearchAction.rewriteQuery(builder, caps);
166-
} catch (Exception e) {
167-
fail("Should not have thrown exception when parsing query.");
168-
}
180+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(builder, caps);
169181
assertThat(rewritten, instanceOf(BoolQueryBuilder.class));
170182
assertThat(((BoolQueryBuilder)rewritten).must().size(), equalTo(1));
171183
}
@@ -178,12 +190,8 @@ public void testMatchAll() {
178190
RollupJobCaps cap = new RollupJobCaps(job.build());
179191
Set<RollupJobCaps> caps = new HashSet<>();
180192
caps.add(cap);
181-
try {
182-
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new MatchAllQueryBuilder(), caps);
183-
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
184-
} catch (Exception e) {
185-
fail("Should not have thrown exception when parsing query.");
186-
}
193+
QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new MatchAllQueryBuilder(), caps);
194+
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
187195
}
188196

189197
public void testAmbiguousResolution() {

0 commit comments

Comments
 (0)