Skip to content

Commit 218bd19

Browse files
Improve FutureUtils.get exception handling (#50339) (#50417)
FutureUtils.get() would unwrap ElasticsearchWrapperExceptions. This is trappy, since nearly all usages of FutureUtils.get() expected only to not have to deal with checked exceptions. In particular, StepListener builds upon ListenableFuture which uses FutureUtils.get to be informed about the exception passed to onFailure. This had the bad consequence of masking away any exception that was an ElasticsearchWrapperException like RemoteTransportException. Specifically for recovery, this made CircuitBreakerExceptions happening on the target node look like they originated from the source node. The only usage that expected that behaviour was AdapterActionFuture. The unwrap behaviour has been moved to that class.
1 parent 3c971f2 commit 218bd19

File tree

5 files changed

+77
-17
lines changed

5 files changed

+77
-17
lines changed

server/src/main/java/org/elasticsearch/action/support/AdapterActionFuture.java

+19-2
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@
1919

2020
package org.elasticsearch.action.support;
2121

22+
import org.elasticsearch.ElasticsearchException;
2223
import org.elasticsearch.action.ActionFuture;
2324
import org.elasticsearch.action.ActionListener;
2425
import org.elasticsearch.common.unit.TimeValue;
2526
import org.elasticsearch.common.util.concurrent.BaseFuture;
2627
import org.elasticsearch.common.util.concurrent.FutureUtils;
28+
import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException;
2729

2830
import java.util.concurrent.TimeUnit;
2931

3032
public abstract class AdapterActionFuture<T, L> extends BaseFuture<T> implements ActionFuture<T>, ActionListener<L> {
3133

3234
@Override
3335
public T actionGet() {
34-
return FutureUtils.get(this);
36+
try {
37+
return FutureUtils.get(this);
38+
} catch (ElasticsearchException e) {
39+
throw unwrapEsException(e);
40+
}
3541
}
3642

3743
@Override
@@ -51,7 +57,11 @@ public T actionGet(TimeValue timeout) {
5157

5258
@Override
5359
public T actionGet(long timeout, TimeUnit unit) {
54-
return FutureUtils.get(this, timeout, unit);
60+
try {
61+
return FutureUtils.get(this, timeout, unit);
62+
} catch (ElasticsearchException e) {
63+
throw unwrapEsException(e);
64+
}
5565
}
5666

5767
@Override
@@ -66,4 +76,11 @@ public void onFailure(Exception e) {
6676

6777
protected abstract T convert(L listenerResponse);
6878

79+
private static RuntimeException unwrapEsException(ElasticsearchException esEx) {
80+
Throwable root = esEx.unwrapCause();
81+
if (root instanceof RuntimeException) {
82+
return (RuntimeException) root;
83+
}
84+
return new UncategorizedExecutionException("Failed execution", root);
85+
}
6986
}

server/src/main/java/org/elasticsearch/cluster/action/index/MappingUpdatedAction.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import org.elasticsearch.common.settings.Setting.Property;
3232
import org.elasticsearch.common.settings.Settings;
3333
import org.elasticsearch.common.unit.TimeValue;
34-
import org.elasticsearch.common.util.concurrent.FutureUtils;
34+
import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException;
3535
import org.elasticsearch.common.xcontent.XContentType;
3636
import org.elasticsearch.index.Index;
3737
import org.elasticsearch.index.mapper.MapperService;
@@ -89,7 +89,16 @@ public void onFailure(Exception e) {
8989
});
9090
}
9191

92+
// todo: this explicit unwrap should not be necessary, but is until guessRootCause is fixed to allow wrapped non-es exception.
9293
private static Exception unwrapException(Exception cause) {
93-
return cause instanceof ElasticsearchException ? FutureUtils.unwrapEsException((ElasticsearchException) cause) : cause;
94+
return cause instanceof ElasticsearchException ? unwrapEsException((ElasticsearchException) cause) : cause;
95+
}
96+
97+
private static RuntimeException unwrapEsException(ElasticsearchException esEx) {
98+
Throwable root = esEx.unwrapCause();
99+
if (root instanceof RuntimeException) {
100+
return (RuntimeException) root;
101+
}
102+
return new UncategorizedExecutionException("Failed execution", root);
94103
}
95104
}

server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java

+1-13
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
package org.elasticsearch.common.util.concurrent;
2121

22-
import org.elasticsearch.ElasticsearchException;
2322
import org.elasticsearch.ElasticsearchTimeoutException;
2423
import org.elasticsearch.common.Nullable;
2524
import org.elasticsearch.common.SuppressForbidden;
@@ -86,21 +85,10 @@ public static <T> T get(Future<T> future, long timeout, TimeUnit unit) {
8685
}
8786

