Guava Futures.transform with "Real" Executor vs. transformAsync with DirectExecutor

March 2024 ยท 2 minute read

Say I have two heavyweight IO blocking operations, findMatchInSomeDB() and getDetailsFromOtherDB(String objectKey). Furthermore, I want them to run in the background, and use Guava Futures to chain them together because one depends on the result of the other (I know this could just be done sequentially in a simple Callable, but keeping it simple for illustration purposes):

Is there any practical or nuanced difference between the consumeChain and consumeChainAsync methods below?

import com.google.common.base.Function; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; public class Consumer { private final Retriever retriever; private final ListeningExecutorService executorSvc; public Consumer(Retriever retriever, ListeningExecutorService executorSvc) { this.retriever = retriever; this.executorSvc = executorSvc; } private void consumeChain(String searchCriteria) throws Exception { ListenableFuture<String> futureMatch = executorSvc.submit( () -> retriever.findMatchInSomeDB(searchCriteria)); Function<String, DataObject> keyToDataObj = objectKey -> retriever.getDetailsFromOtherDB(objectKey); // using "real" executor service so transform function runs // in the background ListenableFuture<DataObject> futureDataObj = Futures.transform( futureMatch, keyToDataObj, executorSvc); // ...do some other stuff in this thread... // do something with futureDataObj result futureDataObj.get(); } private void consumeChainAsync(String searchCriteria) throws Exception { ListenableFuture<String> futureMatch = executorSvc.submit( () -> retriever.findMatchInSomeDB(searchCriteria)); AsyncFunction<String, DataObject> keyToDataObj = objectKey -> { return executorSvc.submit( () -> retriever.getDetailsFromOtherDB(objectKey)); }; // using DirectExecutor because the transform function // only creates and submits a Callable ListenableFuture<DataObject> futureDataObj = Futures.transformAsync( futureMatch, keyToDataObj, MoreExecutors.directExecutor()); // ...do some other stuff in this thread... // do something with futureDataObj futureDataObj.get(); } } 

As far as I can tell, both will run each heavyweight operation via executorSvc, and both will propagate cancellation and/or execution failure.

It seems that the only point of transformAsync (instead of just using transform with an executor other than DirectExecutor) is for when you're working with an API that returns ListenableFuture instead of directly running the operation. Am I missing something?

1 Answer

It seems that the only point of transformAsync (instead of just using transform with an executor other than DirectExecutor) is for when you're working with an API that returns ListenableFuture instead of directly running the operation.

I think that's the idea.

However, I can think of a small difference that makes transformAsync slightly better: If you call cancel(true) on the output Future, transform currently won't interrupt the thread that runs getDetailsFromOtherDB. In contrast, transformAsync will (by calling cancel(true) on the ListenableFuture returned from the ListeningExecutorService). transform should propagate interruption, but there are some subtleties to getting this right, covered in the link above.

ncG1vNJzZmirpJawrLvVnqmfpJ%2Bse6S7zGiorp2jqbawutJobGxoYW1%2FeIKOoKyarpFis7bA1KucrGWkp66vv8WoqaZlp57BqXnRnpilZZWtsqTB06ipZq6jYsGzrc2snaiqnZbAurrCZq6irJhisaq%2BxJyrnrCVmMI%3D