简体   繁体   中英

CompletableFuture.runAsync Swallowing Exceptions

Good morning,

I'm not quite getting the hang of CompletableFutures (I am an experienced developer, but I am not finding them particularly intuitive!).

Given the following snippet:

public CompletionStage<Void> leaveGame(GameService gameService)
{
  return gameService.deregister(playerName)
                    .exceptionally(t -> {
                      LOGGER.info("Could not deregister: {}", t.getMessage());
                      throw new CompletionException(t);
                    });
}

which is called by the unit test:

@Test
public void shouldCompleteExceptionallyForFailedLeave()
{
  var failFlow = new CompletableFuture<Void>();
  failFlow.completeExceptionally(new Exception("TestNonExistentPlayer"));
  when(mockedGameService.deregister(any(String.class))).thenReturn(failFlow);

  try
  {
    player.leaveGame(mockedGameService).toCompletableFuture().get();
    fail("Exception should have been thrown");
  }
  catch (Exception e)
  {
    assertEquals(Exception.class, e.getCause().getClass());
  }
  verify(mockedGameService, times(1)).deregister(any(String.class));
}

which mocks gameService.deregister(...) to completeExceptionally and return Exception .

In the above case, as expected, the exceptionally branch is triggered, the message is logged, and the exception in the unit test is caught, ie the fail(...) assertion is not triggered.

However, when I want to run a CompletionStage before leave game, eg:

public CompletionStage<Void> leaveGame(GameService gameService)
{
  return CompletableFuture.runAsync(() -> System.out.println("BLAH"))
                          .thenRun(() -> gameService.deregister(playerName)
                                                    .exceptionally(t -> {
                                                      LOGGER.info("Could not deregister: {}", t.getMessage());
                                                      throw new CompletionException(t);
                                                    }));
}

The exceptionally branch is still triggered, but the exception is now not caught by the test method, ie the fail(...) assertion is triggered.

What am I doing wrong?

Many thanks in advance!

With your original definition

public CompletionStage<Void> leaveGame(GameService gameService)
{
  return gameService.deregister(playerName)
                    .exceptionally(t -> {
                      LOGGER.info("Could not deregister: {}", t.getMessage());
                      throw new CompletionException(t);
                    });
}

The method leaveGame did not throw an exception but always returned a future. The caller has to examine the future to find out whether the encapsulated operation has failed.

Likewise when you move the same code into a Runnable like

public CompletionStage<Void> leaveGame(GameService gameService)
{
    return CompletableFuture.runAsync(() -> System.out.println("BLAH"))
        .thenRun(() -> gameService.deregister(playerName)
                                  .exceptionally(t -> {
                                    LOGGER.info("Could not deregister: {}", t.getMessage());
                                    throw new CompletionException(t);
                                  }));
}

The Runnable will not throw an exception. It's still required to examine the future returned by gameService.deregister(…).exceptionally(…) to find out whether it failed, but now, you are not returning it but just dropping the reference.

To create a future whose completion depends on a future returned by a function evaluation, you need thenCompose :

public CompletionStage<Void> leaveGame(GameService gameService)
{
    return CompletableFuture.runAsync(() -> System.out.println("BLAH"))
        .thenCompose(voidArg -> gameService.deregister(playerName)
                                  .exceptionally(t -> {
                                    LOGGER.info("Could not deregister: {}", t.getMessage());
                                    throw new CompletionException(t);
                                  }));
}

So now you're implementing a Function<Void,CompletionStage<Void>> rather than Runnable and the stage returned by the function will be use to complete the future returned by leaveGame .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM