Skip to content

Commit

Permalink
Merge pull request #16 from zalando/feature/exceptionally
Browse files Browse the repository at this point in the history
Added support for CompletableFuture.exceptionally
  • Loading branch information
whiskeysierra authored Aug 1, 2017
2 parents 44102f1 + 051f273 commit 8d26317
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 117 deletions.
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The followings statements apply to each of them:
- extends the official interface, i.e. they are 100% compatible
- [*sneakily throws*](https://projectlombok.org/features/SneakyThrows.html) the original exception

### Creation
#### Creation

The way the Java runtime implemented functional interfaces always requires additional type information, either by
using a cast or a local variable:
Expand Down Expand Up @@ -120,6 +120,60 @@ return tryWith(new BufferedReader(new FileReader(path)), br ->
);
```

### CompletableFuture.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)
into a `CompletableFuture`:

```java
future.exceptionally(e -> {
Throwable t = e instanceof CompletionException ? e.getCause() : e;

if (t instanceof NoRouteToHostException) {
return fallbackValueFor(e);
}

throw e instanceof CompletionException ? e : new CompletionException(t);
})
```

Unfortunately it has a contract that makes it harder to use than it needs to:

- It takes a [`Throwable`](https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html) as an argument, but
doesn't allow to re-throw it *as-is*. This can be circumvented by optionally [wrapping it in a
`CompletionException`](http://cs.oswego.edu/pipermail/concurrency-interest/2014-August/012910.html) before
rethrowing it.
- The throwable argument is [sometimes wrapped](https://stackoverflow.com/questions/27430255/surprising-behavior-of-java-8-completablefuture-exceptionally-method)
inside a [`CompletionException`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletionException.html)
and sometimes it's not, depending on whether there is any other computation step before the `exceptionally(..)` call
or not.

In order to use the operation correctly one needs to follow these rules:
1. Unwrap given throwable if it's an instance of `CompletionException`.
2. Wrap checked exceptions in a `CompletionException` before throwing.

`FauxPas.partially(..)` relives some of the pain by changing the interface and contract a bit to make it more usable.
The following example is functionally equivalent to the one from above:

```java
future.exceptionally(partially(e -> {
if (e instanceof NoRouteToHostException) {
return fallbackValueFor(e);
}

throw e;
}))
```

1. Takes a `ThrowingFunction<Throwable, T, Throwable>`, i.e. it allows clients to
- directly re-throw the throwable argument
- throw any exception during exception handling *as-is*
2. Will automatically unwrap a `CompletionException` before passing it to the given function.
I.e. the supplied function will never have to deal with `CompletionException` directly. Except for the rare occasion
that the `CompletionException` has no cause, in which case it will be passed to the given function.
3. Will automatically wrap any thrown `Exception` inside a `CompletionException`, if needed.

## Getting Help

If you have questions, concerns, bug reports, etc., please file an issue in this repository's [Issue Tracker](../../issues).
Expand Down
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit-platform.version>1.0.0-M2</junit-platform.version>
<junit-jupiter.version>5.0.0-M2</junit-jupiter.version>
<junit-platform.version>1.0.0-M6</junit-platform.version>
<junit-jupiter.version>5.0.0-M6</junit-jupiter.version>
</properties>

<dependencies>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.1</version>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.google.gag</groupId>
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/zalando/fauxpas/FauxPas.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.zalando.fauxpas;

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

public final class FauxPas {

FauxPas() {
Expand Down Expand Up @@ -56,4 +60,21 @@ public static <T, U, X extends Throwable> ThrowingBiPredicate<T, U, X> throwingB
return predicate;
}

public static <R> Function<Throwable, R> partially(final ThrowingFunction<Throwable, R, Throwable> function) {
return throwable -> {
try {
return function.tryApply(unpack(throwable));
} catch (final CompletionException e) {
throw e;
} catch (final Throwable e) {
throw new CompletionException(e);
}
};
}

private static Throwable unpack(final Throwable throwable) {
final Throwable cause = throwable.getCause();
return throwable instanceof CompletionException && cause != null ? cause : throwable;
}

}
64 changes: 32 additions & 32 deletions src/test/java/org/zalando/fauxpas/DefaultImplementationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.expectThrows;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.zalando.fauxpas.FauxPas.throwingBiConsumer;
import static org.zalando.fauxpas.FauxPas.throwingBiFunction;
import static org.zalando.fauxpas.FauxPas.throwingBiPredicate;
Expand All @@ -25,151 +25,151 @@
* Tests the default implementation of e.g. {@link Function#apply(Object)} in {@link ThrowingFunction}.
*/
@RunWith(JUnitPlatform.class)
public final class DefaultImplementationTest {
final class DefaultImplementationTest {

@SuppressWarnings("ThrowableInstanceNeverThrown") // we're in fact throwing it, multiple times even...
private final Exception exception = new Exception();

@Test
public void shouldRethrowExceptionFromRunnable() {
void shouldRethrowExceptionFromRunnable() {
final ThrowingRunnable<Exception> runnable = throwingRunnable(() -> {
throw exception;
});
shouldThrow(runnable);
}

@Test
public void shouldNotRethrowExceptionFromRunnable() throws Exception {
void shouldNotRethrowExceptionFromRunnable() throws Exception {
final ThrowingRunnable<Exception> runnable = throwingRunnable(() -> {
});
shouldNotThrow(runnable);
}

@Test
public void shouldRethrowExceptionFromSupplier() {
void shouldRethrowExceptionFromSupplier() {
final ThrowingSupplier<Void, Exception> supplier = throwingSupplier(() -> {
throw exception;
});
shouldThrow(supplier::get);
}

@Test
public void shouldNotRethrowExceptionFromSupplier() throws Exception {
void shouldNotRethrowExceptionFromSupplier() throws Exception {
final ThrowingSupplier<Void, Exception> supplier = throwingSupplier(() -> null);
shouldNotThrow(supplier::get);
}

@Test
public void shouldRethrowExceptionFromConsumer() {
void shouldRethrowExceptionFromConsumer() {
final ThrowingConsumer<Void, Exception> consumer = throwingConsumer($ -> {
throw exception;
});
shouldThrow(() -> consumer.accept(null));
}

@Test
public void shouldNotRethrowExceptionFromConsumer() throws Exception {
void shouldNotRethrowExceptionFromConsumer() throws Exception {
final ThrowingConsumer<Void, Exception> consumer = throwingConsumer($ -> {
});
shouldNotThrow(() -> consumer.accept(null));
}

@Test
public void shouldRethrowExceptionFromFunction() {
void shouldRethrowExceptionFromFunction() {
final ThrowingFunction<Void, Void, Exception> function = throwingFunction($ -> {
throw exception;
});
shouldThrow(() -> function.apply(null));
}

@Test
public void shouldNotRethrowExceptionFromFunction() throws Exception {
void shouldNotRethrowExceptionFromFunction() throws Exception {
final ThrowingFunction<Void, Void, Exception> function = throwingFunction((Void $) -> null);
shouldNotThrow(() -> function.apply(null));
}

@Test
public void shouldRethrowExceptionFromUnaryOperator() {
void shouldRethrowExceptionFromUnaryOperator() {
final ThrowingUnaryOperator<Void, Exception> operator = throwingUnaryOperator($ -> {
throw exception;
});
shouldThrow(() -> operator.apply(null));
}

@Test
public void shouldNotRethrowExceptionFromUnaryOperator() throws Exception {
void shouldNotRethrowExceptionFromUnaryOperator() throws Exception {
final ThrowingUnaryOperator<Void, Exception> operator = throwingUnaryOperator((Void $) -> null);
shouldNotThrow(() -> operator.apply(null));
}

@Test
public void shouldRethrowExceptionFromPredicate() {
void shouldRethrowExceptionFromPredicate() {
final ThrowingPredicate<Void, Exception> predicate = throwingPredicate($ -> {
throw exception;
});
shouldThrow(() -> predicate.test(null));
}

@Test
public void shouldNotRethrowExceptionFromPredicate() throws Exception {
void shouldNotRethrowExceptionFromPredicate() throws Exception {
final ThrowingPredicate<Void, Exception> predicate = throwingPredicate((Void $) -> false);
shouldNotThrow(() -> predicate.test(null));
}

@Test
public void shouldRethrowExceptionFromBiConsumer() {
final ThrowingBiConsumer<Void, Void, Exception> consumer = throwingBiConsumer(($, ) -> {
void shouldRethrowExceptionFromBiConsumer() {
final ThrowingBiConsumer<Void, Void, Exception> consumer = throwingBiConsumer(($, $2) -> {
throw exception;
});
shouldThrow(() -> consumer.accept(null, null));
}

@Test
public void shouldNotRethrowExceptionFromBiConsumer() throws Exception {
final ThrowingBiConsumer<Void, Void, Exception> consumer = throwingBiConsumer(($, ) -> {
void shouldNotRethrowExceptionFromBiConsumer() throws Exception {
final ThrowingBiConsumer<Void, Void, Exception> consumer = throwingBiConsumer(($, $2) -> {
});
shouldNotThrow(() -> consumer.accept(null, null));
}

@Test
public void shouldRethrowExceptionFromBiFunction() {
final ThrowingBiFunction<Void, Void, Void, Exception> function = throwingBiFunction(($, ) -> {
void shouldRethrowExceptionFromBiFunction() {
final ThrowingBiFunction<Void, Void, Void, Exception> function = throwingBiFunction(($, $2) -> {
throw exception;
});
shouldThrow(() -> function.apply(null, null));
}

@Test
public void shouldNotRethrowExceptionFromBiFunction() throws Exception {
final ThrowingBiFunction<Void, Void, Void, Exception> function = throwingBiFunction(($, ) -> null);
void shouldNotRethrowExceptionFromBiFunction() throws Exception {
final ThrowingBiFunction<Void, Void, Void, Exception> function = throwingBiFunction(($, $2) -> null);
shouldNotThrow(() -> function.apply(null, null));
}

@Test
public void shouldRethrowExceptionFromBinaryOperator() {
final ThrowingBinaryOperator<Void, Exception> operator = throwingBinaryOperator(($, ) -> {
void shouldRethrowExceptionFromBinaryOperator() {
final ThrowingBinaryOperator<Void, Exception> operator = throwingBinaryOperator(($, $2) -> {
throw exception;
});
shouldThrow(() -> operator.apply(null, null));
}

@Test
public void shouldNotRethrowExceptionFromBinaryOperator() throws Exception {
final ThrowingBinaryOperator<Void, Exception> operator = throwingBinaryOperator(($, ) -> null);
void shouldNotRethrowExceptionFromBinaryOperator() throws Exception {
final ThrowingBinaryOperator<Void, Exception> operator = throwingBinaryOperator(($, $2) -> null);
shouldNotThrow(() -> operator.apply(null, null));
}

@Test
public void shouldRethrowExceptionFromBiPredicate() {
final ThrowingBiPredicate<Void, Void, Exception> predicate = throwingBiPredicate(($, ) -> {
void shouldRethrowExceptionFromBiPredicate() {
final ThrowingBiPredicate<Void, Void, Exception> predicate = throwingBiPredicate(($, $2) -> {
throw exception;
});
shouldThrow(() -> predicate.test(null, null));
}

@Test
public void shouldNotRethrowExceptionFromBiPredicate() throws Exception {
final ThrowingBiPredicate<Void, Void, Exception> predicate = throwingBiPredicate(($, ) -> false);
void shouldNotRethrowExceptionFromBiPredicate() throws Exception {
final ThrowingBiPredicate<Void, Void, Exception> predicate = throwingBiPredicate(($, $2) -> false);
shouldNotThrow(() -> predicate.test(null, null));
}

Expand All @@ -178,8 +178,8 @@ private void shouldNotThrow(final Runnable nonThrower) {
}

private void shouldThrow(final Runnable thrower) {
expectThrows(Exception.class, thrower::run);
assertThrows(Exception.class, thrower::run);
assertThat(exception, is(sameInstance(exception)));
}

}
}
6 changes: 3 additions & 3 deletions src/test/java/org/zalando/fauxpas/EnforceCoverageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
@Hack
@OhNoYouDidnt
@RunWith(JUnitPlatform.class)
public final class EnforceCoverageTest {
final class EnforceCoverageTest {

@Test
public void shouldUseFauxPasConstructor() {
void shouldUseFauxPasConstructor() {
new FauxPas();
}

@Test
public void shouldUseTryWithConstructor() {
void shouldUseTryWithConstructor() {
new TryWith();
}

Expand Down
Loading

0 comments on commit 8d26317

Please sign in to comment.