Skip to content

Commit 9dfd9b4

Browse files
wjamvelo
authored andcommitted
Add support for CompletableFuture for method return types (#638)
Implements support for `CompletableFuture` on method return types by converting through RxJava `Observable`
1 parent f16553d commit 9dfd9b4

File tree

5 files changed

+135
-3
lines changed

5 files changed

+135
-3
lines changed

hystrix/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ GitHub github = HystrixFeign.builder()
1010
.target(GitHub.class, "https://api.github.com");
1111
```
1212

13-
For asynchronous or reactive use, return `HystrixCommand<YourType>`.
13+
For asynchronous or reactive use, return `HystrixCommand<YourType>` or `CompletableFuture<YourType>`.
1414

1515
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.
1616

17-
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.
17+
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.
1818

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

30+
@RequestLine("GET /yourtype/{id}")
31+
CompletableFuture<YourType> getYourTypeCompletableFuture(@Param("id") String id);
32+
3033
@RequestLine("GET /yourtype/{id}")
3134
YourType getYourTypeSynchronous(@Param("id") String id);
3235
}
@@ -46,6 +49,9 @@ api.getYourType("a").queue();
4649
// for synchronous
4750
api.getYourType("a").execute();
4851

52+
// or for a CompletableFuture
53+
api.getYourTypeCompletableFuture("a").thenApply(o -> "b");
54+
4955
// or to apply hystrix to existing feign methods.
5056
api.getYourTypeSynchronous("a");
5157
```

hystrix/src/main/java/feign/hystrix/HystrixDelegatingContract.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.lang.reflect.ParameterizedType;
1818
import java.lang.reflect.Type;
1919
import java.util.List;
20+
import java.util.concurrent.CompletableFuture;
2021
import com.netflix.hystrix.HystrixCommand;
2122
import feign.Contract;
2223
import feign.MethodMetadata;
@@ -63,6 +64,9 @@ public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
6364
} else if (type instanceof ParameterizedType
6465
&& ((ParameterizedType) type).getRawType().equals(Completable.class)) {
6566
metadata.returnType(void.class);
67+
} else if (type instanceof ParameterizedType
68+
&& ((ParameterizedType) type).getRawType().equals(CompletableFuture.class)) {
69+
metadata.returnType(resolveLastTypeParameter(type, CompletableFuture.class));
6670
}
6771
}
6872

hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import java.util.LinkedHashMap;
2323
import java.util.Map;
2424
import java.util.Set;
25+
import java.util.concurrent.CompletableFuture;
26+
import java.util.concurrent.ExecutionException;
27+
import java.util.concurrent.Future;
2528
import feign.InvocationHandlerFactory.MethodHandler;
2629
import feign.Target;
2730
import feign.Util;
@@ -130,15 +133,21 @@ protected Object getFallback() {
130133
} else if (isReturnsCompletable(method)) {
131134
((Completable) result).await();
132135
return null;
136+
} else if (isReturnsCompletableFuture(method)) {
137+
return ((Future) result).get();
133138
} else {
134139
return result;
135140
}
136141
} catch (IllegalAccessException e) {
137142
// shouldn't happen as method is public due to being an interface
138143
throw new AssertionError(e);
139-
} catch (InvocationTargetException e) {
144+
} catch (InvocationTargetException | ExecutionException e) {
140145
// Exceptions on fallback are tossed by Hystrix
141146
throw new AssertionError(e.getCause());
147+
} catch (InterruptedException e) {
148+
// Exceptions on fallback are tossed by Hystrix
149+
Thread.currentThread().interrupt();
150+
throw new AssertionError(e.getCause());
142151
}
143152
}
144153
};
@@ -155,6 +164,8 @@ protected Object getFallback() {
155164
return hystrixCommand.toObservable().toSingle();
156165
} else if (isReturnsCompletable(method)) {
157166
return hystrixCommand.toObservable().toCompletable();
167+
} else if (isReturnsCompletableFuture(method)) {
168+
return new ObservableCompletableFuture<>(hystrixCommand);
158169
}
159170
return hystrixCommand.execute();
160171
}
@@ -171,6 +182,10 @@ private boolean isReturnsObservable(Method method) {
171182
return Observable.class.isAssignableFrom(method.getReturnType());
172183
}
173184

