Skip to content

Commit 4bdf5f8

Browse files
authored
Indicate when errors represent timeouts (#124936) (#125056)
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 86eb3f7 commit 4bdf5f8

File tree

7 files changed

+86
-2
lines changed

7 files changed

+86
-2
lines changed

docs/changelog/124936.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 124936
2+
summary: Indicate when errors represent timeouts
3+
area: Infra/REST API
4+
type: enhancement
5+
issues: []

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

+32
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,10 @@ 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+
maybePutTimeoutHeader();
128133
}
129134

130135
/**
@@ -136,8 +141,10 @@ public ElasticsearchException(Throwable cause) {
136141
* @param msg the detail message
137142
* @param args the arguments for the message
138143
*/
144+
@SuppressWarnings("this-escape")
139145
public ElasticsearchException(String msg, Object... args) {
140146
super(LoggerMessageFormat.format(msg, args));
147+
maybePutTimeoutHeader();
141148
}
142149

143150
/**
@@ -151,8 +158,10 @@ public ElasticsearchException(String msg, Object... args) {
151158
* @param cause the nested exception
152159
* @param args the arguments for the message
153160
*/
161+
@SuppressWarnings("this-escape")
154162
public ElasticsearchException(String msg, Throwable cause, Object... args) {
155163
super(LoggerMessageFormat.format(msg, args), cause);
164+
maybePutTimeoutHeader();
156165
}
157166

158167
@SuppressWarnings("this-escape")
@@ -163,6 +172,13 @@ public ElasticsearchException(StreamInput in) throws IOException {
163172
metadata.putAll(in.readMapOfLists(StreamInput::readString));
164173
}
165174

175+
private void maybePutTimeoutHeader() {
176+
if (isTimeout()) {
177+
// see https://www.rfc-editor.org/rfc/rfc8941.html#section-4.1.9 for booleans in structured headers
178+
headers.put(TIMED_OUT_HEADER, List.of("?1"));
179+
}
180+
}
181+
166182
/**
167183
* Adds a new piece of metadata with the given key.
168184
* If the provided key is already present, the corresponding metadata will be replaced
@@ -253,6 +269,13 @@ public RestStatus status() {
253269
}
254270
}
255271

272+
/**
273+
* Returns whether this exception represents a timeout.
274+
*/
275+
public boolean isTimeout() {
276+
return false;
277+
}
278+
256279
/**
257280
* Unwraps the actual cause from the exception for cases when the exception is a
258281
* {@link ElasticsearchWrapperException}.
@@ -386,6 +409,15 @@ protected static void innerToXContent(
386409
builder.field(TYPE, type);
387410
builder.field(REASON, message);
388411

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

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

+5
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

+5
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

+6-1
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

+31
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": "?1"
1582+
}
1583+
}""";
1584+
assertEquals(XContentHelper.stripWhitespace(expected), Strings.toString(builder));
1585+
}
15551586
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ public void testExceptionRegistration() throws IOException, URISyntaxException {
146146
final Set<? extends Class<?>> ignore = Sets.newHashSet(
147147
CancellableThreadsTests.CustomException.class,
148148
RestResponseTests.WithHeadersException.class,
149-
AbstractClientHeadersTestCase.InternalException.class
149+
AbstractClientHeadersTestCase.InternalException.class,
150+
ElasticsearchExceptionTests.ExceptionSubclass.class
150151
);
151152
FileVisitor<Path> visitor = new FileVisitor<Path>() {
152153
private Path pkgPrefix = PathUtils.get(path).getParent();

0 commit comments

Comments
 (0)