Skip to content

Commit

Permalink
Merge pull request #21 from zalando/feature/exceptionally-compose
Browse files Browse the repository at this point in the history
Added support for asynchronous fallbacks
  • Loading branch information
whiskeysierra authored Nov 10, 2017
2 parents bd93241 + 409c5c4 commit 26512f5
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 2 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ return tryWith(new BufferedReader(new FileReader(path)), br ->
);
```

### CompletableFuture.exceptionally(..)
### CompletableFuture's *exceptionally*

[`CompletableFuture.exceptionally(..)`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html#exceptionally-java.util.function.Function-)
is a very powerful but often overlooked tool. It allows to inject [partial exception handling](https://stackoverflow.com/questions/37032990/separated-exception-handling-of-a-completablefuture)
Expand Down Expand Up @@ -182,6 +182,15 @@ particular case:
future.exceptionally(partially(NoRouteToHostException.class, this::fallbackValueFor))
```

Other missing pieces in `CompletableFuture`'s API are `exceptionallyCompose` and `handleCompose`. Both can be seen as
a combination of `exceptionally` + `compose` and `handle` + `compose` respectively. They basically allow to supply
another `CompletableFuture` rather than concrete values directly. This is allows for asynchronous fallbacks:

```java
exceptionallyCompose(users.find(name), e -> archive.find(name))
```


## Getting Help

If you have questions, concerns, bug reports, etc., please file an issue in this repository's [Issue Tracker](../../issues).
Expand All @@ -195,4 +204,5 @@ more details, check the [contribution guidelines](CONTRIBUTING.md).

- [Lombok's `@SneakyThrows`](https://projectlombok.org/features/SneakyThrows.html)
- [Durian's Errors](https://github.com/diffplug/durian)
- [Spotify's Completable Futures](https://github.com/spotify/completable-futures)

20 changes: 19 additions & 1 deletion src/main/java/org/zalando/fauxpas/FauxPas.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.zalando.fauxpas;

import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.BiFunction;
import java.util.function.Function;

import static java.util.function.Function.identity;

public final class FauxPas {

FauxPas() {
Expand Down Expand Up @@ -87,4 +90,19 @@ private static Throwable unpack(final Throwable throwable) {
return throwable instanceof CompletionException && cause != null ? cause : throwable;
}

public static <T> CompletableFuture<T> handleCompose(final CompletableFuture<T> future,
final BiFunction<T, Throwable, CompletableFuture<T>> function) {
return future
.handle(function)
.thenCompose(identity());
}

public static <T> CompletableFuture<T> exceptionallyCompose(final CompletableFuture<T> future,
final Function<Throwable, CompletableFuture<T>> function) {
return future
.thenApply(CompletableFuture::completedFuture)
.exceptionally(function)
.thenCompose(identity());
}

}
26 changes: 26 additions & 0 deletions src/test/java/org/zalando/fauxpas/ExceptionallyComposeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.zalando.fauxpas;

import org.junit.jupiter.api.Test;

import java.util.concurrent.CompletableFuture;

import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.zalando.fauxpas.FauxPas.exceptionallyCompose;
import static org.zalando.fauxpas.FauxPas.partially;

class ExceptionallyComposeTest {

@Test
void shouldExceptionallyCompose() {
final CompletableFuture<String> original = new CompletableFuture<>();
final CompletableFuture<String> unit = exceptionallyCompose(original, partially(e ->
completedFuture("result")));

original.completeExceptionally(new RuntimeException());

assertThat(unit.join(), is("result"));
}

}
16 changes: 16 additions & 0 deletions src/test/java/org/zalando/fauxpas/ExceptionallyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
import java.util.function.Function;
import java.util.function.Predicate;

import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.zalando.fauxpas.FauxPas.exceptionallyCompose;
import static org.zalando.fauxpas.FauxPas.partially;

class ExceptionallyTest {
Expand All @@ -31,6 +34,19 @@ void shouldReturnResult() {
assertThat(unit.join(), is("result"));
}

@Test
void shouldCascade() {
final CompletableFuture<String> original = new CompletableFuture<>();
final CompletableFuture<String> unit = original.exceptionally(partially(e -> {
throw new IllegalStateException();
}));

original.completeExceptionally(new IllegalArgumentException());

final CompletionException thrown = assertThrows(CompletionException.class, unit::join);
assertThat(thrown.getCause(), is(instanceOf(IllegalStateException.class)));
}

@Test
void shouldUseFallbackWhenExplicitlyCompletedExceptionally() {
final CompletableFuture<String> original = new CompletableFuture<>();
Expand Down
38 changes: 38 additions & 0 deletions src/test/java/org/zalando/fauxpas/HandleComposeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.zalando.fauxpas;

import org.junit.jupiter.api.Test;

import java.util.concurrent.CompletableFuture;

import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.zalando.fauxpas.FauxPas.exceptionallyCompose;
import static org.zalando.fauxpas.FauxPas.handleCompose;
import static org.zalando.fauxpas.FauxPas.partially;

class HandleComposeTest {

@Test
void shouldHandleComposeResult() {
final CompletableFuture<String> original = new CompletableFuture<>();
final CompletableFuture<String> unit = handleCompose(original, (s, throwable) ->
completedFuture("result"));

original.complete("foo");

assertThat(unit.join(), is("result"));
}

@Test
void shouldHandleComposeException() {
final CompletableFuture<String> original = new CompletableFuture<>();
final CompletableFuture<String> unit = handleCompose(original, (s, throwable) ->
completedFuture("result"));

original.completeExceptionally(new RuntimeException());

assertThat(unit.join(), is("result"));
}

}

0 comments on commit 26512f5

Please sign in to comment.