Skip to content

Commit 1800b27

Browse files
Fix completeWith exception handling (#51734)
ActionListener.completeWith would catch exceptions from listener.onResponse and deliver them to lister.onFailure, essentially double notifying the listener. Instead we now assert that listeners do not throw when using ActionListener.completeWith. Relates #50886
1 parent a6d24d6 commit 1800b27

File tree

2 files changed

+45
-2
lines changed

2 files changed

+45
-2
lines changed

server/src/main/java/org/elasticsearch/action/ActionListener.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,28 @@ protected void innerOnFailure(Exception e) {
315315
/**
316316
* Completes the given listener with the result from the provided supplier accordingly.
317317
* This method is mainly used to complete a listener with a block of synchronous code.
318+
*
319+
* If the supplier fails, the listener's onFailure handler will be called.
320+
* It is the responsibility of {@code delegate} to handle its own exceptions inside `onResponse` and `onFailure`.
318321
*/
319322
static <Response> void completeWith(ActionListener<Response> listener, CheckedSupplier<Response, ? extends Exception> supplier) {
323+
Response response;
320324
try {
321-
listener.onResponse(supplier.get());
325+
response = supplier.get();
322326
} catch (Exception e) {
323-
listener.onFailure(e);
327+
try {
328+
listener.onFailure(e);
329+
} catch (RuntimeException ex) {
330+
assert false : ex;
331+
throw ex;
332+
}
333+
return;
334+
}
335+
try {
336+
listener.onResponse(response);
337+
} catch (RuntimeException ex) {
338+
assert false : ex;
339+
throw ex;
324340
}
325341
}
326342
}

server/src/test/java/org/elasticsearch/action/ActionListenerTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,33 @@ public void testCompleteWith() {
233233
ActionListener.completeWith(onFailureListener, () -> { throw new IOException("not found"); });
234234
assertThat(onFailureListener.isDone(), equalTo(true));
235235
assertThat(expectThrows(ExecutionException.class, onFailureListener::get).getCause(), instanceOf(IOException.class));
236+
237+
AtomicReference<Exception> exReference = new AtomicReference<>();
238+
ActionListener<String> listener = new ActionListener<String>() {
239+
@Override
240+
public void onResponse(String s) {
241+
if (s == null) {
242+
throw new IllegalArgumentException("simulate onResponse exception");
243+
}
244+
}
245+
246+
@Override
247+
public void onFailure(Exception e) {
248+
exReference.set(e);
249+
if (e instanceof IllegalArgumentException) {
250+
throw (IllegalArgumentException) e;
251+
}
252+
}
253+
};
254+
255+
AssertionError assertionError = expectThrows(AssertionError.class, () -> ActionListener.completeWith(listener, () -> null));
256+
assertThat(assertionError.getCause(), instanceOf(IllegalArgumentException.class));
257+
assertNull(exReference.get());
258+
259+
assertionError = expectThrows(AssertionError.class, () -> ActionListener.completeWith(listener,
260+
() -> { throw new IllegalArgumentException(); }));
261+
assertThat(assertionError.getCause(), instanceOf(IllegalArgumentException.class));
262+
assertThat(exReference.get(), instanceOf(IllegalArgumentException.class));
236263
}
237264

238265
/**

0 commit comments

Comments
 (0)