Skip to content

Commit 9626e70

Browse files
authored
LLRC: Make warning behavior pluggable per request (#36345)
This allows you to plug the behavior that the LLRC uses to handle warnings on a per request basis. We entertained the idea of allowing you to set the warnings behavior to strict mode on a per request basis but that wouldn't allow the high level rest client to fail when it sees an unexpected warning. We also entertained the idea of adding a list of "required warnings" to the `RequestOptions` but that won't work well with failures that occur *sometimes* like those we see in mixed clusters. Adding a list of "allowed warnings" to the `RequestOptions` would work for mixed clusters but it'd leave many of the assertions in our tests weaker than we'd like. This behavior plugging implementation allows us to make a "required warnings" option when we need it and an "allowed warnings" behavior when we need it. I don't think this behavior is going to be commonly used by used outside of the Elasticsearch build, but I expect they'll be a few commendably paranoid folks who could use this behavior.
1 parent f79e602 commit 9626e70

File tree

8 files changed

+222
-24
lines changed

8 files changed

+222
-24
lines changed

client/rest/src/main/java/org/elasticsearch/client/RequestOptions.java

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,31 +24,37 @@
2424
import org.apache.http.nio.protocol.HttpAsyncResponseConsumer;
2525
import org.elasticsearch.client.HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory;
2626

27+
import java.util.ArrayList;
2728
import java.util.Collections;
2829
import java.util.List;
2930
import java.util.Objects;
3031

31-
32-
import java.util.ArrayList;
33-
3432
/**
3533
* The portion of an HTTP request to Elasticsearch that can be
3634
* manipulated without changing Elasticsearch's behavior.
3735
*/
3836
public final class RequestOptions {
37+
/**
38+
* Default request options.
39+
*/
3940
public static final RequestOptions DEFAULT = new Builder(
40-
Collections.<Header>emptyList(), HeapBufferedResponseConsumerFactory.DEFAULT).build();
41+
Collections.<Header>emptyList(), HeapBufferedResponseConsumerFactory.DEFAULT, null).build();
4142

4243
private final List<Header> headers;
4344
private final HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory;
45+
private final WarningsHandler warningsHandler;
4446

4547
private RequestOptions(Builder builder) {
4648
this.headers = Collections.unmodifiableList(new ArrayList<>(builder.headers));
4749
this.httpAsyncResponseConsumerFactory = builder.httpAsyncResponseConsumerFactory;
50+
this.warningsHandler = builder.warningsHandler;
4851
}
4952

53+
/**
54+
* Create a builder that contains these options but can be modified.
55+
*/
5056
public Builder toBuilder() {
51-
return new Builder(headers, httpAsyncResponseConsumerFactory);
57+
return new Builder(headers, httpAsyncResponseConsumerFactory, warningsHandler);
5258
}
5359

5460
/**
@@ -68,12 +74,35 @@ public HttpAsyncResponseConsumerFactory getHttpAsyncResponseConsumerFactory() {
6874
return httpAsyncResponseConsumerFactory;
6975
}
7076

77+
/**
78+
* How this request should handle warnings. If null (the default) then
79+
* this request will default to the behavior dictacted by
80+
* {@link RestClientBuilder#setStrictDeprecationMode}.
81+
* <p>
82+
* This can be set to {@link WarningsHandler#PERMISSIVE} if the client
83+
* should ignore all warnings which is the same behavior as setting
84+
* strictDeprecationMode to true. It can be set to
85+
* {@link WarningsHandler#STRICT} if the client should fail if there are
86+
* any warnings which is the same behavior as settings
87+
* strictDeprecationMode to false.
88+
* <p>
89+
* It can also be set to a custom implementation of
90+
* {@linkplain WarningsHandler} to permit only certain warnings or to
91+
* fail the request if the warnings returned don't
92+
* <strong>exactly</strong> match some set.
93+
*/
94+
public WarningsHandler getWarningsHandler() {
95+
return warningsHandler;
96+
}
97+
7198
@Override
7299
public String toString() {
73100
StringBuilder b = new StringBuilder();
74101
b.append("RequestOptions{");
102+
boolean comma = false;
75103
if (headers.size() > 0) {
76-
b.append(", headers=");
104+
b.append("headers=");
105+
comma = true;
77106
for (int h = 0; h < headers.size(); h++) {
78107
if (h != 0) {
79108
b.append(',');
@@ -82,7 +111,14 @@ public String toString() {
82111
}
83112
}
84113
if (httpAsyncResponseConsumerFactory != HttpAsyncResponseConsumerFactory.DEFAULT) {
85-
b.append(", consumerFactory=").append(httpAsyncResponseConsumerFactory);
114+
if (comma) b.append(", ");
115+
comma = true;
116+
b.append("consumerFactory=").append(httpAsyncResponseConsumerFactory);
117+
}
118+
if (warningsHandler != null) {
119+
if (comma) b.append(", ");
120+
comma = true;
121+
b.append("warningsHandler=").append(warningsHandler);
86122
}
87123
return b.append('}').toString();
88124
}
@@ -98,21 +134,30 @@ public boolean equals(Object obj) {
98134

99135
RequestOptions other = (RequestOptions) obj;
100136
return headers.equals(other.headers)
101-
&& httpAsyncResponseConsumerFactory.equals(other.httpAsyncResponseConsumerFactory);
137+
&& httpAsyncResponseConsumerFactory.equals(other.httpAsyncResponseConsumerFactory)
138+
&& Objects.equals(warningsHandler, other.warningsHandler);
102139
}
103140

104141
@Override
105142
public int hashCode() {
106-
return Objects.hash(headers, httpAsyncResponseConsumerFactory);
143+
return Objects.hash(headers, httpAsyncResponseConsumerFactory, warningsHandler);
107144
}
108145

146+
/**
147+
* Builds {@link RequestOptions}. Get one by calling
148+
* {@link RequestOptions#toBuilder} on {@link RequestOptions#DEFAULT} or
149+
* any other {@linkplain RequestOptions}.
150+
*/
109151
public static class Builder {
110152
private final List<Header> headers;
111153
private HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory;
154+
private WarningsHandler warningsHandler;
112155

113-
private Builder(List<Header> headers, HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory) {
156+
private Builder(List<Header> headers, HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory,
157+
WarningsHandler warningsHandler) {
114158
this.headers = new ArrayList<>(headers);
115159
this.httpAsyncResponseConsumerFactory = httpAsyncResponseConsumerFactory;
160+
this.warningsHandler = warningsHandler;
116161
}
117162

118163
/**
@@ -141,6 +186,27 @@ public void setHttpAsyncResponseConsumerFactory(HttpAsyncResponseConsumerFactory
141186
this.httpAsyncResponseConsumerFactory =
142187
Objects.requireNonNull(httpAsyncResponseConsumerFactory, "httpAsyncResponseConsumerFactory cannot be null");
143188
}
189+
190+
/**
191+
* How this request should handle warnings. If null (the default) then
192+
* this request will default to the behavior dictacted by
193+
* {@link RestClientBuilder#setStrictDeprecationMode}.
194+
* <p>
195+
* This can be set to {@link WarningsHandler#PERMISSIVE} if the client
196+
* should ignore all warnings which is the same behavior as setting
197+
* strictDeprecationMode to true. It can be set to
198+
* {@link WarningsHandler#STRICT} if the client should fail if there are
199+
* any warnings which is the same behavior as settings
200+
* strictDeprecationMode to false.
201+
* <p>
202+
* It can also be set to a custom implementation of
203+
* {@linkplain WarningsHandler} to permit only certain warnings or to
204+
* fail the request if the warnings returned don't
205+
* <strong>exactly</strong> match some set.
206+
*/
207+
public void setWarningsHandler(WarningsHandler warningsHandler) {
208+
this.warningsHandler = warningsHandler;
209+
}
144210
}
145211

146212
/**

client/rest/src/main/java/org/elasticsearch/client/RestClient.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public class RestClient implements Closeable {
110110
private final FailureListener failureListener;
111111
private final NodeSelector nodeSelector;
112112
private volatile NodeTuple<List<Node>> nodeTuple;
113-
private final boolean strictDeprecationMode;
113+
private final WarningsHandler warningsHandler;
114114

115115
RestClient(CloseableHttpAsyncClient client, long maxRetryTimeoutMillis, Header[] defaultHeaders, List<Node> nodes, String pathPrefix,
116116
FailureListener failureListener, NodeSelector nodeSelector, boolean strictDeprecationMode) {
@@ -120,7 +120,7 @@ public class RestClient implements Closeable {
120120
this.failureListener = failureListener;
121121
this.pathPrefix = pathPrefix;
122122
this.nodeSelector = nodeSelector;
123-
this.strictDeprecationMode = strictDeprecationMode;
123+
this.warningsHandler = strictDeprecationMode ? WarningsHandler.STRICT : WarningsHandler.PERMISSIVE;
124124
setNodes(nodes);
125125
}
126126

@@ -275,11 +275,13 @@ void performRequestAsyncNoCatch(Request request, ResponseListener listener) thro
275275
FailureTrackingResponseListener failureTrackingResponseListener = new FailureTrackingResponseListener(listener);
276276
long startTime = System.nanoTime();
277277
performRequestAsync(startTime, nextNode(), httpRequest, ignoreErrorCodes,
278+
request.getOptions().getWarningsHandler() == null ? warningsHandler : request.getOptions().getWarningsHandler(),
278279
request.getOptions().getHttpAsyncResponseConsumerFactory(), failureTrackingResponseListener);
279280
}
280281

281282
private void performRequestAsync(final long startTime, final NodeTuple<Iterator<Node>> nodeTuple, final HttpRequestBase request,
282283
final Set<Integer> ignoreErrorCodes,
284+
final WarningsHandler thisWarningsHandler,
283285
final HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory,
284286
final FailureTrackingResponseListener listener) {
285287
final Node node = nodeTuple.nodes.next();
@@ -298,7 +300,7 @@ public void completed(HttpResponse httpResponse) {
298300
Response response = new Response(request.getRequestLine(), node.getHost(), httpResponse);
299301
if (isSuccessfulResponse(statusCode) || ignoreErrorCodes.contains(response.getStatusLine().getStatusCode())) {
300302
onResponse(node);
301-
if (strictDeprecationMode && response.hasWarnings()) {
303+
if (thisWarningsHandler.warningsShouldFailRequest(response.getWarnings())) {
302304
listener.onDefinitiveFailure(new ResponseException(response));
303305
} else {
304306
listener.onSuccess(response);
@@ -343,7 +345,8 @@ private void retryIfPossible(Exception exception) {
343345
} else {
344346
listener.trackFailure(exception);
345347
request.reset();
346-
performRequestAsync(startTime, nodeTuple, request, ignoreErrorCodes, httpAsyncResponseConsumerFactory, listener);
348+
performRequestAsync(startTime, nodeTuple, request, ignoreErrorCodes,
349+
thisWarningsHandler, httpAsyncResponseConsumerFactory, listener);
347350
}
348351
} else {
349352
listener.onDefinitiveFailure(exception);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client;
21+
22+
import java.util.List;
23+
24+
/**
25+
* Called if there are warnings to determine if those warnings should fail the
26+
* request.
27+
*/
28+
public interface WarningsHandler {
29+
boolean warningsShouldFailRequest(List<String> warnings);
30+
31+
WarningsHandler PERMISSIVE = new WarningsHandler() {
32+
@Override
33+
public boolean warningsShouldFailRequest(List<String> warnings) {
34+
return false;
35+
}
36+
37+
@Override
38+
public String toString() {
39+
return "permissive";
40+
}
41+
};
42+
WarningsHandler STRICT = new WarningsHandler() {
43+
@Override
44+
public boolean warningsShouldFailRequest(List<String> warnings) {
45+
return false == warnings.isEmpty();
46+
}
47+
48+
@Override
49+
public String toString() {
50+
return "strict";
51+
}
52+
};
53+
}

client/rest/src/test/java/org/elasticsearch/client/RequestOptionsTests.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ static RequestOptions.Builder randomBuilder() {
118118
builder.setHttpAsyncResponseConsumerFactory(new HeapBufferedResponseConsumerFactory(1));
119119
}
120120

121+
if (randomBoolean()) {
122+
builder.setWarningsHandler(randomBoolean() ? WarningsHandler.STRICT : WarningsHandler.PERMISSIVE);
123+
}
124+
121125
return builder;
122126
}
123127

@@ -127,14 +131,23 @@ private static RequestOptions copy(RequestOptions options) {
127131

128132
private static RequestOptions mutate(RequestOptions options) {
129133
RequestOptions.Builder mutant = options.toBuilder();
130-
int mutationType = between(0, 1);
134+
int mutationType = between(0, 2);
131135
switch (mutationType) {
132136
case 0:
133137
mutant.addHeader("extra", "m");
134138
return mutant.build();
135139
case 1:
136140
mutant.setHttpAsyncResponseConsumerFactory(new HeapBufferedResponseConsumerFactory(5));
137141
return mutant.build();
142+
case 2:
143+
mutant.setWarningsHandler(new WarningsHandler() {
144+
@Override
145+
public boolean warningsShouldFailRequest(List<String> warnings) {
146+
fail("never called");
147+
return false;
148+
}
149+
});
150+
return mutant.build();
138151
default:
139152
throw new UnsupportedOperationException("Unknown mutation type [" + mutationType + "]");
140153
}

0 commit comments

Comments
 (0)