Skip to content

Commit 5a5ae12

Browse files
committed
Indicate when errors represent timeouts
This commit adds a `timed_out` key to the error responses that represent a timeout condition. It also adds an `X-Timed-Out` header to the response indicating the same outside the response body.
1 parent d387af8 commit 5a5ae12

File tree

5 files changed

+76
-1
lines changed

5 files changed

+76
-1
lines changed

server/src/main/java/org/elasticsearch/ElasticsearchException.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,16 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
108108

109109
private static final String TYPE = "type";
110110
private static final String REASON = "reason";
111+
private static final String TIMED_OUT = "timed_out";
111112
private static final String CAUSED_BY = "caused_by";
112113
private static final ParseField SUPPRESSED = new ParseField("suppressed");
113114
public static final String STACK_TRACE = "stack_trace";
114115
private static final String HEADER = "header";
115116
private static final String ERROR = "error";
116117
private static final String ROOT_CAUSE = "root_cause";
117118

119+
static final String TIMED_OUT_HEADER = "X-Timed-Out";
120+
118121
private static final Map<Integer, CheckedFunction<StreamInput, ? extends ElasticsearchException, IOException>> ID_TO_SUPPLIER;
119122
private static final Map<Class<? extends ElasticsearchException>, ElasticsearchExceptionHandle> CLASS_TO_ELASTICSEARCH_EXCEPTION_HANDLE;
120123
private final Map<String, List<String>> metadata = new HashMap<>();
@@ -123,8 +126,12 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
123126
/**
124127
* Construct a <code>ElasticsearchException</code> with the specified cause exception.
125128
*/
129+
@SuppressWarnings("this-escape")
126130
public ElasticsearchException(Throwable cause) {
127131
super(cause);
132+
if (isTimeout()) {
133+
headers.put(TIMED_OUT_HEADER, List.of("true"));
134+
}
128135
}
129136

130137
/**
@@ -136,8 +143,12 @@ public ElasticsearchException(Throwable cause) {
136143
* @param msg the detail message
137144
* @param args the arguments for the message
138145
*/
146+
@SuppressWarnings("this-escape")
139147
public ElasticsearchException(String msg, Object... args) {
140148
super(LoggerMessageFormat.format(msg, args));
149+
if (isTimeout()) {
150+
headers.put(TIMED_OUT_HEADER, List.of("true"));
151+
}
141152
}
142153

143154
/**
@@ -151,8 +162,12 @@ public ElasticsearchException(String msg, Object... args) {
151162
* @param cause the nested exception
152163
* @param args the arguments for the message
153164
*/
165+
@SuppressWarnings("this-escape")
154166
public ElasticsearchException(String msg, Throwable cause, Object... args) {
155167
super(LoggerMessageFormat.format(msg, args), cause);
168+
if (isTimeout()) {
169+
headers.put(TIMED_OUT_HEADER, List.of("true"));
170+
}
156171
}
157172

158173
@SuppressWarnings("this-escape")
@@ -253,6 +268,13 @@ public RestStatus status() {
253268
}
254269
}
255270

271+
/**
272+
* Returns whether this exception represents a timeout.
273+
*/
274+
public boolean isTimeout() {
275+
return false;
276+
}
277+
256278
/**
257279
* Unwraps the actual cause from the exception for cases when the exception is a
258280
* {@link ElasticsearchWrapperException}.
@@ -386,6 +408,13 @@ protected static void innerToXContent(
386408
builder.field(TYPE, type);
387409
builder.field(REASON, message);
388410

411+
boolean timedOut = false;
412+
if (throwable instanceof ElasticsearchException exception) {
413+
// TODO: we could walk the exception chain to see if _any_ causes are timeouts?
414+
timedOut = exception.isTimeout();
415+
}
416+
builder.field(TIMED_OUT, timedOut);
417+
389418
for (Map.Entry<String, List<String>> entry : metadata.entrySet()) {
390419
headerToXContent(builder, entry.getKey().substring("es.".length()), entry.getValue());
391420
}

server/src/main/java/org/elasticsearch/ElasticsearchTimeoutException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ public RestStatus status() {
4141
// closest thing to "your request took longer than you asked for"
4242
return RestStatus.TOO_MANY_REQUESTS;
4343
}
44+
45+
@Override
46+
public boolean isTimeout() {
47+
return true;
48+
}
4449
}

server/src/main/java/org/elasticsearch/cluster/metadata/ProcessClusterEventTimeoutException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ public ProcessClusterEventTimeoutException(StreamInput in) throws IOException {
3030
public RestStatus status() {
3131
return RestStatus.TOO_MANY_REQUESTS;
3232
}
33+
34+
@Override
35+
public boolean isTimeout() {
36+
return true;
37+
}
3338
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
/**
2020
* Specific instance of {@link SearchException} that indicates that a search timeout occurred.
21-
* Always returns http status 504 (Gateway Timeout)
21+
* Always returns http status 429 (Too Many Requests)
2222
*/
2323
public class SearchTimeoutException extends SearchException {
2424
public SearchTimeoutException(SearchShardTarget shardTarget, String msg) {
@@ -34,6 +34,11 @@ public RestStatus status() {
3434
return RestStatus.TOO_MANY_REQUESTS;
3535
}
3636

37+
@Override
38+
public boolean isTimeout() {
39+
return true;
40+
}
41+
3742
/**
3843
* Propagate a timeout according to whether partial search results are allowed or not.
3944
* In case partial results are allowed, a flag will be set to the provided {@link QuerySearchResult} to indicate that there was a

server/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,4 +1552,35 @@ private void testExceptionLoop(Exception rootException) throws IOException {
15521552
assertThat(ser.getMessage(), equalTo(rootException.getMessage()));
15531553
assertArrayEquals(ser.getStackTrace(), rootException.getStackTrace());
15541554
}
1555+
1556+
static class ExceptionSubclass extends ElasticsearchException {
1557+
@Override
1558+
public boolean isTimeout() {
1559+
return true;
1560+
}
1561+
1562+
ExceptionSubclass(String message) {
1563+
super(message);
1564+
}
1565+
}
1566+
1567+
public void testTimeout() throws IOException {
1568+
var e = new ExceptionSubclass("some timeout");
1569+
assertThat(e.getHeaderKeys(), hasItem(ElasticsearchException.TIMED_OUT_HEADER));
1570+
1571+
XContentBuilder builder = XContentFactory.jsonBuilder();
1572+
builder.startObject();
1573+
e.toXContent(builder, ToXContent.EMPTY_PARAMS);
1574+
builder.endObject();
1575+
String expected = """
1576+
{
1577+
"type": "exception_subclass",
1578+
"reason": "some timeout",
1579+
"timed_out": true,
1580+
"header": {
1581+
"X-Timed-Out": "true"
1582+
}
1583+
}""";
1584+
assertEquals(XContentHelper.stripWhitespace(expected), Strings.toString(builder));
1585+
}
15551586
}

0 commit comments

Comments
 (0)