Browse Source

Merge pull request #308 from Netflix/now-with-fallbacks

Adds fallback implementation configuration to the HystrixFeign builder
pull/311/head
Adrian Cole 9 years ago
parent
commit
455525b87d
  1. 1
      CHANGELOG.md
  2. 31
      hystrix/README.md
  3. 203
      hystrix/src/main/java/feign/hystrix/HystrixFeign.java
  4. 31
      hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java
  5. 91
      hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java

1
CHANGELOG.md

@ -1,5 +1,6 @@ @@ -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

31
hystrix/README.md

@ -48,4 +48,33 @@ api.getYourType("a").execute(); @@ -48,4 +48,33 @@ api.getYourType("a").execute();
// or to apply hystrix to existing feign methods.
api.getYourTypeSynchronous("a");
```
```
### 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<String> 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);
```

203
hystrix/src/main/java/feign/hystrix/HystrixFeign.java

@ -2,12 +2,27 @@ package feign.hystrix; @@ -2,12 +2,27 @@ package feign.hystrix;
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 final class HystrixFeign { @@ -15,16 +30,186 @@ public final class HystrixFeign {
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> T target(Target<T> target, final T fallback) {
delegate.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> 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.
*
* <p>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:
* <pre>
* {@code
*
* // When dealing with fallbacks, it is less tedious to keep interfaces small.
* interface GitHub {
* @RequestLine("GET /repos/{owner}/{repo}/contributors")
* List<String> 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);
* }</pre>
*
* @see #target(Target, Object)
*/
public <T> T target(Class<T> apiType, String url, T fallback) {
return target(new Target.HardCodedTarget<T>(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> T target(Class<T> apiType, String url) {
return target(new Target.HardCodedTarget<T>(apiType, url));
}
/**
* @see feign.Feign.Builder#target(Target)
*/
public <T> T target(Target<T> 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<RequestInterceptor> requestInterceptors) {
delegate.requestInterceptors(requestInterceptors);
return this;
}
}
}

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

@ -15,30 +15,33 @@ @@ -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<Method, MethodHandler> dispatch;
private final Object fallback; // Nullable
HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch) {
HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch, Object fallback) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallback = fallback;
}
@Override
@ -60,6 +63,20 @@ final class HystrixInvocationHandler implements InvocationHandler { @@ -60,6 +63,20 @@ final class HystrixInvocationHandler implements InvocationHandler {
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 @@ final class HystrixInvocationHandler implements InvocationHandler { @@ -78,7 +95,7 @@ final class HystrixInvocationHandler implements InvocationHandler {
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
return new HystrixInvocationHandler(target, dispatch);
return new HystrixInvocationHandler(target, dispatch, null);
}
}
}

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

@ -1,26 +1,31 @@ @@ -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 class HystrixBuilderTest { @@ -61,7 +66,71 @@ public class HystrixBuilderTest {
HystrixCommand<List<String>> 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<String> 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<String> 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<String> 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<String> 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 class HystrixBuilderTest { @@ -113,7 +182,7 @@ public class HystrixBuilderTest {
TestSubscriber<List<String>> testSubscriber = new TestSubscriber<List<String>>();
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 class HystrixBuilderTest { @@ -164,7 +233,7 @@ public class HystrixBuilderTest {
TestSubscriber<List<String>> testSubscriber = new TestSubscriber<List<String>>();
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 class HystrixBuilderTest { @@ -186,7 +255,7 @@ public class HystrixBuilderTest {
List<String> list = api.getList();
assertThat(list).isNotNull().hasSize(2).contains("foo", "bar");
assertThat(list).isNotNull().containsExactly("foo", "bar");
}
private TestInterface target() {

Loading…
Cancel
Save