Skip to content

Commit

Permalink
Add support for CompletableFuture for method return types
Browse files Browse the repository at this point in the history
Implements support for `CompletableFuture` on method return types by converting through RxJava `Observable`
  • Loading branch information
wjam committed Feb 8, 2018
1 parent 0ad007a commit 4816937
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 2 deletions.
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true

[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2

[pom.xml]
indent_size = 2

[*.xml]
indent_size = 4
10 changes: 8 additions & 2 deletions hystrix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ GitHub github = HystrixFeign.builder()
.target(GitHub.class, "https://api.github.com");
```

For asynchronous or reactive use, return `HystrixCommand<YourType>`.
For asynchronous or reactive use, return `HystrixCommand<YourType>` or `CompletableFuture<YourType>`.

For RxJava compatibility, use `rx.Observable<YourType>` or `rx.Single<YourType>`. Rx types are <a href="http://reactivex.io/documentation/observable.html">cold</a>, which means a http call isn't made until there's a subscriber.

Methods that do *not* return [`HystrixCommand`](https://netflix.github.io/Hystrix/javadoc/com/netflix/hystrix/HystrixCommand.html), [`rx.Observable`](http://reactivex.io/RxJava/javadoc/rx/Observable.html) or [`rx.Single`] are still wrapped in a `HystrixCommand`, but `execute()` is automatically called for you.
Methods that do *not* return [`HystrixCommand`](https://netflix.github.io/Hystrix/javadoc/com/netflix/hystrix/HystrixCommand.html), `CompletableFuture`, [`rx.Observable`](http://reactivex.io/RxJava/javadoc/rx/Observable.html) or `rx.Single` are still wrapped in a `HystrixCommand`, but `execute()` is automatically called for you.

```java
interface YourApi {
Expand All @@ -27,6 +27,9 @@ interface YourApi {
@RequestLine("GET /yourtype/{id}")
Single<YourType> getYourTypeSingle(@Param("id") String id);

@RequestLine("GET /yourtype/{id}")
CompletableFuture<YourType> getYourTypeCompletableFuture(@Param("id") String id);

@RequestLine("GET /yourtype/{id}")
YourType getYourTypeSynchronous(@Param("id") String id);
}
Expand All @@ -46,6 +49,9 @@ api.getYourType("a").queue();
// for synchronous
api.getYourType("a").execute();

// or for a CompletableFuture
api.getYourTypeCompletableFuture("a").thenApply(o -> "b");

// or to apply hystrix to existing feign methods.
api.getYourTypeSynchronous("a");
```
Expand Down
6 changes: 6 additions & 0 deletions hystrix/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
<artifactId>feign-core</artifactId>
</dependency>

<dependency>
<groupId>org.jvnet</groupId>
<artifactId>animal-sniffer-annotation</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
metadata.returnType(actualType);
} else if (type instanceof ParameterizedType && ((ParameterizedType) type).getRawType().equals(Completable.class)) {
metadata.returnType(void.class);
} else if (type instanceof ParameterizedType && Util.isCompletableFuture(((ParameterizedType) type).getRawType())) {
metadata.returnType(Util.resolveTypeParameter(type));
}
}

Expand Down
17 changes: 17 additions & 0 deletions hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
Expand Down Expand Up @@ -132,6 +134,8 @@ protected Object getFallback() {
} else if (isReturnsCompletable(method)) {
((Completable) result).await();
return null;
} else if (isReturnsCompletableFuture(method)) {
return ((Future) result).get();
} else {
return result;
}
Expand All @@ -141,6 +145,13 @@ protected Object getFallback() {
} catch (InvocationTargetException e) {
// Exceptions on fallback are tossed by Hystrix
throw new AssertionError(e.getCause());
} catch (InterruptedException e) {
// Exceptions on fallback are tossed by Hystrix
Thread.currentThread().interrupt();
throw new AssertionError(e.getCause());
} catch (ExecutionException e) {
// Exceptions on fallback are tossed by Hystrix
throw new AssertionError(e.getCause());
}
}
};
Expand All @@ -155,6 +166,8 @@ protected Object getFallback() {
return hystrixCommand.toObservable().toSingle();
} else if (isReturnsCompletable(method)) {
return hystrixCommand.toObservable().toCompletable();
} else if (isReturnsCompletableFuture(method)) {
return new ObservableCompletableFuture<Object>(hystrixCommand);
}
return hystrixCommand.execute();
}
Expand All @@ -171,6 +184,10 @@ private boolean isReturnsObservable(Method method) {
return Observable.class.isAssignableFrom(method.getReturnType());
}

private boolean isReturnsCompletableFuture(Method method) {
return Util.isCompletableFuture(method.getReturnType());
}

private boolean isReturnsSingle(Method method) {
return Single.class.isAssignableFrom(method.getReturnType());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package feign.hystrix;

import com.netflix.hystrix.HystrixCommand;
import org.jvnet.animal_sniffer.IgnoreJRERequirement;
import rx.Subscription;
import rx.functions.Action1;

import java.util.concurrent.CompletableFuture;

@IgnoreJRERequirement
final class ObservableCompletableFuture<T> extends CompletableFuture<T> {

private final Subscription sub;

ObservableCompletableFuture(final HystrixCommand<T> command) {
this.sub = command.toObservable().single().subscribe(new Action1<T>() {
@Override
public void call(final T o) {
ObservableCompletableFuture.this.complete(o);
}
}, new Action1<Throwable>() {
@Override
public void call(final Throwable throwable) {
ObservableCompletableFuture.this.completeExceptionally(throwable);
}
});
}


@Override
public boolean cancel(final boolean b) {
sub.unsubscribe();
return super.cancel(b);
}
}
34 changes: 34 additions & 0 deletions hystrix/src/main/java/feign/hystrix/Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package feign.hystrix;

import java.lang.reflect.Type;

import static feign.Util.resolveLastTypeParameter;

final class Util {

private static final Class<?> COMPLETABLE_FUTURE_CLASS;

static {
Class<?> clazz = null;
try {
clazz = Class.forName("java.util.concurrent.CompletableFuture");
} catch (ClassNotFoundException e) {
// Not running on Java 8+
}

COMPLETABLE_FUTURE_CLASS = clazz;
}

static boolean isCompletableFuture(final Class<?> clazz) {
return COMPLETABLE_FUTURE_CLASS != null && COMPLETABLE_FUTURE_CLASS.isAssignableFrom(clazz);
}

static boolean isCompletableFuture(final Type type) {
return COMPLETABLE_FUTURE_CLASS != null && type.equals(COMPLETABLE_FUTURE_CLASS);
}

static Type resolveTypeParameter(final Type type) {
return resolveLastTypeParameter(type, COMPLETABLE_FUTURE_CLASS);
}

}
69 changes: 69 additions & 0 deletions hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import feign.FeignException;
import feign.Headers;
Expand Down Expand Up @@ -406,6 +410,63 @@ public void rxSingleListFallback() {
assertThat(testSubscriber.getOnNextEvents().get(0)).containsExactly("fallback");
}

@Test
public void completableFutureEmptyBody() throws InterruptedException, ExecutionException, TimeoutException {
server.enqueue(new MockResponse());

TestInterface api = target();

CompletableFuture<String> completable = api.completableFuture();

assertThat(completable).isNotNull();

completable.get(5, TimeUnit.SECONDS);
}

@Test
public void completableFutureWithBody() throws InterruptedException, ExecutionException, TimeoutException {
server.enqueue(new MockResponse().setBody("foo"));

TestInterface api = target();

CompletableFuture<String> completable = api.completableFuture();

assertThat(completable).isNotNull();

assertThat(completable.get(5, TimeUnit.SECONDS)).isEqualTo("foo");
}

@Test
public void completableFutureFailWithoutFallback() throws TimeoutException, InterruptedException {
server.enqueue(new MockResponse().setResponseCode(500));

TestInterface api = HystrixFeign.builder()
.target(TestInterface.class, "http://localhost:" + server.getPort());

CompletableFuture<String> completable = api.completableFuture();

assertThat(completable).isNotNull();

try {
completable.get(5, TimeUnit.SECONDS);
} catch (ExecutionException e) {
assertThat(e).hasCauseInstanceOf(HystrixRuntimeException.class);
}
}

@Test
public void completableFutureFallback() throws InterruptedException, ExecutionException, TimeoutException {
server.enqueue(new MockResponse().setResponseCode(500));

TestInterface api = target();

CompletableFuture<String> completable = api.completableFuture();

assertThat(completable).isNotNull();

assertThat(completable.get(5, TimeUnit.SECONDS)).isEqualTo("fallback");
}

@Test
public void rxCompletableEmptyBody() {
server.enqueue(new MockResponse());
Expand Down Expand Up @@ -632,6 +693,9 @@ interface TestInterface {

@RequestLine("GET /")
Completable completable();

@RequestLine("GET /")
CompletableFuture<String> completableFuture();
}

class FallbackTestInterface implements TestInterface {
Expand Down Expand Up @@ -717,5 +781,10 @@ public List<String> getList() {
public Completable completable() {
return Completable.complete();
}

@Override
public CompletableFuture<String> completableFuture() {
return CompletableFuture.completedFuture("fallback");
}
}
}

0 comments on commit 4816937

Please sign in to comment.