1

I want to run couple of tasks in parallel via ExecutorService. Each of these task return a string from the same endpoint but with a different result! For example if I execute 4 of these tasks in parallel I can get apple, mangoes, oranges, or a API failure. I am trying to figure out how I can run these tasks in parallel and be able to get the result for the first successful task execution and then I would like to cancel the rest of the tasks in the executor thread pool


    List<Callable<String>> tasks = new ArrayList<>();
    for (String id:  ids){
      tasks.add(() -> hitEndpoint(id));
    }
      List<Future<String>> stringFutureResults = null;
      stringFutureResults= executor.invokeAll(tasks); //execute all in parallel 

      for (Future<String> strFuture : stringFutureResults) {
        String st1 = strFuture.get();
        if (st1 == "apple") {
         // stop execution of all the other tasks or is it too late at this point and all the tasks would have already been executed with the result and status 
        }
    }```    
Sergey Tsypanov
  • 3,265
  • 3
  • 8
  • 34
luckysing_noobster
  • 1,933
  • 5
  • 24
  • 50
  • I'm not sure which api you are calling, but if it was mine I'd be pretty unhappy. This borders on abuse, especially if the list of ids is bigger than ~4. – Jorn Jul 20 '23 at 09:30

2 Answers2

0

You can do this with CompletableFuture:

void foo(List<String> ids) throws Exception {
  List<CompletableFuture<String>> futures = new ArrayList<>();
  for (String id : ids) {
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> hitEndpoint(id), executor);
    futures.add(future);
  }

  String string = anyMatch(futures, str -> str == "apple").get();
}

static CompletableFuture<String> anyMatch(List<? extends CompletionStage<String>> l, Predicate<String> criteria) {

  CompletableFuture<String> result = new CompletableFuture<>();
  Consumer<String> whenMatching = v -> {
    if (criteria.test(v)) result.complete(v);
  };
  CompletableFuture.allOf(l.stream()
                  .map(f -> f.thenAccept(whenMatching)).toArray(CompletableFuture<?>[]::new))
                  .whenComplete((ignored, t) ->
                  result.completeExceptionally(t != null ? t : new NoSuchElementException()));
  return result;
}

The second method is taken from In java, how do I process CompletableFutures and get the first desireable result that completes?

One more solution can be found on Chain CompletableFuture and stop on first success

Sergey Tsypanov
  • 3,265
  • 3
  • 8
  • 34
0

Two factors are important for below algorithm to work efficiently

  • The blocking http call is sensitive to interrupt ,so as soon as it gets one ,it terminates with interruption and throws InterruptedException
  • The shutdownNow from executors will cancel execution of waiting threads,interrupts the executing thread as mentioned in doc

Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. This method does not wait for actively executing tasks to terminate. Use awaitTermination to do that. There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. For example, typical implementations will cancel via Thread.interrupt, so any task that fails to respond to interrupts may never terminate.

We check on interrupt status before making the blocking call so that we don't miss on the already interrupted status

import org.apache.commons.lang3.RandomUtils;
...
 public static void main(String[] args) throws InterruptedException, ExecutionException {

        Predicate<CompletableFuture> isCompletedExceptionally = CompletableFuture::isCompletedExceptionally;
        Predicate<CompletableFuture> isNotCompletedExceptionally = isCompletedExceptionally.negate();
        Stream<String> streamIds = IntStream.rangeClosed(1, 1000).mapToObj(String::valueOf);


        CountDownLatch latch = new CountDownLatch(1);//only one result to succeed
        ExecutorService executor = Executors.newFixedThreadPool(4);
        List<CompletableFuture<String>> stringFutureResults = streamIds
                                                                    .map(id -> CompletableFuture.supplyAsync(() -> hitEndpoint(id, latch), executor))
                                                                    .collect(Collectors.toList());
        latch.await();//waits for atleast one api call to succeed
        executor.shutdownNow();//This will stop accepting new tasks and interrupts already executing tasks

        Optional<CompletableFuture<String>> resultFuture = stringFutureResults.stream()
                .filter(isNotCompletedExceptionally)
                //.filter(CompletableFuture::isDone) // thread executing hitEndpoint never got a chance to return
                .filter(future -> future.join() != null)
                .findFirst();

        System.out.println(resultFuture.get().get());

    }


    private static String hitEndpoint(String id, CountDownLatch latch) {

        try {
            if (latch.getCount()>0 && !Thread.currentThread().isInterrupted()){
                //api call
                Thread.sleep(RandomUtils.nextLong(100,900));//Generally a http call is sensitive to interrupt
                latch.countDown();
                return "Api Response"+id;
            }

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Thread.currentThread().interrupt();//Its response is not required
        return null;
    }
   
Sagar
  • 104
  • 1
  • 5