-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Client: Wrap synchronous exceptions #28919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
211d31b
d823914
fdabee2
966bdde
17c10f0
888c6bd
b5fa54c
6b3e0b5
990444a
7212e84
fa47ddb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,7 @@ | |
import org.apache.http.message.BasicStatusLine; | ||
import org.apache.http.nio.protocol.HttpAsyncRequestProducer; | ||
import org.apache.http.nio.protocol.HttpAsyncResponseConsumer; | ||
import org.junit.After; | ||
import org.junit.Before; | ||
import org.mockito.invocation.InvocationOnMock; | ||
import org.mockito.stubbing.Answer; | ||
|
@@ -44,6 +45,8 @@ | |
import java.util.Collections; | ||
import java.util.HashSet; | ||
import java.util.Set; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.Future; | ||
|
||
import static org.elasticsearch.client.RestClientTestUtil.randomErrorNoRetryStatusCode; | ||
|
@@ -66,6 +69,7 @@ | |
*/ | ||
public class RestClientMultipleHostsTests extends RestClientTestCase { | ||
|
||
private ExecutorService exec = Executors.newFixedThreadPool(1); | ||
private RestClient restClient; | ||
private HttpHost[] httpHosts; | ||
private HostsTrackingFailureListener failureListener; | ||
|
@@ -79,23 +83,28 @@ public void createRestClient() throws IOException { | |
@Override | ||
public Future<HttpResponse> answer(InvocationOnMock invocationOnMock) throws Throwable { | ||
HttpAsyncRequestProducer requestProducer = (HttpAsyncRequestProducer) invocationOnMock.getArguments()[0]; | ||
HttpUriRequest request = (HttpUriRequest)requestProducer.generateRequest(); | ||
HttpHost httpHost = requestProducer.getTarget(); | ||
final HttpUriRequest request = (HttpUriRequest)requestProducer.generateRequest(); | ||
final HttpHost httpHost = requestProducer.getTarget(); | ||
HttpClientContext context = (HttpClientContext) invocationOnMock.getArguments()[2]; | ||
assertThat(context.getAuthCache().get(httpHost), instanceOf(BasicScheme.class)); | ||
FutureCallback<HttpResponse> futureCallback = (FutureCallback<HttpResponse>) invocationOnMock.getArguments()[3]; | ||
final FutureCallback<HttpResponse> futureCallback = (FutureCallback<HttpResponse>) invocationOnMock.getArguments()[3]; | ||
//return the desired status code or exception depending on the path | ||
if (request.getURI().getPath().equals("/soe")) { | ||
futureCallback.failed(new SocketTimeoutException(httpHost.toString())); | ||
} else if (request.getURI().getPath().equals("/coe")) { | ||
futureCallback.failed(new ConnectTimeoutException(httpHost.toString())); | ||
} else if (request.getURI().getPath().equals("/ioe")) { | ||
futureCallback.failed(new IOException(httpHost.toString())); | ||
} else { | ||
int statusCode = Integer.parseInt(request.getURI().getPath().substring(1)); | ||
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, ""); | ||
futureCallback.completed(new BasicHttpResponse(statusLine)); | ||
} | ||
exec.execute(new Runnable() { | ||
@Override | ||
public void run() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that I can't do that here because these tests are compiled with source compatibility set to 1.7. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The non-test code has to be compiled against 1.7 because we want folks still on 1.7 to be able to use it. The tests kind of come along for the ride mostly to make sure that everything works properly in 1.7. For things like that it is a bit of a pain though. |
||
if (request.getURI().getPath().equals("/soe")) { | ||
futureCallback.failed(new SocketTimeoutException(httpHost.toString())); | ||
} else if (request.getURI().getPath().equals("/coe")) { | ||
futureCallback.failed(new ConnectTimeoutException(httpHost.toString())); | ||
} else if (request.getURI().getPath().equals("/ioe")) { | ||
futureCallback.failed(new IOException(httpHost.toString())); | ||
} else { | ||
int statusCode = Integer.parseInt(request.getURI().getPath().substring(1)); | ||
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, ""); | ||
futureCallback.completed(new BasicHttpResponse(statusLine)); | ||
} | ||
} | ||
}); | ||
return null; | ||
} | ||
}); | ||
|
@@ -108,6 +117,14 @@ public Future<HttpResponse> answer(InvocationOnMock invocationOnMock) throws Thr | |
restClient = new RestClient(httpClient, 10000, new Header[0], httpHosts, null, failureListener); | ||
} | ||
|
||
/** | ||
* Shutdown the executor so we don't leak threads into other test runs. | ||
*/ | ||
@After | ||
public void shutdownExec() { | ||
exec.shutdown(); | ||
} | ||
|
||
public void testRoundRobinOkStatusCodes() throws IOException { | ||
int numIters = RandomNumbers.randomIntBetween(getRandom(), 1, 5); | ||
for (int i = 0; i < numIters; i++) { | ||
|
@@ -163,6 +180,11 @@ public void testRoundRobinRetryErrors() throws IOException { | |
restClient.performRequest(randomHttpMethod(getRandom()), retryEndpoint); | ||
fail("request should have failed"); | ||
} catch(ResponseException e) { | ||
/* | ||
* Unwrap the top level failure that was added so the stack trace contains | ||
* the caller. It wraps the exception that contains the failed hosts. | ||
*/ | ||
e = (ResponseException) e.getCause(); | ||
Set<HttpHost> hostsSet = new HashSet<>(); | ||
Collections.addAll(hostsSet, httpHosts); | ||
//first request causes all the hosts to be blacklisted, the returned exception holds one suppressed exception each | ||
|
@@ -183,6 +205,11 @@ public void testRoundRobinRetryErrors() throws IOException { | |
} while(e != null); | ||
assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size()); | ||
} catch(IOException e) { | ||
/* | ||
* Unwrap the top level failure that was added so the stack trace contains | ||
* the caller. It wraps the exception that contains the failed hosts. | ||
*/ | ||
e = (IOException) e.getCause(); | ||
Set<HttpHost> hostsSet = new HashSet<>(); | ||
Collections.addAll(hostsSet, httpHosts); | ||
//first request causes all the hosts to be blacklisted, the returned exception holds one suppressed exception each | ||
|
@@ -221,6 +248,11 @@ public void testRoundRobinRetryErrors() throws IOException { | |
failureListener.assertCalled(response.getHost()); | ||
assertEquals(0, e.getSuppressed().length); | ||
} catch(IOException e) { | ||
/* | ||
* Unwrap the top level failure that was added so the stack trace contains | ||
* the caller. It wraps the exception that contains the failed hosts. | ||
*/ | ||
e = (IOException) e.getCause(); | ||
HttpHost httpHost = HttpHost.create(e.getMessage()); | ||
assertTrue("host [" + httpHost + "] not found, most likely used multiple times", hostsSet.remove(httpHost)); | ||
//after the first request, all hosts are blacklisted, a single one gets resurrected each time | ||
|
@@ -263,6 +295,11 @@ public void testRoundRobinRetryErrors() throws IOException { | |
assertThat(response.getHost(), equalTo(selectedHost)); | ||
failureListener.assertCalled(selectedHost); | ||
} catch(IOException e) { | ||
/* | ||
* Unwrap the top level failure that was added so the stack trace contains | ||
* the caller. It wraps the exception that contains the failed hosts. | ||
*/ | ||
e = (IOException) e.getCause(); | ||
HttpHost httpHost = HttpHost.create(e.getMessage()); | ||
assertThat(httpHost, equalTo(selectedHost)); | ||
failureListener.assertCalled(selectedHost); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have thought to use reflection here to look for a constructor of the appropriate shape (taking an
Exception
of the same concrete type), falling back toString
and callingThrowable#initCause
if not, and then falling back toRuntimeException
. What do you think of that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about the reflection solution here and rejected it, mostly because we tend to dislike it in the rest of Elasticsearch. I think the trouble with reflection here is that it is difficult to reason about. Not in the "what is going to happen?" sense, but in the "are the ctors that I call going to do the right thing with the things I give them?" sense.
For what it is worth I'd like to abstract async http client from the Elasticsearch client eventually and I don't think the reflection based approach would play well with that.
I certainly understand that this will change the exceptions that we throw in the cases where I don't have an explicit branch though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or is it a way to wrap the exceptions thrown (all or parts) already in the async call into a "CustomException" ? kind of like the
ElasticsearchException
? This might make exception a little bit redundant but it keeps the consistency of the 2 sides of sync and async API. (If the exception parse back stuff in sync listener is too tricky to do) ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the exception unwrapping is going to be tricky to get perfect. I'd be ok with some kind of rethrown exception. I mean, ultimately, that is what we end up with if we fall through all the
instanceof
blocks. We could save ourselves the trouble and wrap the same way every time. I'm honestly not sure what'd be easier for callers though.@javanna, what do you think? You've been working in this area a lot longer than I have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I understand what it all boils down to here is the different IOExceptions that can be thrown by the underlying http client, there may be more than the ones that we have a branch for.
I think the current solution is good enough, I even wonder if anybody is ever going to catch socket timeout rather connect timeout etc. maybe all those could just be generic IOExceptions like we already do below and one has to look at the cause to see what it really is? I think that would be reasonable too, but we do want to rethrow ResponseException as a proper ResponseException as it's our own (like we already do).
I do not have anything against the proposed reflection solution either, but maybe what we have now is easier to reason about and we can always add branches if we missed something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should document what we do with exceptions in the sync methods, so people know what to expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another idea, not sure though how much it makes sense, could be to add our own specific runtime exception (instead of a generic one) with a good name that indicates that it's used only for wrapping, and always use that one, and document well that users have to look at the cause. Then we may want to also remove all the
throws IOException
from theperformRequest
methods. Not sure though, maybe this is the silliest way of addressing this problem, it is hard to evaluate how much this would change things for users, I suspect not much but not sure.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I keep coming back to "I'd like to make async httpclient an implementation detail one day" and I think this kind of
if instanceof then rethrow
is the kind of logic I'm going to need to do that one day. So, I think I'll stick with this approach for entirely selfish reasons. As a side effect, I think it should mostly be transparent to users. Except for branches I haven't caught....++ on adding javadoc for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nik9000 do you think it would be helpful to add another branch for SSLException ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine with me! I can do it.