Skip to content

Commit b1412f6

Browse files
authored
Clean up search timeout handling code (#116678)
TimeExceededException was made public to be able to catch it outside of the search.internal package. That is rather dangerous, because we really need it to be created only from `ContextIndexSearcher#throwTimeExceededException`. This commit makes its constructor private to prevent it from being created outside of ContextIndexSearcher. It also adds javadocs around that. I took the chance to also share the timeout handling code that is now copy pasted in different places.
1 parent a514aad commit b1412f6

File tree

7 files changed

+63
-42
lines changed

7 files changed

+63
-42
lines changed

server/src/main/java/org/elasticsearch/search/fetch/FetchPhase.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,10 @@ protected SearchHit nextDoc(int doc) throws IOException {
195195
context.shardTarget(),
196196
context.searcher().getIndexReader(),
197197
docIdsToLoad,
198-
context.request().allowPartialSearchResults()
198+
context.request().allowPartialSearchResults(),
199+
context.queryResult()
199200
);
200201

201-
if (docsIterator.isTimedOut()) {
202-
context.queryResult().searchTimedOut(true);
203-
}
204-
205202
if (context.isCancelled()) {
206203
for (SearchHit hit : hits) {
207204
// release all hits that would otherwise become owned and eventually released by SearchHits below

server/src/main/java/org/elasticsearch/search/fetch/FetchPhaseDocsIterator.java

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.search.SearchHits;
1717
import org.elasticsearch.search.SearchShardTarget;
1818
import org.elasticsearch.search.internal.ContextIndexSearcher;
19+
import org.elasticsearch.search.query.QuerySearchResult;
1920
import org.elasticsearch.search.query.SearchTimeoutException;
2021

2122
import java.io.IOException;
@@ -30,12 +31,6 @@
3031
*/
3132
abstract class FetchPhaseDocsIterator {
3233

33-
private boolean timedOut = false;
34-
35-
public boolean isTimedOut() {
36-
return timedOut;
37-
}
38-
3934
/**
4035
* Called when a new leaf reader is reached
4136
* @param ctx the leaf reader for this set of doc ids
@@ -53,7 +48,13 @@ public boolean isTimedOut() {
5348
/**
5449
* Iterate over a set of docsIds within a particular shard and index reader
5550
*/
56-
public final SearchHit[] iterate(SearchShardTarget shardTarget, IndexReader indexReader, int[] docIds, boolean allowPartialResults) {
51+
public final SearchHit[] iterate(
52+
SearchShardTarget shardTarget,
53+
IndexReader indexReader,
54+
int[] docIds,
55+
boolean allowPartialResults,
56+
QuerySearchResult querySearchResult
57+
) {
5758
SearchHit[] searchHits = new SearchHit[docIds.length];
5859
DocIdToIndex[] docs = new DocIdToIndex[docIds.length];
5960
for (int index = 0; index < docIds.length; index++) {
@@ -69,12 +70,10 @@ public final SearchHit[] iterate(SearchShardTarget shardTarget, IndexReader inde
6970
int[] docsInLeaf = docIdsInLeaf(0, endReaderIdx, docs, ctx.docBase);
7071
try {
7172
setNextReader(ctx, docsInLeaf);
72-
} catch (ContextIndexSearcher.TimeExceededException timeExceededException) {
73-
if (allowPartialResults) {
74-
timedOut = true;
75-
return SearchHits.EMPTY;
76-
}
77-
throw new SearchTimeoutException(shardTarget, "Time exceeded");
73+
} catch (ContextIndexSearcher.TimeExceededException e) {
74+
SearchTimeoutException.handleTimeout(allowPartialResults, shardTarget, querySearchResult);
75+
assert allowPartialResults;
76+
return SearchHits.EMPTY;
7877
}
7978
for (int i = 0; i < docs.length; i++) {
8079
try {
@@ -88,15 +87,15 @@ public final SearchHit[] iterate(SearchShardTarget shardTarget, IndexReader inde
8887
currentDoc = docs[i].docId;
8988
assert searchHits[docs[i].index] == null;
9089
searchHits[docs[i].index] = nextDoc(docs[i].docId);
91-
} catch (ContextIndexSearcher.TimeExceededException timeExceededException) {
92-
if (allowPartialResults) {
93-
timedOut = true;
94-
SearchHit[] partialSearchHits = new SearchHit[i];
95-
System.arraycopy(searchHits, 0, partialSearchHits, 0, i);
96-
return partialSearchHits;
90+
} catch (ContextIndexSearcher.TimeExceededException e) {
91+
if (allowPartialResults == false) {
92+
purgeSearchHits(searchHits);
9793
}
98-
purgeSearchHits(searchHits);
99-
throw new SearchTimeoutException(shardTarget, "Time exceeded");
94+
SearchTimeoutException.handleTimeout(allowPartialResults, shardTarget, querySearchResult);
95+
assert allowPartialResults;
96+
SearchHit[] partialSearchHits = new SearchHit[i];
97+
System.arraycopy(searchHits, 0, partialSearchHits, 0, i);
98+
return partialSearchHits;
10099
}
101100
}
102101
} catch (SearchTimeoutException e) {

server/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ public void setProfiler(QueryProfiler profiler) {
169169
* Add a {@link Runnable} that will be run on a regular basis while accessing documents in the
170170
* DirectoryReader but also while collecting them and check for query cancellation or timeout.
171171
*/
172-
public Runnable addQueryCancellation(Runnable action) {
173-
return this.cancellable.add(action);
172+
public void addQueryCancellation(Runnable action) {
173+
this.cancellable.add(action);
174174
}
175175

176176
/**
@@ -425,8 +425,16 @@ public void throwTimeExceededException() {
425425
}
426426
}
427427

428-
public static class TimeExceededException extends RuntimeException {
428+
/**
429+
* Exception thrown whenever a search timeout occurs. May be thrown by {@link ContextIndexSearcher} or {@link ExitableDirectoryReader}.
430+
*/
431+
public static final class TimeExceededException extends RuntimeException {
429432
// This exception should never be re-thrown, but we fill in the stacktrace to be able to trace where it does not get properly caught
433+
434+
/**
435+
* Created via {@link #throwTimeExceededException()}
436+
*/
437+
private TimeExceededException() {}
430438
}
431439

432440
@Override
@@ -570,14 +578,12 @@ public DirectoryReader getDirectoryReader() {
570578
}
571579

572580
private static class MutableQueryTimeout implements ExitableDirectoryReader.QueryCancellation {
573-
574581
private final List<Runnable> runnables = new ArrayList<>();
575582

576-
private Runnable add(Runnable action) {
583+
private void add(Runnable action) {
577584
Objects.requireNonNull(action, "cancellation runnable should not be null");
578585
assert runnables.contains(action) == false : "Cancellation runnable already added";
579586
runnables.add(action);
580-
return action;
581587
}
582588

583589
private void remove(Runnable action) {

server/src/main/java/org/elasticsearch/search/query/QueryPhase.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,11 @@ static void addCollectorsAndSearch(SearchContext searchContext) throws QueryPhas
217217
queryResult.topDocs(queryPhaseResult.topDocsAndMaxScore(), queryPhaseResult.sortValueFormats());
218218
if (searcher.timeExceeded()) {
219219
assert timeoutRunnable != null : "TimeExceededException thrown even though timeout wasn't set";
220-
if (searchContext.request().allowPartialSearchResults() == false) {
221-
throw new SearchTimeoutException(searchContext.shardTarget(), "Time exceeded");
222-
}
223-
queryResult.searchTimedOut(true);
220+
SearchTimeoutException.handleTimeout(
221+
searchContext.request().allowPartialSearchResults(),
222+
searchContext.shardTarget(),
223+
searchContext.queryResult()
224+
);
224225
}
225226
if (searchContext.terminateAfter() != SearchContext.DEFAULT_TERMINATE_AFTER) {
226227
queryResult.terminatedEarly(queryPhaseResult.terminatedAfter());

server/src/main/java/org/elasticsearch/search/query/SearchTimeoutException.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,17 @@ public SearchTimeoutException(StreamInput in) throws IOException {
3333
public RestStatus status() {
3434
return RestStatus.GATEWAY_TIMEOUT;
3535
}
36+
37+
/**
38+
* Propagate a timeout according to whether partial search results are allowed or not.
39+
* In case partial results are allowed, a flag will be set to the provided {@link QuerySearchResult} to indicate that there was a
40+
* timeout, but the execution will continue and partial results will be returned to the user.
41+
* When partial results are disallowed, a {@link SearchTimeoutException} will be thrown and returned to the user.
42+
*/
43+
public static void handleTimeout(boolean allowPartialSearchResults, SearchShardTarget target, QuerySearchResult querySearchResult) {
44+
if (allowPartialSearchResults == false) {
45+
throw new SearchTimeoutException(target, "Time exceeded");
46+
}
47+
querySearchResult.searchTimedOut(true);
48+
}
3649
}

server/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ public static void execute(SearchContext context) {
7373
} catch (IOException e) {
7474
throw new ElasticsearchException("Rescore Phase Failed", e);
7575
} catch (ContextIndexSearcher.TimeExceededException e) {
76-
if (context.request().allowPartialSearchResults() == false) {
77-
throw new SearchTimeoutException(context.shardTarget(), "Time exceeded");
78-
}
79-
context.queryResult().searchTimedOut(true);
76+
SearchTimeoutException.handleTimeout(
77+
context.request().allowPartialSearchResults(),
78+
context.shardTarget(),
79+
context.queryResult()
80+
);
8081
}
8182
}
8283

server/src/test/java/org/elasticsearch/search/fetch/FetchPhaseDocsIteratorTests.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.apache.lucene.store.Directory;
1818
import org.apache.lucene.tests.index.RandomIndexWriter;
1919
import org.elasticsearch.search.SearchHit;
20+
import org.elasticsearch.search.query.QuerySearchResult;
2021
import org.elasticsearch.test.ESTestCase;
2122

2223
import java.io.IOException;
@@ -77,7 +78,7 @@ protected SearchHit nextDoc(int doc) {
7778
}
7879
};
7980

80-
SearchHit[] hits = it.iterate(null, reader, docs, randomBoolean());
81+
SearchHit[] hits = it.iterate(null, reader, docs, randomBoolean(), new QuerySearchResult());
8182

8283
assertThat(hits.length, equalTo(docs.length));
8384
for (int i = 0; i < hits.length; i++) {
@@ -125,7 +126,10 @@ protected SearchHit nextDoc(int doc) {
125126
}
126127
};
127128

128-
Exception e = expectThrows(FetchPhaseExecutionException.class, () -> it.iterate(null, reader, docs, randomBoolean()));
129+
Exception e = expectThrows(
130+
FetchPhaseExecutionException.class,
131+
() -> it.iterate(null, reader, docs, randomBoolean(), new QuerySearchResult())
132+
);
129133
assertThat(e.getMessage(), containsString("Error running fetch phase for doc [" + badDoc + "]"));
130134
assertThat(e.getCause(), instanceOf(IllegalArgumentException.class));
131135

0 commit comments

Comments
 (0)