1

I'm running into an issue with CompletableFutures. I have a JAX RS-based REST endpoint that reaches out to an API, and I need to make 3 sequential calls to this API. Current flow looks like this:

FruitBasket fruitBasket = RestGateway.GetFruitBasket("today");

Fruit chosenFruit = chooseFruitFromBasket(fruitBasket);

Boolean success = RestGateway.RemoveFromBasket(chosenFruit);

if (success) {
  RestGateway.WhatWasRemoved(chosenFruit.getName());
} else {
  throw RuntimeException("Could not remove fruit from basket.");
}

return chosenFruit

Of course, each of the calls to RestGateway.SomeEndpoint() is blocking because it does not use .async() in building my request.

So now let's add .async() and return a CompletableFuture from each of the RestGateway interactions.

My initial thought is to do this:

Fruit chosenFruit;

RestGateway.GetFruitBasket("today")
  .thenCompose(fruitBasket -> {
    chosenFruit = chooseFruitFromBasket(fruitBasket);
    return RestGateway.RemoveFromBasket(chosenFruit);
  })
  .thenCompose(success -> {
    if(success) {
      RestGateway.WhatWasRemoved(chosenFruit);
    } else {
    throw RuntimeException("Could not remove fruit from basket.");
  });

return chosenFruit;

Because this seems to guarantee me that execution will happen sequentially, and if a previous stage fails then the rest will fail.

Unfortunately, this example is simple and has many less stages than my actual use-case. It feels like I'm writing lots of nested conditionals inside of stacked .thenCompose() blocks. Is there any way to write this in a more sectioned/compartmentalized way?

What I'm looking for is something like the original code:

FruitBasket fruitBasket = RestGateway.GetFruitBasket("today").get();

Fruit chosenFruit = chooseFruitFromBasket(fruitBasket);

Boolean success = RestGateway.RemoveFromBasket(chosenFruit).get();

if (success) {
  RestGateway.WhatWasRemoved(chosenFruit.getName()).get();
} else {
  throw RuntimeException("Could not remove fruit from basket.");
}

return chosenFruit

But the calls to .get() are blocking! So there is absolutely no benefit from the asynchronous re-write of the RestGateway!

TL;DR - Is there any way to preserve the original code flow, while capturing the benefits of asynchronous non-blocking web interactions? The only way I see it is to cascade lots of .thenApply() or .thenCompose methods from the CompletableFuture library, but there must be a better way!

I think this problem is solved by await() in JavaScript, but I don't think Java has anything of that sort.

R. Gosman
  • 31
  • 6
  • 2
    AFAIK You are right. There is no way to keep the non-blocking benefits for a calls like this when calling from a blocking context. In order to leverage these benefits you need to fully go async up to to the outermost part of your application that handles the incoming requests. That is the use case for things like spring reactive (or others), trying to provide a fully async stack from incoming request to comminucation with services or databases. The only use case for async calls in blocking environment I know is parallel execution of multiple independent requests. – Benjamin Eckardt Jun 05 '20 at 20:06
  • 1
    Regarding the `await` in JS: It is just syntactical sugar in the language to hide what is happening underneath. In JS Promises are used which could be seen as similar thing. There you are also passing Promises all the way up. This might not be obvious because of the less declarative (and dynamically typed) nature of the language. – Benjamin Eckardt Jun 05 '20 at 20:16
  • I believe just using more expressive functions instead of those lambdas would also make it look simpler. Like instead of using a lambda, you can create a function called `chooseFruitFromBasket` that does whatever you do in that lambda, but it will be simpler. I also join @BenjaminEckardt's suggestion that futures really are the best when you go fully async. – Mansur Jun 05 '20 at 20:20
  • 1
    Indeed since those calls are sequential, *that* operation in itself won't benefit from `CompletableFuture`. In theory you might start up the `CF`, do some processing and then block to wait for the results when you need them, but in most cases that's not really feasible. Also, `CompletableFuture` is [asynchronous and multithreaded](https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading) whereas `async/await` is single threaded. – Kayaman Jun 05 '20 at 20:21
  • @Kayaman Thanks for clarifying the destinction! – Benjamin Eckardt Jun 05 '20 at 20:24
  • I appreciate all the great feedback guys. I guess I'll just have to work with what I've got for the time being, and plan for a full async migration in the future. @BenjaminEckardt thanks for picking this up so quickly after it was posted! – R. Gosman Jun 05 '20 at 21:05
  • your code is already clean and understandable. Syncronous code like this is always cleaner than asynchronos. The only benefit of async code is less memory consumption. And since the internal code of the RestGateway module is synchronous, adding additional asynchrony has no sense. – Alexei Kaigorodov Jun 06 '20 at 11:35

1 Answers1

0

As the folks in the comments section mentioned, there's not much that can be done.

I will however close this off with a link to another stack that proved tremendously helpful, and solved another problem that I didn't explicitly mention. Namely, how to pass values forward from previous stages while dealing with this warning:

variable used in lambda expression should be final or effectively final.

Using values from previously chained thenCompose lambdas in Java 8

I ended up with something like this:

CompletableFuture<Boolean> stepOne(String input) {
  return RestGateway.GetFruitBasket("today")
      .thenCompose(fruitBasket -> {
        Fruit chosenFruit = chooseFruitFromBasket(fruitBasket);
        return stepTwo(chosenFruit);
      });
}

CompletableFuture<Boolean> stepTwo(Fruit chosenFruit) {
  return RestGateway.RemoveFromBasket(chosenFruit)
      .thenCompose(success -> {
        if (success) {
          return stepThree(chosenFruit.getName());
        } else {
          throw RuntimeException("Could not remove fruit from basket.");
        }
      });
}

CompletableFuture<Boolean> stepThree(String fruitName) {
  return RestGateway.WhatWasRemoved(fruitName);
}

Assuming that RestGateway.WhatWasRemoved() returns a Boolean.

R. Gosman
  • 31
  • 6