Skip to content

feat: Add ApiFuture.completable default method - #14470

Open
eranl wants to merge 2 commits into
googleapis:mainfrom
eranl:ApiFuture-completable
Open

eranl wants to merge 2 commits into
googleapis:mainfrom
eranl:ApiFuture-completable

Conversation

@eranl

@eranl eranl commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

This makes it easier to integrate ApiFuture-based calls into CompletableFuture pipelines.

@eranl
eranl requested review from a team as code owners September 21, 2026 23:20

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a default completable(Executor) method to the ApiFuture interface to allow conversion to a CompletableFuture, along with a corresponding unit test. The feedback suggests several improvements to make the completable implementation more robust, including unwrapping ExecutionException, restoring the interrupted status on InterruptedException, propagating cancellation back to the underlying future, and adding a null check for the executor. Additionally, the unit test should be simplified using lambdas and updated to avoid swallowing exceptions in the asynchronous block.

Comment on lines +44 to +56
default CompletableFuture<V> completable(Executor executor) {
CompletableFuture<V> completableFuture = new CompletableFuture<>();

addListener(() -> {
try {
completableFuture.complete(get());
} catch (Exception e) {
completableFuture.completeExceptionally(e);
}
}, executor);

return completableFuture;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There are a few improvements we can make to the completable method to ensure robustness and correctness:

  1. Exception Unwrapping: When get() throws an ExecutionException, completing the CompletableFuture with the ExecutionException itself leads to double-wrapping (e.g., CompletionException wrapping ExecutionException wrapping the actual cause) when calling join() or other monadic operations on the CompletableFuture. We should unwrap ExecutionException and complete with its cause.
  2. InterruptedException Handling: If get() throws InterruptedException, we should restore the interrupted status of the current thread using Thread.currentThread().interrupt().
  3. Cancellation Propagation: If the returned CompletableFuture is cancelled, the cancellation should propagate back to the underlying ApiFuture to release resources or stop the background work.
  4. Null Check: Ensure executor is not null by using java.util.Objects.requireNonNull.
  default CompletableFuture<V> completable(Executor executor) {
    java.util.Objects.requireNonNull(executor, "executor cannot be null");
    CompletableFuture<V> completableFuture = new CompletableFuture<>();

    addListener(() -> {
      try {
        completableFuture.complete(get());
      } catch (java.util.concurrent.ExecutionException e) {
        completableFuture.completeExceptionally(e.getCause() != null ? e.getCause() : e);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        completableFuture.completeExceptionally(e);
      } catch (Throwable t) {
        completableFuture.completeExceptionally(t);
      }
    }, executor);

    completableFuture.whenComplete((v, t) -> {
      if (completableFuture.isCancelled()) {
        cancel(true);
      }
    });

    return completableFuture;
  }
References
  1. In Java, do not swallow InterruptedException. When catching it, restore the thread's interrupted status by calling Thread.currentThread().interrupt() and handle the interruption appropriately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +100 to +112
new Executor() {
@Override
public void execute(Runnable r) {
r.run();
}
}).handle(
new BiFunction<Integer, Throwable, Object>() {
@Override
public Object apply(Integer integer, Throwable throwable) {
flag.set(1);
return null;
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

We can simplify the test by using Java 8 lambdas and method references instead of verbose anonymous inner classes. Additionally, ensure that we do not swallow exceptions in the asynchronous block so that any failures are allowed to fail the test.

        Runnable::run)
        .handle((integer, throwable) -> {
          if (throwable != null) {
            throw new RuntimeException(throwable);
          }
          flag.set(1);
          return null;
        });
References
  1. In test cases, do not swallow exceptions in asynchronous blocks (such as CompletableFuture). Ensure that any exceptions thrown during async execution are allowed to fail the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I followed the style of the exiting test cases in not using lambdas
  2. I followed the style of the exiting test case in keeping it simple and not testing for impossible exceptions.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant