2

I have a program that makes HTTP requests, where I might need to try several different servers to get a successful response. The HTTP client is async, so I get CompletableFuture results when making requests.

I can easily try the "next" server when I get a bad response status code, for example:

return httpClient.sendAsync(request,
                HttpResponse.BodyHandlers.ofByteArray()
).thenCompose(response -> {
    if (response.statusCode() == 200) {
        return completedStage(...); // all ok, done!
    }

    // try the next server
    return callThisMethodAgain(...);
);

This works because thenCompose expects the lambda it's given to return another CompletableFuture so I can chain asynchronous computations.

However, if the server is not available, the HTTP client will throw an Exception and my lambda won't execute and the async chain terminates with that Exception.

I can see there are methods like handle and handleAsync, but they don't let me return yet another CompletableFuture, as thenCompose does. They are kind of analogous to thenApply (i.e. like stream's map rather than flatMap), so it seems to me CompletableFuture is missing the method that would be analogous to flatMap where I want to handle both success and failures from the previous async action in the chain?

Is that correct? Is there some workaround I am missing?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Renato
  • 12,940
  • 3
  • 54
  • 85

1 Answers1

0

I have found a work around, which is to write the function myself and throw it into a Utils class on my project (my AsyncUtils keeps growing!).

Here it is, in case it might be useful to anyone else:

public static <T, U> CompletionStage<U> handlingAsync(CompletableFuture<T> future,
                                                      BiFunction<T, Throwable, CompletionStage<U>> handler) {
    var result = new CompletableFuture<U>();
    future.whenComplete((ok, error) -> {
        var handledResult = handler.apply(ok, error);
        handledResult.whenComplete((ok2, err2) -> {
            if (err2 != null) {
                result.completeExceptionally(err2);
            } else {
                result.complete(ok2);
            }
        });
    });
    return result;
}

I wrote a little unit test and it works well for my purposes.

EDIT: thanks to the other linked answers, the above can be much simplified to this:

public static <T, U> CompletionStage<U> handlingAsync(CompletableFuture<T> future,
                                                      BiFunction<T, Throwable, CompletionStage<U>> handler) {
    return future.handle(handler).thenCompose(x -> x);
}
Renato
  • 12,940
  • 3
  • 54
  • 85
  • 1
    You should not name your methods `…Async` when they are not asynchronous but calling the methods without the `Async` suffix. – Holger Dec 21 '21 at 10:28
  • Any method that returns a `CompletionStage` is probably async. What do you mean this method is not `async`? – Renato Dec 21 '21 at 15:17
  • I think you're talking about the distinction the Java API makes between the methods with an `Async` suffix and the ones without (like `handle` VS `handleAsync`) which funnily enough have the same type signature... not like the more general concept of async VS blocking... with would obviously result in different type signatures... can you explain what's the point of that distinction? – Renato Dec 21 '21 at 15:23
  • 1
    When you call `future.handle(handler)`, it is possible that the code of `handler` gets executed right in the current thread. This may happen when `future` is already completed. In contrast, the `handleAsync` method would never evaluate the function in the current thread. So naming your method `handlingAsync` creates the expectation that `handler` gets never evaluated in the caller’s thread but calling `handle(handler)` contradicts this expectation (of course, for `thenCompose(x -> x)` it doesn’t matter). – Holger Dec 21 '21 at 15:43
  • That's a really good explanation, but after doing some research I didn't find this information anywhere in the javadocs. Do you have a good source about this distinction? Also, it seems pretty arbritary to differentiate the two cases as it seems like an implementation detail the caller shouldn't care about. – Renato Dec 21 '21 at 15:52
  • 1
    It’s important when you have a potentially long running or even blocking operation, as then, you want a guaranty that the operation is not executed in the caller thread. Mind that this `…Async(…)` method is just a special case of the `…Async(…, Executor)` method with a default executor. In contrast, the non-async method is the best for a trivial operation like `thenCompose(x -> x)` where the executing thread really doesn’t matter and you likely want the fastest option. [This answer](https://stackoverflow.com/a/46062939/2711488) explains very well what the documentation says and what it doesn’t – Holger Dec 21 '21 at 16:02
  • Thanks... I still find the distinction less than helpful because you're kicking the can down the road by doing that... if anyone wants to block an async call , they will do it regardless and using the `Async` methods will just postpone blocking (or even happen to block the same thread anyway). – Renato Dec 21 '21 at 16:05
  • 1
    You seem to assume that the caller has to be a worker thread of the same (default) executor. This is not necessarily the case. The simplest example would the the event handling thread of whatever UI framework you are using. Of course, this thread must not be blocked and calling the `…Async` method guarantees exactly that. – Holger Dec 21 '21 at 16:07
  • It doesn't guarantee the only thing that matters: that the caller thread (UI in your example) won't block. I didn't assume the same executor, I just considered that a possibility. – Renato Dec 21 '21 at 16:09
  • 1
    I’m not sure whether we’re still talking about the same thing. The `…Async` method *does* guarantee that the caller thread is not blocked. Unlike the non-async method. – Holger Dec 21 '21 at 16:36
  • We are talking about the same thing. I am saying it doesn't guarantee that "the caller Thread won't be blocked, ever", it only guarantees the actual task that may block will not block immediately, and you are saying that's good enough, while I am saying it's not good enough (kicking the can down the road - it might block on your very next async call that yields control). Which is the case (enough or not) is probably dependent on your actual needs. – Renato Dec 21 '21 at 16:50
  • 1
    What is an “async call that yields control”? What you say, doesn’t make any sense. Since *each* `…Async` method guarantees not to block the caller, you can have as many `…Async` you want, the caller will never be blocked. – Holger Dec 21 '21 at 16:54
  • If you don't know what "yields control" is, I need to explain some basics. Hope this gist helps you understand what I am saying: https://gist.github.com/renatoathaydes/7f4a92d9b0514c59994b365289983103 – Renato Dec 21 '21 at 17:11
  • 1
    And what’s the point of this example code? Which of the Async methods do you claim does block the *caller*? – Holger Dec 21 '21 at 17:16
  • The control was "yielded" on the future3 and 4 tasks (in the sense the thread of the program "switched" elsewhere to "await" results). You said "yielding" doesn't make any sense, probably because you are not used to working on other languages that support async natively... notice how BOTH 3 and 4 end up blocked by both 1, then 2. Doesn't matter you used `Async` methods. Guaranteeing not blocking the caller immediately, but blocking the whole pipeline anyway, is just not very useful. Would result in the same at the end in this case if it blocked. – Renato Dec 21 '21 at 17:30
  • 1
    I never said anything about worker threads. If you use an executor with insufficient threads, it’s not the APIs fault. I said that, for example, a UI thread must never get blocked and the Async methods deliver exactly that. Nothing else. You decided to bring in an entirely obsolete term “yields control” to describe an obvious thing. If you explicitly use an executor with an insufficient number of threads, you might end up with an insufficient number of threads. What’s the point of that? Why is that the Async method’s fault? What magic should solve your insufficient setup? – Holger Dec 21 '21 at 17:37