20

I am trying to call a rest api for PUT request in a loop. Each call is a CompletableFuture. Each api call returns an object of type RoomTypes.RoomType

  • I want to collect the responses (both successful and error responses) in different lists. How do I achieve that? I am sure I cannot use allOf because it would not get all the results if any one call fails to update.

  • How do I log errors/exception for each call?


public void sendRequestsAsync(Map<Integer, List> map1) {
    List<CompletableFuture<Void>> completableFutures = new ArrayList<>(); //List to hold all the completable futures
    List<RoomTypes.RoomType> responses = new ArrayList<>(); //List for responses
    ExecutorService yourOwnExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    for (Map.Entry<Integer, List> entry :map1.entrySet()) { 
        CompletableFuture requestCompletableFuture = CompletableFuture
                .supplyAsync(
                        () -> 
            //API call which returns object of type RoomTypes.RoomType
            updateService.updateRoom(51,33,759,entry.getKey(),
                           new RoomTypes.RoomType(entry.getKey(),map2.get(entry.getKey()),
                                    entry.getValue())),
                    yourOwnExecutor
            )//Supply the task you wanna run, in your case http request
            .thenApply(responses::add);

    completableFutures.add(requestCompletableFuture);
}
Didier L
  • 18,905
  • 10
  • 61
  • 103
Rudrani Angira
  • 956
  • 2
  • 14
  • 28
  • First thing, don't use `thenApply(responses::add)` on a non-thread-safe collection like `ArrayList`, as it will likely break the collection structure. Additionally, `allOf` actually waits for all successes/failures, but the documentation may not be very explicit on this point (I actually tested it myself). – Didier L Jul 02 '18 at 15:58
  • @DidierL What do you mean by "wait"? I just tried it and I can see that the completable future returned by `allOf` calls the next stage method (e.g. `handle`) as soon as one of the completable futures in the collection produces an exception. – Edwin Dalorzo Jul 02 '18 at 16:22
  • @EdwinDalorzo In my test it's not the case: it waits until the last future is completed. Maybe it depends on what you do but that would be surprising. – Didier L Jul 02 '18 at 16:31
  • @DidierL When you say "wait", do you mean the next stage method of the computable future is not invoked until **all futures** are resolved either successfully or not? – Edwin Dalorzo Jul 02 '18 at 17:38
  • @EdwinDalorzo Indeed, yes: the `allOf` stage gets only completed after all futures are completed, even if some failed firs – AFAICT. – Didier L Jul 02 '18 at 18:11
  • @EdwinDalorzo Thought I'd share [my test code](https://ideone.com/OP95RP). Could you also share yours? – Didier L Jul 03 '18 at 10:03
  • @DidierL No need, I did check what you suggested and revised my tests and verified that you were right. I was mistaken. Thanks for sharing your code. It helped clarify my missundestanding. – Edwin Dalorzo Jul 03 '18 at 15:17
  • @EdwinDalorzo Thanks, I am reassured now :) – Didier L Jul 03 '18 at 17:03

3 Answers3

29

You can simply use allOf() to get a future that is completed when all your initial futures are completed (exceptionally or not), and then split them between succeeded and failed using Collectors.partitioningBy():

List<CompletableFuture<RoomTypes.RoomType>> completableFutures = new ArrayList<>(); //List to hold all the completable futures
ExecutorService yourOwnExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

for (Map.Entry<Integer, List> entry : map1.entrySet()) {
    CompletableFuture<RoomTypes.RoomType> requestCompletableFuture = CompletableFuture
            .supplyAsync(
                    () ->
                //API call which returns object of type RoomTypes.RoomType
                updateService.updateRoom(51, 33, 759, entry.getKey(),
                        new RoomTypes.RoomType(entry.getKey(), map2.get(entry.getKey()),
                                entry.getValue())),
                    yourOwnExecutor
            );

    completableFutures.add(requestCompletableFuture);
}

CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]))
        // avoid throwing an exception in the join() call
        .exceptionally(ex -> null)
        .join();
Map<Boolean, List<CompletableFuture<RoomTypes.RoomType>>> result =
        completableFutures.stream()
                .collect(Collectors.partitioningBy(CompletableFuture::isCompletedExceptionally)));

The resulting map will contain one entry with true for the failed futures, and another entry with false key for the succeeded ones. You can then inspect the 2 entries to act accordingly.

Note that there are 2 slight changes compared to your original code:

  • requestCompletableFuture is now a CompletableFuture<RoomTypes.RoomType>
  • thenApply(responses::add) and the responses list were removed

Concerning logging/exception handling, just add the relevant requestCompletableFuture.handle() to log them individually, but keep the requestCompletableFuture and not the one resulting from handle().

Didier L
  • 18,905
  • 10
  • 61
  • 103
  • 2
    I get `Non-static method cant be accessed through static context ` at `CompletableFuture::isCompletedExceptionally` . But my method is not static. – Rudrani Angira Jul 02 '18 at 17:49
  • 3
    I have seen it when they type of `result` does not match exactly what `collect` returns. Try removing the assignment to `result` and use your IDE to extract a local variable from the whole expression again. – Didier L Jul 02 '18 at 18:14
  • Wouldn't the use of "entry" within the for loop be non-thread safe? – TokyoMike Jul 01 '20 at 09:51
  • @TokyoMike it might be indeed, but that's part of the code from the original question which I didn't want to change. I would advise to extract the key and value in the loop instead, and capture those in the lambda (instead of the entry itself) – Didier L Jul 01 '20 at 10:32
11

Alternatively, perhaps you can approach the problem from a different perspective and instead of forcing the use of CompletableFuture, you can use a CompletionService instead.

