2

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:

  1. 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.

  2. 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 a CompletableFuture<HttpResponse<InputStream>>, but my sendHttpRequest 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 the CompletableFuture, 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.

Bobulous
  • 12,967
  • 4
  • 37
  • 68
  • What HTTP API are you using? – Johnny V Feb 23 '19 at 22:00
  • HTTP requests are instructed using the method `HttpClient.sendAsync(HttpRequest, BodyHandler)` introduced in the new 'java.net.http` package in Java 11. – Bobulous Feb 23 '19 at 22:18
  • Using an ASYNC API you will never get a stack overflow because that thread will return when you call `sendHttpRequest` because that callback is executed in a thread owned or deferred by the HTTP API. If you added a breakpoint or printed the stack at your outer most method you will see that the stack never gets larger from one callback to another. – Johnny V Feb 23 '19 at 22:45
  • But the `HttpClient.sendAsync` method does not take the callback itself, it simply returns a `CompletableFuture`. My code then waits for the result of that `CompletableFuture` within the same thread that called for it, and then passes the result to the callback, all within the same thread. – Bobulous Feb 23 '19 at 22:49
  • 1
    That is more information; have you checked `handleAsync` or `whenCompleteAsync` in the `CompletableFuture` api? – Johnny V Feb 23 '19 at 22:52
  • @JohnnyV, no I did not spot that method within `CompletableFuture`, and that is interesting. – Bobulous Feb 23 '19 at 22:55
  • Using `Future.await()` will eventually give you a stack overflow when using this patten you described. See my previous comment about the async API in `CompletableFuture`. – Johnny V Feb 23 '19 at 22:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/188955/discussion-between-bobulous-and-johnny-v). – Bobulous Feb 24 '19 at 16:54
  • As discussed in the [chat session](https://chat.stackoverflow.com/rooms/188955/discussion-between-bobulous-and-johnny-v), I'm finding that the `CompletableFuture.handleAsync` method is not giving an obvious solution to the problem. – Bobulous Feb 24 '19 at 17:00

1 Answers1

2

First of all, if a method takes callback as parameter it should not block the calling thread. If You are blocking there is no need for callback.(You could just return InputStream from sendHttpRequest and call the next method with it.)

You should go full async using CompletableFuture's. But there is one thing You must consider here. Parallel stream operations and CompletableFuture's uses the common-pool when they are not specifically executed on an Executor(thread pool). Since http download is blocking operation, You should not execute it in common-pool (to not to block common-pool threads doing IO operations). You should create IO pool and pass it to CompletableFuture methods that takes Executor as parameter when You are downloading.

As for what will happen If You continue with the current design;

When a method is called, a stack frame will be created and pushed to calling thread's stack. This frame will hold return address of where this method called, parameters this method takes and local variables of the method. If this parameters and variables are primitive types, they will be stored in the stack, If they are objects, their addresses will be stored in the stack. And when this method is finishes execution its frame will be destroyed.

Chain of 26 method call should not be a problem for stack overflow. Also You can control the stack size with -Xss switch. Default stack size will be different for each platforms(being 32 and 64 bit also effect the default size.) If You are delivering executable command for Your app, You might want to define this value If You have worries about this.

miskender
  • 7,460
  • 1
  • 19
  • 23