diff --git a/README.md b/README.md index 7e81462..2b871de 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,27 @@ final ListenableFuture future = getFuture(); FuturesExtra.addFailureCallback(future, e -> e.printStackTrace()); ``` +#### Completed futures + +In some cases you want to extract the value (or exception) from the future and you know that +the future is completed so it won't be a blocking operation. + +You could use these methods for that, but they will also block if the future is not complete which may lead to +hard to find bugs. +```java +T value = future.get(); +T value = Futures.getUnchecked(future); +``` + +Instead you can use these methods which will never block but instead immediately +throw an exception if the future is not completed. This is typically useful in unit tests +(where futures should be immediate) and in general future callbacks/transforms where you know that a +specific future must be completed for this codepath to be triggered. +```java +T value = FuturesExtra.getCompleted(future); +Throwable exc = FuturesExtra.getException(future); +``` + #### JDK 8 CompletableFuture <-> ListenableFuture Conversion * From `ListenableFuture` To JDK 8 `CompletableFuture` diff --git a/src/main/java/com/spotify/futures/FuturesExtra.java b/src/main/java/com/spotify/futures/FuturesExtra.java index 2b502c3..4c74cfa 100644 --- a/src/main/java/com/spotify/futures/FuturesExtra.java +++ b/src/main/java/com/spotify/futures/FuturesExtra.java @@ -22,10 +22,12 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.common.util.concurrent.Uninterruptibles; import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -624,4 +626,45 @@ public static ListenableFuture asyncTransform( ListenableFuture input, AsyncFunction function) { return Futures.transform(input, function); } + + /** + * check that a future is completed. + * @param future the future. + * @throws IllegalStateException if the future is not completed. + */ + public static void checkCompleted(ListenableFuture future) { + if (!future.isDone()) { + throw new IllegalStateException("future was not completed"); + } + } + + /** + * Get the value of a completed future. + * + * @param future a completed future. + * @return the value of the future if it has one. + * @throws IllegalStateException if the future is not completed. + * @throws com.google.common.util.concurrent.UncheckedExecutionException if the future has failed + */ + public static T getCompleted(ListenableFuture future) { + checkCompleted(future); + return Futures.getUnchecked(future); + } + + /** + * Get the exception of a completed future. + * + * @param future a completed future. + * @return the exception of a future or null if no exception was thrown + * @throws IllegalStateException if the future is not completed. + */ + public static Throwable getException(ListenableFuture future) { + checkCompleted(future); + try { + Uninterruptibles.getUninterruptibly(future); + return null; + } catch (ExecutionException e) { + return e.getCause(); + } + } } diff --git a/src/test/java/com/spotify/futures/FuturesExtraTest.java b/src/test/java/com/spotify/futures/FuturesExtraTest.java index 176f71a..f918645 100644 --- a/src/test/java/com/spotify/futures/FuturesExtraTest.java +++ b/src/test/java/com/spotify/futures/FuturesExtraTest.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.common.util.concurrent.UncheckedExecutionException; import com.spotify.futures.FuturesExtra.Consumer; import org.junit.Test; @@ -571,4 +572,46 @@ public ListenableFuture apply(String a, String b, String c, String d, St }); future.get(); } + + @Test + public void testCheckCompleted() throws Exception { + FuturesExtra.checkCompleted(Futures.immediateFuture("hello")); + FuturesExtra.checkCompleted(Futures.immediateFailedFuture(new RuntimeException())); + } + + @Test(expected = IllegalStateException.class) + public void testCheckCompletedFails() throws Exception { + FuturesExtra.checkCompleted(SettableFuture.create()); + } + + @Test + public void testGetCompleted() throws Exception { + assertEquals("hello", FuturesExtra.getCompleted(Futures.immediateFuture("hello"))); + } + + @Test(expected = UncheckedExecutionException.class) + public void testGetCompletedThrows() throws Exception { + FuturesExtra.getCompleted(Futures.immediateFailedFuture(new ArrayIndexOutOfBoundsException())); + } + + @Test(expected = IllegalStateException.class) + public void testGetCompletedNotComplete() throws Exception { + FuturesExtra.getCompleted(SettableFuture.create()); + } + + @Test + public void testGetException() throws Exception { + assertEquals(null, FuturesExtra.getException(Futures.immediateFuture("hello"))); + } + + @Test + public void testGetExceptionThrows() throws Exception { + ArrayIndexOutOfBoundsException t = new ArrayIndexOutOfBoundsException(); + assertEquals(t, FuturesExtra.getException(Futures.immediateFailedFuture(t))); + } + + @Test(expected = IllegalStateException.class) + public void testExceptiondNotComplete() throws Exception { + FuturesExtra.getException(SettableFuture.create()); + } }