The whole idea of the CompletionService is that as soon as an answer for a given future is ready, it gets placed in a queue from which you can consume results.

Alternative 1: Without CompletableFuture

CompletionService<String> cs = new ExecutorCompletionService<>(executor);

List<Future<String>> futures = new ArrayList<>();

futures.add(cs.submit(() -> "One"));
futures.add(cs.submit(() -> "Two"));
futures.add(cs.submit(() -> "Three"));
futures.add(cs.submit(() -> { throw new RuntimeException("Sucks to be four"); }));
futures.add(cs.submit(() -> "Five"));


List<String> successes = new ArrayList<>();
List<String> failures = new ArrayList<>();

while (futures.size() > 0) {
    Future<String> f = cs.poll();
    if (f != null) {
        futures.remove(f);
        try {
            //at this point the future is guaranteed to be solved
            //so there won't be any blocking here
            String value = f.get();
            successes.add(value);
        } catch (Exception e) {
            failures.add(e.getMessage());
        }
    }
}

System.out.println(successes); 
System.out.println(failures);

Which yields:

[One, Two, Three, Five]
[java.lang.RuntimeException: Sucks to be four]

Alternative 2: With CompletableFuture

However, if you really, really need to deal with CompletableFuture you can submit those to the completion service as well, just by placing them directly into its queue:

For example, the following variation has the same result:

BlockingQueue<Future<String>> tasks = new ArrayBlockingQueue<>(5);
CompletionService<String> cs = new ExecutorCompletionService<>(executor, tasks);

List<Future<String>> futures = new ArrayList<>();

futures.add(CompletableFuture.supplyAsync(() -> "One"));
futures.add(CompletableFuture.supplyAsync(() -> "Two"));
futures.add(CompletableFuture.supplyAsync(() -> "Three"));
futures.add(CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Sucks to be four"); }));
futures.add(cs.submit(() -> "Five"));

//places all futures in completion service queue
tasks.addAll(futures);

List<String> successes = new ArrayList<>();
List<String> failures = new ArrayList<>();

while (futures.size() > 0) {
    Future<String> f = cs.poll();
    if (f != null) {
        futures.remove(f);
        try {
            //at this point the future is guaranteed to be solved
            //so there won't be any blocking here
            String value = f.get();
            successes.add(value);
        } catch (Exception e) {
            failures.add(e.getMessage());
        }
    }
}
Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205
  • Would not `while(futures.size() > 0)` become infinite when no future has been resolved and poll() will keep scanning for the completed futures if any. Would you recommend using `take()` instead of `poll()` – Rudrani Angira Jul 12 '18 at 03:22
  • 1
    I think it goes without saying that in your question we are discussing futures that are expected to either produce an answer or fail to produce one. How long that takes it is not obvious in your question, but it is understood that you would expect to get all the answers before moving on. Clearly the algorithm above will collect all answers before moving on. The while loop won't be an infinite loop if none of your futures is an infinite loop (which is a given on your question). Whether you use `poll` or `take` won't change the final outcome and I leave it to you to decide. – Edwin Dalorzo Jul 12 '18 at 14:14
  • I'm afraid this works differently then described in the "Alternative 2", the fact that submit was not used, but direct addAll instead, results in poll returning NOT completed futures and subsequently get blocking – Paweł Prażak Oct 21 '21 at 15:01
  • @PawełPrażak Interesting. I did not consider that. I will try to review it later and see if there's a way to fix it, otherwise I'll remove it from the answer. – Edwin Dalorzo Oct 21 '21 at 16:04
  • what I've ended up doing is to remove `ExecutorCompletionService` just use `tasks.poll()` with `if (f.isDone())` and re-adding to `tasks` if not done - not pretty but it works; probably using `peek()` would make the impl cleaner, but I haven't looked at it yet – Paweł Prażak Oct 22 '21 at 10:48
4

For places where you want to use For loop. This is a working solution. CompletableFuture.allOf() ->

You want to download the contents of 100 different web pages of a website. You can do this operation sequentially but this will take a lot of time. So, can write a function that takes a web page link, and returns a CompletableFuture:

CompletableFuture<String> downloadWebPage(String pageLink) {
return CompletableFuture.supplyAsync(() -> {
    // Code to download and return the web page's content
});
} 

Call the previous function in a loop, we are using JAVA 8

List<String> webPageLinks = Arrays.asList(...)  // A list of 100 web page links

// Download contents of all the web pages asynchronously
List<CompletableFuture<String>> pageContentFutures = webPageLinks.stream()
    .map(webPageLink -> downloadWebPage(webPageLink))
    .collect(Collectors.toList());


// Create a combined Future using allOf()
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
    pageContentFutures.toArray(new CompletableFuture[pageContentFutures.size()])
);

The problem with CompletableFuture.allOf() is that it returns CompletableFuture. But we can get the results of all the wrapped CompletableFutures by writing few additional lines of code

// When all the Futures are completed, call `future.join()` to get their results and collect the results in a list -
CompletableFuture<List<String>> allPageContentsFuture = allFutures.thenApply(v -> {
return pageContentFutures.stream()
       .map(pageContentFuture -> pageContentFuture.join())
       .collect(Collectors.toList());
});

Let’s now count the number of web pages that contain our keyword ->

// Count the number of web pages having the "CompletableFuture" keyword.
CompletableFuture<Long> countFuture = allPageContentsFuture.thenApply(pageContents -> {
 return pageContents.stream()
        .filter(pageContent -> pageContent.contains("CompletableFuture"))
        .count();
});

System.out.println("Number of Web Pages having CompletableFuture keyword - " + 
    countFuture.get());
CodingBee
  • 1,011
  • 11
  • 8