My application is handling a lot of HTTP request/response transactions, where the response from one service leads to the next step and a subsequent request, and so on along a sequence of steps.
To make the code elegant, I'm using a download-and-callback structure which can be simply represented like this:
private void runFirstStep() {
String firstRequest = buildRequest();
sendHttpRequest(firstRequest, this::handleFirstStepResponse);
}
private void handleFirstStepResponse(InputStream responseBody) {
doStuffWithFirstStepResponse(responseBody);
String secondRequest = buildSecondRequest();
sendHttpRequest(secondRequest, this::handleSecondStepResponse);
}
private void handleSecondStepResponse(InputStream responseBody) {
doStuffWithSecondStepResponse(responseBody);
String thirdRequest = buildThirdRequest();
sendHttpRequest(thirdRequest, this::handleThirdStepResponse);
}
private void handleThirdStepResponse(InputStream responseBody) {
doStuffWithThirdStepResponse(responseBody);
// The flow has finished, so no further HTTP transactions.
}
Though in my case the sequence length is currently reaching about 26 steps, all chained in this way.
This is working fine, but I happened to notice logging lines in the console which made it abundantly clear that every method is simply sat waiting for all of the other methods in the chain to finish (which is obvious when I think about it). But it also made me think that this pattern I'm using may be at risk of leading to a stack overflow.
So the questions are:
Will a sequence like this, of potentially a few dozen chained steps, be at risk of stack overflow, or does it require a lot more abuse than this to exhaust the typical stack? Note that my simplified code structure above hides the fact that the methods are actually doing quite a lot (building XML, extracting XML, logging request/response data to a log file) so we're not talking about lightweight tasks.
Is there a different pattern I should be using which will not leave a chain of methods all waiting patiently for the entire flow to finish? My
sendHttpRequest
method is already using the JDK 11 HTTP framework to generate aCompletableFuture<HttpResponse<InputStream>>
, but mysendHttpRequest
method then simply waits for that to complete and calls the specified callback method with the result. Should a new thread be created instead in order to handle theCompletableFuture
, so that the calling method can close down gracefully? And how to do this without causing the JVM to shutdown (seeing as a slow HTTP response will leave the JVM with no methods executing in the meantime)?
Being Stack Overflow (the site, not the exception) I am of course looking for answers which refer to the bare metal mechanics of Java, rather than speculation or anecdote.
Update: just to clarify, my sendHttpRequest
method currently has this sort of shape:
private void sendHttpRequest(String request,
Consumer<InputStream> callback) {
HttpRequest httpRequest = buildHttpRequestFromXml(request);
CompletableFuture<HttpResponse<InputStream>> completableExchange
= httpClient.
sendAsync(httpRequest, BodyHandlers.ofInputStream());
HttpResponse<InputStream> httpResponse = completableExchange.join();
InputStream responseBody = getBodyFromResponse(httpResponse);
callback.accept(responseBody);
}
The important points being that Java's HttpClient.sendAsync
method returns a CompletablFuture
, and then join()
is called on that object to wait for the HTTP response to be received and returned as an HttpResponse
object, which is then used to feed a response body to the specified callback method. But my question is not specifically about HTTP request/response sequences, rather the risks and best practices when dealing with any sort of flow which lends itself to a wait-for-result-and-callback structure.