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
This commit is contained in:
Henning Andersen 2020-02-03 14:02:31 +01:00 committed by Henning Andersen
parent a6d24d6a46
commit 1800b2730f
2 changed files with 45 additions and 2 deletions

View File

@ -315,12 +315,28 @@ public interface ActionListener<Response> {
/** /**
* Completes the given listener with the result from the provided supplier accordingly. * Completes the given listener with the result from the provided supplier accordingly.
* This method is mainly used to complete a listener with a block of synchronous code. * This method is mainly used to complete a listener with a block of synchronous code.
*
* If the supplier fails, the listener's onFailure handler will be called.
* It is the responsibility of {@code delegate} to handle its own exceptions inside `onResponse` and `onFailure`.
*/ */
static <Response> void completeWith(ActionListener<Response> listener, CheckedSupplier<Response, ? extends Exception> supplier) { static <Response> void completeWith(ActionListener<Response> listener, CheckedSupplier<Response, ? extends Exception> supplier) {
Response response;
try { try {
listener.onResponse(supplier.get()); response = supplier.get();
} catch (Exception e) { } catch (Exception e) {
listener.onFailure(e); try {
listener.onFailure(e);
} catch (RuntimeException ex) {
assert false : ex;
throw ex;
}
return;
}
try {
listener.onResponse(response);
} catch (RuntimeException ex) {
assert false : ex;
throw ex;
} }
} }
} }

View File

@ -233,6 +233,33 @@ public class ActionListenerTests extends ESTestCase {
ActionListener.completeWith(onFailureListener, () -> { throw new IOException("not found"); }); ActionListener.completeWith(onFailureListener, () -> { throw new IOException("not found"); });
assertThat(onFailureListener.isDone(), equalTo(true)); assertThat(onFailureListener.isDone(), equalTo(true));
assertThat(expectThrows(ExecutionException.class, onFailureListener::get).getCause(), instanceOf(IOException.class)); assertThat(expectThrows(ExecutionException.class, onFailureListener::get).getCause(), instanceOf(IOException.class));
AtomicReference<Exception> exReference = new AtomicReference<>();
ActionListener<String> listener = new ActionListener<String>() {
@Override
public void onResponse(String s) {
if (s == null) {
throw new IllegalArgumentException("simulate onResponse exception");
}
}
@Override
public void onFailure(Exception e) {
exReference.set(e);
if (e instanceof IllegalArgumentException) {
throw (IllegalArgumentException) e;
}
}
};
AssertionError assertionError = expectThrows(AssertionError.class, () -> ActionListener.completeWith(listener, () -> null));
assertThat(assertionError.getCause(), instanceOf(IllegalArgumentException.class));
assertNull(exReference.get());
assertionError = expectThrows(AssertionError.class, () -> ActionListener.completeWith(listener,
() -> { throw new IllegalArgumentException(); }));
assertThat(assertionError.getCause(), instanceOf(IllegalArgumentException.class));
assertThat(exReference.get(), instanceOf(IllegalArgumentException.class));
} }
/** /**