-
Notifications
You must be signed in to change notification settings - Fork 38.2k
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
Use only one request timeout mechanism in JdkClientHttpRequest #33090
Conversation
Previously, a timeout was set both on HttpRequest, and used on httpClient.sendAsync().get(). This leads to inconsistent behaviour depending on which timeout gets triggered first. This change changes so that timeout is only set on the HttpRequest.
This was added in |
@snicoll thanks I was not aware this issue existed with HttpClient. But according to JDK-8258397 then the current implementation with additional timeout on CompletableFuture will not help because it fetches an InputStream?
It feels like there are two options; introducing a separate timeout where the input stream is consumed (this would impact other client types too, maybe in a good way of they have similar behavior?), or accepting the behavior that the existing timeout does not account for the body being received. |
Indeed, the comment in JDK-8258397 refers to the That said I'm not sure I follow the original comment about the inconsistency. @cfredri4 can you elaborate? |
My comment about inconsistency was a hypothetical one. I just happened across the code and reacted to that the same timeout was set twice in two different ways. |
A suggestion; use the non-async send without timeout, capture the input stream before returning it, and schedule a separate timer to interrupt the sending thread+cancel input stream on timeout? |
I suppose we don't gain anything from the Future, and only using it as a timer. What scheduling did you have in mind to replace that with? |
I haven't looked into it yet but initial thought is to use the common scheduler used for CompletableFuture timeouts/delayedExecutor. A guess is that it's the same or similar to what's used by HttpClient internally so it should be no overall change in behavior. |
Sure, give that a try. |
After looking some more into this. We don't want to schedule multiple timeouts so that means no timeout on HttpRequest, or on the request future get(), and instead our custom timeout needs to cancel the request future if not already finished. I've pushed an initial draft proposal to my branch. |
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.
Thanks for the changes so far.
I think this is a direction we could work towards. I would like to suggest a little refactoring. Create an inner class, e.g. TimeoutHandler
that manages the timeout future internally, and exposes a method to wrap the response InputStream
. That would keep the executeInternal
method simpler, and separate out the timeout concern, so that eventually when the JDK provides a fix, it's easier to see what code we no longer need.
It would be good to have some tests as well possibly in RestClientIntegrationTests
.
A few more specific comments below.
CompletableFuture<Void> timeoutFuture = new CompletableFuture<Void>() | ||
.completeOnTimeout(null, this.timeout.toMillis(), TimeUnit.MILLISECONDS); | ||
timeoutFuture.thenRun(() -> { | ||
if (!responsefuture.cancel(true) && !responsefuture.isCompletedExceptionally()) { |
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.
Shouldn't this be checking if the task was cancelled successfully, i.e. that cancel()
returns true?
timeoutFuture.thenRun(() -> { | ||
if (!responsefuture.cancel(true) && !responsefuture.isCompletedExceptionally()) { | ||
try { | ||
responsefuture.resultNow().body().close(); |
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.
Can we really be sure there is a body, I imagine the timeout could happen before the server responds with status and headers.
public JdkClientHttpResponse(int statusCode, java.net.http.HttpHeaders headers, InputStream body) { | ||
this.statusCode = statusCode; | ||
this.headers = adaptHeaders(headers); | ||
this.body = body != null ? body : InputStream.nullInputStream(); |
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 we could have constructors like these:
public JdkClientHttpResponse(HttpResponse<InputStream> response) {
this(response, response.getInputStream());
}
public JdkClientHttpResponse(HttpResponse<InputStream> response, InputStream body) {
}
That would keep the logic mostly as it was before and avoid the full qualified java.net.http.HttpHeaders
type.
Cool, I'll sort it out after summer vacation. |
Previously, a timeout was set both on HttpRequest, and used on httpClient.sendAsync().get(). This leads to inconsistent behaviour depending on which timeout gets triggered first. See gh-33090
I've gone ahead and completed this, but if you have a chance, please take a look. |
Previously, a timeout was set both on HttpRequest, and used on httpClient.sendAsync().get(). This leads to inconsistent behaviour depending on which timeout gets triggered first. This change changes so that timeout is only set on the HttpRequest.