185+
private boolean isReturnsCompletableFuture(Method method) {
186+
return CompletableFuture.class.isAssignableFrom(method.getReturnType());
187+
}
188+
174189
private boolean isReturnsSingle(Method method) {
175190
return Single.class.isAssignableFrom(method.getReturnType());
176191
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.hystrix;
15+
16+
import com.netflix.hystrix.HystrixCommand;
17+
import rx.Subscription;
18+
import java.util.concurrent.CompletableFuture;
19+
20+
final class ObservableCompletableFuture<T> extends CompletableFuture<T> {
21+
22+
private final Subscription sub;
23+
24+
ObservableCompletableFuture(final HystrixCommand<T> command) {
25+
this.sub = command.toObservable().single().subscribe(ObservableCompletableFuture.this::complete,
26+
ObservableCompletableFuture.this::completeExceptionally);
27+
}
28+
29+
30+
@Override
31+
public boolean cancel(final boolean b) {
32+
sub.unsubscribe();
33+
return super.cancel(b);
34+
}
35+
}

hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
import java.util.Arrays;
2727
import java.util.Collections;
2828
import java.util.List;
29+
import java.util.concurrent.CompletableFuture;
30+
import java.util.concurrent.ExecutionException;
31+
import java.util.concurrent.TimeUnit;
32+
import java.util.concurrent.TimeoutException;
2933
import feign.FeignException;
3034
import feign.Headers;
3135
import feign.Param;
@@ -427,6 +431,66 @@ public void rxSingleListFallback() {
427431
assertThat(testSubscriber.getOnNextEvents().get(0)).containsExactly("fallback");
428432
}
429433

434+
@Test
435+
public void completableFutureEmptyBody()
436+
throws InterruptedException, ExecutionException, TimeoutException {
437+
server.enqueue(new MockResponse());
438+
439+
TestInterface api = target();
440+
441+
CompletableFuture<String> completable = api.completableFuture();
442+
443+
assertThat(completable).isNotNull();
444+
445+
completable.get(5, TimeUnit.SECONDS);
446+
}
447+
448+
@Test
449+
public void completableFutureWithBody()
450+
throws InterruptedException, ExecutionException, TimeoutException {
451+
server.enqueue(new MockResponse().setBody("foo"));
452+
453+
TestInterface api = target();
454+
455+
CompletableFuture<String> completable = api.completableFuture();
456+
457+
assertThat(completable).isNotNull();
458+
459+
assertThat(completable.get(5, TimeUnit.SECONDS)).isEqualTo("foo");
460+
}
461+
462+
@Test
463+
public void completableFutureFailWithoutFallback() throws TimeoutException, InterruptedException {
464+
server.enqueue(new MockResponse().setResponseCode(500));
465+
466+
TestInterface api = HystrixFeign.builder()
467+
.target(TestInterface.class, "http://localhost:" + server.getPort());
468+
469+
CompletableFuture<String> completable = api.completableFuture();
470+
471+
assertThat(completable).isNotNull();
472+
473+
try {
474+
completable.get(5, TimeUnit.SECONDS);
475+
} catch (ExecutionException e) {
476+
assertThat(e).hasCauseInstanceOf(HystrixRuntimeException.class);
477+
}
478+
}
479+
480+
@Test
481+
public void completableFutureFallback()
482+
throws InterruptedException, ExecutionException, TimeoutException {
483+
server.enqueue(new MockResponse().setResponseCode(500));
484+
485+
TestInterface api = target();
486+
487+
CompletableFuture<String> completable = api.completableFuture();
488+
489+
assertThat(completable).isNotNull();
490+
491+
assertThat(completable.get(5, TimeUnit.SECONDS)).isEqualTo("fallback");
492+
}
493+
430494
@Test
431495
public void rxCompletableEmptyBody() {
432496
server.enqueue(new MockResponse());
@@ -657,6 +721,9 @@ default HystrixCommand<String> defaultMethodReturningCommand() {
657721

658722
@RequestLine("GET /")
659723
Completable completable();
724+
725+
@RequestLine("GET /")
726+
CompletableFuture<String> completableFuture();
660727
}
661728

662729
class FallbackTestInterface implements TestInterface {
@@ -742,5 +809,10 @@ public List<String> getList() {
742809
public Completable completable() {
743810
return Completable.complete();
744811
}
812+
813+
@Override
814+
public CompletableFuture<String> completableFuture() {
815+
return CompletableFuture.completedFuture("fallback");
816+
}
745817
}
746818
}

0 commit comments

Comments
 (0)