8887
public static RuntimeException rethrowExecutionException(ExecutionException e) {
89-
if (e.getCause() instanceof ElasticsearchException) {
90-
ElasticsearchException esEx = (ElasticsearchException) e.getCause();
91-
return unwrapEsException(esEx);
92-
} else if (e.getCause() instanceof RuntimeException) {
88+
if (e.getCause() instanceof RuntimeException) {
9389
return (RuntimeException) e.getCause();
9490
} else {
9591
return new UncategorizedExecutionException("Failed execution", e);
9692
}
9793
}
98-
99-
public static RuntimeException unwrapEsException(ElasticsearchException esEx) {
100-
Throwable root = esEx.unwrapCause();
101-
if (root instanceof ElasticsearchException || root instanceof RuntimeException) {
102-
return (RuntimeException) root;
103-
}
104-
return new UncategorizedExecutionException("Failed execution", root);
105-
}
10694
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
import org.elasticsearch.test.ESTestCase;
2323
import org.elasticsearch.threadpool.TestThreadPool;
2424
import org.elasticsearch.threadpool.ThreadPool;
25+
import org.elasticsearch.transport.RemoteTransportException;
2526
import org.junit.After;
2627
import org.junit.Before;
2728

2829
import java.util.concurrent.CountDownLatch;
2930
import java.util.concurrent.atomic.AtomicInteger;
31+
import java.util.concurrent.atomic.AtomicReference;
3032
import java.util.function.Consumer;
3133

3234
import static org.hamcrest.Matchers.equalTo;
@@ -110,4 +112,20 @@ private void executeAction(Runnable runnable) {
110112
runnable.run();
111113
}
112114
}
115+
116+
/**
117+
* This test checks that we no longer unwrap exceptions when using StepListener.
118+
*/
119+
public void testNoUnwrap() {
120+
StepListener<String> step = new StepListener<>();
121+
step.onFailure(new RemoteTransportException("test", new RuntimeException("expected")));
122+
AtomicReference<RuntimeException> exception = new AtomicReference<>();
123+
step.whenComplete(null, e -> {
124+
exception.set((RuntimeException) e);
125+
});
126+
127+
assertEquals(RemoteTransportException.class, exception.get().getClass());
128+
RuntimeException e = expectThrows(RuntimeException.class, () -> step.result());
129+
assertEquals(RemoteTransportException.class, e.getClass());
130+
}
113131
}

server/src/test/java/org/elasticsearch/action/support/AdapterActionFutureTests.java

+28
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919

2020
package org.elasticsearch.action.support;
2121

22+
import org.elasticsearch.ElasticsearchException;
2223
import org.elasticsearch.common.unit.TimeValue;
24+
import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException;
2325
import org.elasticsearch.test.ESTestCase;
26+
import org.elasticsearch.transport.RemoteTransportException;
2427

2528
import java.util.Objects;
2629
import java.util.concurrent.BrokenBarrierException;
2730
import java.util.concurrent.CyclicBarrier;
31+
import java.util.concurrent.ExecutionException;
2832
import java.util.concurrent.TimeUnit;
2933
import java.util.concurrent.atomic.AtomicBoolean;
3034

@@ -90,4 +94,28 @@ protected String convert(final Integer listenerResponse) {
9094
thread.join();
9195
}
9296

97+
public void testUnwrapException() {
98+
checkUnwrap(new RemoteTransportException("test", new RuntimeException()), RuntimeException.class, RemoteTransportException.class);
99+
checkUnwrap(new RemoteTransportException("test", new Exception()),
100+
UncategorizedExecutionException.class, RemoteTransportException.class);
101+
checkUnwrap(new Exception(), UncategorizedExecutionException.class, Exception.class);
102+
checkUnwrap(new ElasticsearchException("test", new Exception()), ElasticsearchException.class, ElasticsearchException.class);
103+
}
104+
105+
private void checkUnwrap(Exception exception, Class<? extends Exception> actionGetException,
106+
Class<? extends Exception> getException) {
107+
final AdapterActionFuture<Void, Void> adapter = new AdapterActionFuture<Void, Void>() {
108+
@Override
109+
protected Void convert(Void listenerResponse) {
110+
fail();
111+
return null;
112+
}
113+
};
114+
115+
adapter.onFailure(exception);
116+
assertEquals(actionGetException, expectThrows(RuntimeException.class, adapter::actionGet).getClass());
117+
assertEquals(actionGetException, expectThrows(RuntimeException.class, () -> adapter.actionGet(10, TimeUnit.SECONDS)).getClass());
118+
assertEquals(getException, expectThrows(ExecutionException.class, () -> adapter.get()).getCause().getClass());
119+
assertEquals(getException, expectThrows(ExecutionException.class, () -> adapter.get(10, TimeUnit.SECONDS)).getCause().getClass());
120+
}
93121
}

0 commit comments

Comments
 (0)