diff --git a/CHANGELOG.md b/CHANGELOG.md index 2507a945e..2a6c01ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Version 8.14 * Add support for RxJava Observable and Single return types via the `HystrixFeign` builder. +* Adds fallback implementation configuration to the `HystrixFeign` builder ### Version 8.13 * Never expands >8kb responses into memory diff --git a/hystrix/README.md b/hystrix/README.md index 7d946548e..40d4d22d2 100644 --- a/hystrix/README.md +++ b/hystrix/README.md @@ -48,4 +48,33 @@ api.getYourType("a").execute(); // or to apply hystrix to existing feign methods. api.getYourTypeSynchronous("a"); -``` \ No newline at end of file +``` + +### Fallback support + +Fallbacks are known values, which you return when there's an error invoking an http method. +For example, you can return a cached result as opposed to raising an error to the caller. To use +this feature, pass a safe implementation of your target interface as the last parameter to `HystrixFeign.Builder.target`. + +Here's an example: + +```java +// When dealing with fallbacks, it is less tedious to keep interfaces small. +interface GitHub { + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("repo") String repo); +} + +// This instance will be invoked if there are errors of any kind. +GitHub fallback = (owner, repo) -> { + if (owner.equals("Netflix") && repo.equals("feign")) { + return Arrays.asList("stuarthendren"); // inspired this approach! + } else { + return Collections.emptyList(); + } +}; + +GitHub github = HystrixFeign.builder() + ... + .target(GitHub.class, "https://api.github.com", fallback); +``` \ No newline at end of file diff --git a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java index f158ae762..ca60a08b8 100644 --- a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java +++ b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java @@ -2,12 +2,27 @@ import com.netflix.hystrix.HystrixCommand; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +import feign.Client; import feign.Contract; import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.Logger; +import feign.Request; +import feign.RequestInterceptor; +import feign.Retryer; +import feign.Target; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; /** - * Allows Feign interfaces to return HystrixCommand or rx.Observable or rx.Single objects. - * Also decorates normal Feign methods with circuit breakers, but calls {@link HystrixCommand#execute()} directly. + * Allows Feign interfaces to return HystrixCommand or rx.Observable or rx.Single objects. Also + * decorates normal Feign methods with circuit breakers, but calls {@link HystrixCommand#execute()} + * directly. */ public final class HystrixFeign { @@ -15,16 +30,186 @@ public static Builder builder() { return new Builder(); } - public static final class Builder extends Feign.Builder { + // Doesn't extend Feign.Builder for two reasons: + // * Hide invocationHandlerFactory - as this isn't customizable + // * Provide a path to the new fallback method w/o using covariant return types + public static final class Builder { + private final Feign.Builder delegate = new Feign.Builder(); + private Contract contract = new Contract.Default(); + + /** + * @see #target(Class, String, Object) + */ + public T target(Target target, final T fallback) { + delegate.invocationHandlerFactory(new InvocationHandlerFactory() { + @Override + public InvocationHandler create(Target target, Map dispatch) { + return new HystrixInvocationHandler(target, dispatch, fallback); + } + }); + delegate.contract(new HystrixDelegatingContract(contract)); + return delegate.build().newInstance(target); + } + + /** + * Like {@link Feign#newInstance(Target)}, except with {@link HystrixCommand#getFallback() + * fallback} support. + * + *

Fallbacks are known values, which you return when there's an error invoking an http + * method. For example, you can return a cached result as opposed to raising an error to the + * caller. To use this feature, pass a safe implementation of your target interface as the last + * parameter. + * + * Here's an example: + *

