Proper way to adapt an exception with CompletableFuture

霸气de小男生 提交于 2020-01-15 10:09:57

问题


I am working on chaining CompletableFuture to adapt an exception. While I have something that is working, I don't understand why it works.

@Test
public void futureExceptionAdapt() throws ExecutionException, InterruptedException {
    class SillyException extends Exception { }
    class AdaptedException extends Exception { AdaptedException(SillyException silly) { } }

    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        sleepForThreeSeconds();
        if (true)
            throw new CompletionException(new SillyException());
        return 5;
    })

    .thenApplyAsync(val -> val * 10);

    CompletableFuture<Integer> futureAdaptException = future.exceptionally((t) -> {
        if (t instanceof CompletionException && t.getCause() instanceof SillyException) {
            System.out.println("adapt SillyException to AdaptedException");
            SillyException silly = (SillyException) t.getCause();
            future.obtrudeException(new AdaptedException(silly));
        }
        return null;
    });

    try {
        future.get();
        fail("future should have failed with an exception");
    } catch (ExecutionException e) {
        assertTrue("got exception: " + getCauseClass(e),
                   e.getCause() instanceof AdaptedException);
    }

    // I am not sure why the above succeeds
    // because I did not call futureAdaptException.get()
    // According to the IDE futureAdaptException is an unused variable at this point

    assertTrue("expect futureAdaptException to have failed with AdaptedException but instead the result is: " + futureAdaptException.get(),
               futureAdaptException.isCompletedExceptionally());
}

private static void sleepForThreeSeconds() {
    try {
        Thread.sleep(3000L);
    } catch (InterruptedException e) {
    }
}

private static String getCauseClass(Throwable t) {
    if (t.getCause() == null) return "null";
    return t.getCause().getClass().getName();
}

First question is why is futureAdaptException called when I simply call future.get()?

Secondly, is there any way to get futureAdaptException.get() to fail with the desired exception? Because I don't want to create a dummy object if I don't have to. You cannot modify futureAdaptException from the lambda.

Or maybe there is a better way to adapt exceptions. Or maybe there is a variant of exceptionally() that keeps the object in an exception stage (seems the return null causes futureAdaptException to put the object into normal stage with value null). Or maybe we shouldn't be adapting exceptions in CompletableFutures.

    @Test
public void futureStrangeBehaviorFromChaining1() throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("sleep for 3 seconds then return 5");
        sleepForThreeSeconds();
        return 5;
    });

    future.thenApplyAsync(val -> {
        System.out.println("multiply by 3");
        return val*3;
    });

    sleepForFiveSeconds();

    assertEquals(5, future.get().intValue());

    sleepForFiveSeconds();

    assertEquals(5, future.get().intValue());
}

@Test
public void futureStrangeBehaviorFromChaining2() throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("sleep for 3 seconds then return 5");
        sleepForThreeSeconds();
        return 5;
    })

    .thenApplyAsync(val -> {
        System.out.println("multiply by 3");
        return val*3;
    });

    sleepForFiveSeconds();

    assertEquals(15, future.get().intValue());

    sleepForFiveSeconds();

    assertEquals(15, future.get().intValue());
}

回答1:


futureAdaptException is not “called” when you call future.get(). What happens is that you created it with future.exceptionally() so it will automatically be triggered when future completes exceptionally.

So even though futureAdaptException is unused (so you could remove the variable), exceptionally() still has a side effect.

The CompletableFuture that you get from exceptionally() will succeed or fail depending on what you do in the passed-in function. If you want it to fail, you can still throw an exception again:

CompletableFuture<Integer> futureAdaptException = future.exceptionally((t) -> {
    if (t instanceof CompletionException && t.getCause() instanceof SillyException) {
        System.out.println("adapt SillyException to AdaptedException");
        SillyException silly = (SillyException) t.getCause();
        final AdaptedException ex = new AdaptedException(silly);
        future.obtrudeException(ex);
        throw new CompletionException(ex);
    }
    return null;
});

Note that you should probably avoid using obtrudeException() as this is non-deterministic. If fact, I am surprised that your first assert succeeds¹. The CompletableFuture returned by exceptionally() would complete with the same result as the original if it succeeds, so you should work with that one instead.

¹ I definitely think this is due to a bug in the JDK. If you add a sleepForThreeSeconds() in exceptionally(), the test still passes. However, if you add a sleep for more than 3s before future.get() the assert fails and you get the original exception. If you call get() before completion, it appears to wait for the exceptionally() to execute as well. I have posted this question to understand this better.



来源:https://stackoverflow.com/questions/44578803/proper-way-to-adapt-an-exception-with-completablefuture

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!