3

I have two completionStages method calls that each call a remote service if a condition is not met. They are both pretty long running processes and we need to decrease latency. I also do not care for the secondFuture's response. It could return CompletionStage<Void> as I only care if the method runs before we exit the main method. An added complexity is that injectedClass2.serviceCall also throws a really important exception (404 StatusRuntimeException) that needs to be surfaced to the client.

How do I ensure that the first and second future run asynchronously (not dependant on each other) meanwhile the second future surfaces its error codes and exceptions for the client.

Main Method below is my best attempt at this. It works, but I am looking to learn a better implementation that takes advantage of completables/streams,etc.

try {
      .
      .
      .
      CompletionStage<Response> firstFuture;
      CompletionStage<Response> secondFuture = CompletableFuture.completedFuture(Response.default());
      if (condition) {
        firstFuture = legacyImplThing.resolve(param1, param2);
      } else {
        firstFuture =
            injectedClass1.longRunningOp(param1, param2);
        secondFuture = injectedClass2.serviceCall(param1, param2, someOtherData);
      }

      final CompletionStage<MainMethodResponse> response =
          CompletableFutures.combine(firstFuture, secondFuture, (a, b) -> a)
              .thenApply(
                  v -> ServiceResponse.newBuilder().setParam(v.toString()).build());

      handleResponse(response, responseObserver);
    } catch (Exception e) {
      responseObserver.onError(e);
    }

Maybe out of scope, how would one test/check that two completionStages were run concurrently?

EDIT: CompletableFutures.combine() is a third-party library method and not part of the java.util.concurrent package.

sizzle
  • 2,883
  • 2
  • 13
  • 10

1 Answers1

3

Chaining other stages does not alter the previous stages. In other words, the parallelism is entirely outside your control, as it has been determined already.

More specifically, when you invoke injectedClass1.longRunningOp(param1, param2), the implementation of the method longRunningOp decides, how the returned future will be completed. Likewise, when you call injectedClass2.serviceCall(param1, param2, someOtherData), the implementation of serviceCall will determine the returned future’s completion. Both methods could use the same executor behind the scenes or entirely different approaches.

The only scenario where you can influence the parallelism, is that both methods perform the actual operation in the caller’s thread, to eventually return an already completed future. In this case, you would have to wrap each call into another asynchronous operation to let them run in parallel. But it would be a strange design to return a future when performing a lengthy operation in the caller’s thread.

Your code

CompletableFutures.combine(firstFuture, secondFuture, (a, b) -> a)

does not match the documented API. A valid call would be

firstFuture.thenCombine(secondFuture, (a, b) -> a)

In this case, you are not influencing the completions of firstFuture or secondFuture. You are only specifying what should happen after both futures have been completed.

There is, by the way, no reason to specify a trivial function like (a, b) -> a in thenCombine, just to chain another thenApply. You can use

firstFuture.thenCombine(secondFuture,
    (v, b) -> ServiceResponse.newBuilder().setParam(v.toString()).build())

in the first place.

Holger
  • 285,553
  • 42
  • 434
  • 765