chrisrhyno2003
chrisrhyno2003

Reputation: 4197

Unit testing a method with CompletableFuture.allOf()

I'm trying to test a method that uses CompletableFuture.allOf(). This is my method:

    static CompletableFuture<byte[]> anySuccess(List<CompletableFuture<byte[]>> futures) {
        CompletableFuture<byte[]> delegateFuture = new CompletableFuture<>();

        CompletableFuture.allOf(futures.stream().map(s -> s.thenApply(t -> {
                    if (t == null) {
                        // Here we treat null as a failed lookup.
                        // The inner dummy exception serves as a signal for the caller to return null.
                        throw new RuntimeException();
                    }
                    return t;
                }).thenAccept(delegateFuture::complete)).toArray(CompletableFuture<?>[]::new))
                .exceptionally(ex -> {
                    delegateFuture.completeExceptionally(ex);
                    return null;
                });
        return delegateFuture;
    }

The input set of CompletableFutures can complete/are expected to complete in different times. Here's how my unit test looks

    @Test
    void testAnySuccess() {
        final byte[] payload1 = "payload1".getBytes();
        final byte[] payload2 = "payload2".getBytes();

        List<CompletableFuture<byte[]>> futures = Arrays.asList(
                CompletableFuture.supplyAsync(
                        () -> null
                ),
            CompletableFuture.supplyAsync(
                () -> {
                    LockSupport.parkNanos(Duration.ofSeconds(5).toNanos());
                    System.out.println("Wait for 5 seconds");
                    return payload2;
                }),
                CompletableFuture.supplyAsync(
                        () -> {
                            LockSupport.parkNanos(Duration.ofMillis(100).toNanos());
                            System.out.println("Wait for 100ms");
                            return payload1;
                        })
        );
        long start = System.nanoTime();
        CompletableFuture<byte[]> res = anySuccess(futures);
        byte[] actualRes = res.join();
        Assert.assertEquals(actualRes, payload1);

From what I understand - the allOf() method isn't blocking - but it returns another CompletableFuture, that is complete only when all the futures in it are complete.

Here's my expected outcome - Return the first non null value, or if all of them are null - then return an exception.

I'm confused on whether I need to use the allOf -vs- anyOf method in this particular case. Or do I need to change the case such that I don't use the .exceptionally(...) method, and try and modify it using thenAccept(...)

Upvotes: 3

Views: 121

Answers (1)

Rob Spoor
Rob Spoor

Reputation: 9175

anyOf will not work. It will wait until any future is done, either successfully or exceptionally. That means that it will fail if the quickest future fails.

I think your current code works fine as long as the list is not empty. If it is there will be no call to complete or completeExceptionally, and delegateFuture will never complete. A quick check should solve that.

Once you've ensured the list isn't empty, the result of calling allOf isn't returned but remains active in the background until all futures are done. Any future with a null result will fail, but as soon as one has a non-null value it will complete delegateFuture. Since both complete and completeExceptionally don't do anything if delegateFuture is already completed, there are two options:

  1. At least one of the futures has a non-null value and will complete delegateFuture. All other futures with a non-null value will effectively be ignored. If any of the futures failed due to a null value of another exception then completeExceptionally is called, but this won't do anything since delegateFuture was already completed.

  2. All of the futures fail due to a null value or another exception. The exceptionally block will call completeExceptionally which will be the final result of delegateFuture. The exception you get is determined by the first failure.


There is a better solution coming in a future Java version - StructuredTaskScope, especially using StructuredTaskScope.ShutdownOnSuccess. That will return once the first success is found, and fail if there are no successes. Unlike the solution with allOf it will not continue with any unnecessary work once the first success is found.

Upvotes: 1

Related Questions