+     * {@code
+     *
+     * // When dealing with fallbacks, it is less tedious to keep interfaces small.
+     * interface GitHub {
+     *   @RequestLine("GET /repos/{owner}/{repo}/contributors")
+     *   List contributors(@Param("owner") String owner, @Param("repo") String repo);
+     * }
+     *
+     * // This instance will be invoked if there are errors of any kind.
+     * GitHub fallback = (owner, repo) -> {
+     *   if (owner.equals("Netflix") && repo.equals("feign")) {
+     *     return Arrays.asList("stuarthendren"); // inspired this approach!
+     *   } else {
+     *     return Collections.emptyList();
+     *   }
+     * };
+     *
+     * GitHub github = HystrixFeign.builder()
+     *                             ...
+     *                             .target(GitHub.class, "https://api.github.com", fallback);
+     * }
+ * + * @see #target(Target, Object) + */ + public T target(Class apiType, String url, T fallback) { + return target(new Target.HardCodedTarget(apiType, url), fallback); + } + + /** + * @see feign.Feign.Builder#contract + */ + public Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + /** + * @see feign.Feign.Builder#build + */ + public Feign build() { + delegate.invocationHandlerFactory(new HystrixInvocationHandler.Factory()); + delegate.contract(new HystrixDelegatingContract(contract)); + return delegate.build(); + } + + // re-declaring methods in Feign.Builder is same work as covariant overrides, + // but results in less complex bytecode. + + /** + * @see feign.Feign.Builder#target(Class, String) + */ + public T target(Class apiType, String url) { + return target(new Target.HardCodedTarget(apiType, url)); + } + + /** + * @see feign.Feign.Builder#target(Target) + */ + public T target(Target target) { + return build().newInstance(target); + } + + /** + * @see feign.Feign.Builder#logLevel + */ + public Builder logLevel(Logger.Level logLevel) { + delegate.logLevel(logLevel); + return this; + } + + /** + * @see feign.Feign.Builder#client + */ + public Builder client(Client client) { + delegate.client(client); + return this; + } + + /** + * @see feign.Feign.Builder#retryer + */ + public Builder retryer(Retryer retryer) { + delegate.retryer(retryer); + return this; + } + + /** + * @see feign.Feign.Builder#retryer + */ + public Builder logger(Logger logger) { + delegate.logger(logger); + return this; + } + + /** + * @see feign.Feign.Builder#encoder + */ + public Builder encoder(Encoder encoder) { + delegate.encoder(encoder); + return this; + } + + /** + * @see feign.Feign.Builder#decoder + */ + public Builder decoder(Decoder decoder) { + delegate.decoder(decoder); + return this; + } + + /** + * @see feign.Feign.Builder#decode404 + */ + public Builder decode404() { + delegate.decode404(); + return this; + } + + /** + * @see feign.Feign.Builder#errorDecoder + */ + public Builder errorDecoder(ErrorDecoder errorDecoder) { + delegate.errorDecoder(errorDecoder); + return this; + } + + /** + * @see feign.Feign.Builder#options + */ + public Builder options(Request.Options options) { + delegate.options(options); + return this; + } - public Builder() { - invocationHandlerFactory(new HystrixInvocationHandler.Factory()); - contract(new HystrixDelegatingContract(new Contract.Default())); + /** + * @see feign.Feign.Builder#requestInterceptor + */ + public Builder requestInterceptor(RequestInterceptor requestInterceptor) { + delegate.requestInterceptor(requestInterceptor); + return this; } - @Override - public Feign.Builder contract(Contract contract) { - return super.contract(new HystrixDelegatingContract(contract)); + /** + * @see feign.Feign.Builder#requestInterceptors + */ + public Builder requestInterceptors(Iterable requestInterceptors) { + delegate.requestInterceptors(requestInterceptors); + return this; } } } diff --git a/hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java b/hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java index 284c87faa..1d0aea7ac 100644 --- a/hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java +++ b/hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java @@ -15,30 +15,33 @@ */ package feign.hystrix; -import static feign.Util.checkNotNull; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; -import com.netflix.hystrix.HystrixCommand; -import com.netflix.hystrix.HystrixCommandGroupKey; -import com.netflix.hystrix.HystrixCommandKey; - import feign.InvocationHandlerFactory; import feign.InvocationHandlerFactory.MethodHandler; import feign.Target; import rx.Observable; import rx.Single; +import static feign.Util.checkNotNull; + final class HystrixInvocationHandler implements InvocationHandler { private final Target target; private final Map dispatch; + private final Object fallback; // Nullable - HystrixInvocationHandler(Target target, Map dispatch) { + HystrixInvocationHandler(Target target, Map dispatch, Object fallback) { this.target = checkNotNull(target, "target"); this.dispatch = checkNotNull(dispatch, "dispatch"); + this.fallback = fallback; } @Override @@ -60,6 +63,20 @@ protected Object run() throws Exception { throw (Error)t; } } + + @Override + protected Object getFallback() { + if (fallback == null) return super.getFallback(); + try { + return method.invoke(fallback, args); + } catch (IllegalAccessException e) { + // shouldn't happen as method is public due to being an interface + throw new AssertionError(e); + } catch (InvocationTargetException e) { + // Exceptions on fallback are tossed by Hystrix + throw new AssertionError(e.getCause()); + } + } }; if (HystrixCommand.class.isAssignableFrom(method.getReturnType())) { @@ -78,7 +95,7 @@ static final class Factory implements InvocationHandlerFactory { @Override public InvocationHandler create(Target target, Map dispatch) { - return new HystrixInvocationHandler(target, dispatch); + return new HystrixInvocationHandler(target, dispatch, null); } } } \ No newline at end of file diff --git a/hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java b/hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java index 4112eaaf4..9df8c6a60 100644 --- a/hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java +++ b/hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java @@ -1,26 +1,31 @@ package feign.hystrix; -import static feign.assertj.MockWebServerAssertions.assertThat; - -import java.util.Arrays; -import java.util.List; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.exception.HystrixRuntimeException; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import com.netflix.hystrix.HystrixCommand; -import com.squareup.okhttp.mockwebserver.MockResponse; -import com.squareup.okhttp.mockwebserver.MockWebServer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import feign.FeignException; import feign.Headers; +import feign.Param; import feign.RequestLine; import feign.gson.GsonDecoder; import rx.Observable; import rx.Single; import rx.observers.TestSubscriber; +import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.hamcrest.core.Is.isA; + public class HystrixBuilderTest { @Rule @@ -61,7 +66,71 @@ public void hystrixCommandList() { HystrixCommand> command = api.listCommand(); assertThat(command).isNotNull(); - assertThat(command.execute()).hasSize(2).contains("foo", "bar"); + assertThat(command.execute()).containsExactly("foo", "bar"); + } + + // When dealing with fallbacks, it is less tedious to keep interfaces small. + interface GitHub { + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("repo") String repo); + } + + @Test + public void fallbacksApplyOnError() { + server.enqueue(new MockResponse().setResponseCode(500)); + + GitHub fallback = new GitHub(){ + @Override + public List contributors(String owner, String repo) { + if (owner.equals("Netflix") && repo.equals("feign")) { + return Arrays.asList("stuarthendren"); // inspired this approach! + } else { + return Collections.emptyList(); + } + } + }; + + GitHub api = HystrixFeign.builder() + .target(GitHub.class, "http://localhost:" + server.getPort(), fallback); + + List result = api.contributors("Netflix", "feign"); + + assertThat(result).containsExactly("stuarthendren"); + } + + @Test + public void errorInFallbackHasExpectedBehavior() { + thrown.expect(HystrixRuntimeException.class); + thrown.expectMessage("contributors failed and fallback failed."); + thrown.expectCause(isA(FeignException.class)); // as opposed to RuntimeException (from the fallback) + + server.enqueue(new MockResponse().setResponseCode(500)); + + GitHub fallback = new GitHub(){ + @Override + public List contributors(String owner, String repo) { + throw new RuntimeException("oops"); + } + }; + + GitHub api = HystrixFeign.builder() + .target(GitHub.class, "http://localhost:" + server.getPort(), fallback); + + api.contributors("Netflix", "feign"); + } + + @Test + public void hystrixRuntimeExceptionPropagatesOnException() { + thrown.expect(HystrixRuntimeException.class); + thrown.expectMessage("contributors failed and no fallback available."); + thrown.expectCause(isA(FeignException.class)); + + server.enqueue(new MockResponse().setResponseCode(500)); + + GitHub api = HystrixFeign.builder() + .target(GitHub.class, "http://localhost:" + server.getPort()); + + api.contributors("Netflix", "feign"); } @Test @@ -113,7 +182,7 @@ public void rxObservableList() { TestSubscriber> testSubscriber = new TestSubscriber>(); observable.subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); - assertThat(testSubscriber.getOnNextEvents().get(0)).hasSize(2).contains("foo", "bar"); + assertThat(testSubscriber.getOnNextEvents().get(0)).containsExactly("foo", "bar"); } @Test @@ -164,7 +233,7 @@ public void rxSingleList() { TestSubscriber> testSubscriber = new TestSubscriber>(); single.subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); - assertThat(testSubscriber.getOnNextEvents().get(0)).hasSize(2).contains("foo", "bar"); + assertThat(testSubscriber.getOnNextEvents().get(0)).containsExactly("foo", "bar"); } @Test @@ -186,7 +255,7 @@ public void plainList() { List list = api.getList(); - assertThat(list).isNotNull().hasSize(2).contains("foo", "bar"); + assertThat(list).isNotNull().containsExactly("foo", "bar"); } private TestInterface target() {