Skip to content

Commit 8ecb6ad

Browse files
authored
Adds FallbackFactory, allowing access to the cause of a Hystrix fallback (OpenFeign#443)
The cause of the fallback is now logged by default to FINE level. You can programmatically inspect the cause by making your own `FallbackFactory`. In many cases, the cause will be a `FeignException`, which includes the http status. Here's an example of using `FallbackFactory`: ```java // This instance will be invoked if there are errors of any kind. FallbackFactory<GitHub> fallbackFactory = cause -> (owner, repo) -> { if (cause instanceof FeignException && ((FeignException) cause).status() == 403) { return Collections.emptyList(); } else { return Arrays.asList("yogi"); } }; GitHub github = HystrixFeign.builder() ... .target(GitHub.class, "https://api.github.com", fallbackFactory); ```
1 parent baf8e25 commit 8ecb6ad

File tree

6 files changed

+278
-9
lines changed

6 files changed

+278
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 9.3
2+
* Adds `FallbackFactory`, allowing access to the cause of a Hystrix fallback
3+
14
### Version 9.2
25
* Adds Hystrix `SetterFactory` to customize group and command keys
36
* Supports context path when using Ribbon `LoadBalancingTarget`

hystrix/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,26 @@ GitHub github = HystrixFeign.builder()
106106
...
107107
.target(GitHub.class, "https://api.github.com", fallback);
108108
```
109+
110+
#### Considering the cause
111+
112+
The cause of the fallback is logged by default to FINE level. You can programmatically inspect
113+
the cause by making your own `FallbackFactory`. In many cases, the cause will be a `FeignException`,
114+
which includes the http status.
115+
116+
Here's an example of using `FallbackFactory`:
117+
118+
```java
119+
// This instance will be invoked if there are errors of any kind.
120+
FallbackFactory<GitHub> fallbackFactory = cause -> (owner, repo) -> {
121+
if (cause instanceof FeignException && ((FeignException) cause).status() == 403) {
122+
return Collections.emptyList();
123+
} else {
124+
return Arrays.asList("yogi");
125+
}
126+
};
127+
128+
GitHub github = HystrixFeign.builder()
129+
...
130+
.target(GitHub.class, "https://api.github.com", fallbackFactory);
131+
```
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package feign.hystrix;
2+
3+
import feign.FeignException;
4+
import java.util.logging.Level;
5+
import java.util.logging.Logger;
6+
7+
import static feign.Util.checkNotNull;
8+
9+
/**
10+
* Used to control the fallback given its cause.
11+
*
12+
* Ex.
13+
* <pre>{@code
14+
* // This instance will be invoked if there are errors of any kind.
15+
* FallbackFactory<GitHub> fallbackFactory = cause -> (owner, repo) -> {
16+
* if (cause instanceof FeignException && ((FeignException) cause).status() == 403) {
17+
* return Collections.emptyList();
18+
* } else {
19+
* return Arrays.asList("yogi");
20+
* }
21+
* };
22+
*
23+
* GitHub github = HystrixFeign.builder()
24+
* ...
25+
* .target(GitHub.class, "https://api.github.com", fallbackFactory);
26+
* }
27+
* </pre>
28+
*
29+
* @param <T> the feign interface type
30+
*/
31+
public interface FallbackFactory<T> {
32+
33+
/**
34+
* Returns an instance of the fallback appropriate for the given cause
35+
*
36+
* @param cause corresponds to {@link com.netflix.hystrix.AbstractCommand#getFailedExecutionException()}
37+
* often, but not always an instance of {@link FeignException}.
38+
*/
39+
T create(Throwable cause);
40+
41+
/** Returns a constant fallback after logging the cause to FINE level. */
42+
final class Default<T> implements FallbackFactory<T> {
43+
// jul to not add a dependency
44+
final Logger logger;
45+
final T constant;
46+
47+
public Default(T constant) {
48+
this(constant, Logger.getLogger(Default.class.getName()));
49+
}
50+
51+
Default(T constant, Logger logger) {
52+
this.constant = checkNotNull(constant, "fallback");
53+
this.logger = checkNotNull(logger, "logger");
54+
}
55+
56+
@Override
57+
public T create(Throwable cause) {
58+
if (logger.isLoggable(Level.FINE)) {
59+
logger.log(Level.FINE, "fallback due to: " + cause.getMessage(), cause);
60+
}
61+
return constant;
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return constant.toString();
67+
}
68+
}
69+
}

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ public Builder setterFactory(SetterFactory setterFactory) {
4646
/**
4747
* @see #target(Class, String, Object)
4848
*/
49-
public <T> T target(Target<T> target, final T fallback) {
50-
return buildWithFallback(fallback).newInstance(target);
49+
public <T> T target(Target<T> target, T fallback) {
50+
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null)
51+
.newInstance(target);
52+
}
53+
54+
/**
55+
* @see #target(Class, String, FallbackFactory)
56+
*/
57+
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
58+
return build(fallbackFactory).newInstance(target);
5159
}
5260

5361
/**
@@ -89,6 +97,14 @@ public <T> T target(Class<T> apiType, String url, T fallback) {
8997
return target(new Target.HardCodedTarget<T>(apiType, url), fallback);
9098
}
9199

100+
/**
101+
* Same as {@link #target(Class, String, T)}, except you can inspect a source exception before
102+
* creating a fallback object.
103+
*/
104+
public <T> T target(Class<T> apiType, String url, FallbackFactory<? extends T> fallbackFactory) {
105+
return target(new Target.HardCodedTarget<T>(apiType, url), fallbackFactory);
106+
}
107+
92108
@Override
93109
public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
94110
throw new UnsupportedOperationException();
@@ -102,15 +118,15 @@ public Builder contract(Contract contract) {
102118

103119
@Override
104120
public Feign build() {
105-
return buildWithFallback(null);
121+
return build(null);
106122
}
107123

108124
/** Configures components needed for hystrix integration. */
109-
Feign buildWithFallback(final Object nullableFallback) {
125+
Feign build(final FallbackFactory<?> nullableFallbackFactory) {
110126
super.invocationHandlerFactory(new InvocationHandlerFactory() {
111127
@Override public InvocationHandler create(Target target,
112128
Map<Method, MethodHandler> dispatch) {
113-
return new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallback);
129+
return new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallbackFactory);
114130
}
115131
});
116132
super.contract(new HystrixDelegatingContract(contract));

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ final class HystrixInvocationHandler implements InvocationHandler {
3838

3939
private final Target<?> target;
4040
private final Map<Method, MethodHandler> dispatch;
41-
private final Object fallback; // Nullable
41+
private final FallbackFactory<?> fallbackFactory; // Nullable
4242
private final Map<Method, Method> fallbackMethodMap;
4343
private final Map<Method, Setter> setterMethodMap;
4444

4545
HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch,
46-
SetterFactory setterFactory, Object fallback) {
46+
SetterFactory setterFactory, FallbackFactory<?> fallbackFactory) {
4747
this.target = checkNotNull(target, "target");
4848
this.dispatch = checkNotNull(dispatch, "dispatch");
49-
this.fallback = fallback;
49+
this.fallbackFactory = fallbackFactory;
5050
this.fallbackMethodMap = toFallbackMethod(dispatch);
5151
this.setterMethodMap = toSetters(setterFactory, target, dispatch.keySet());
5252
}
@@ -115,10 +115,11 @@ protected Object run() throws Exception {
115115

116116
@Override
117117
protected Object getFallback() {
118-
if (fallback == null) {
118+
if (fallbackFactory == null) {
119119
return super.getFallback();
120120
}
121121
try {
122+
Object fallback = fallbackFactory.create(getFailedExecutionException());
122123
Object result = fallbackMethodMap.get(method).invoke(fallback, args);
123124
if (isReturnsHystrixCommand(method)) {
124125
return ((HystrixCommand) result).execute();
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package feign.hystrix;
2+
3+
import feign.FeignException;
4+
import feign.RequestLine;
5+
import java.util.concurrent.atomic.AtomicBoolean;
6+
import java.util.logging.Level;
7+
import java.util.logging.Logger;
8+
import okhttp3.mockwebserver.MockResponse;
9+
import okhttp3.mockwebserver.MockWebServer;
10+
import org.junit.Rule;
11+
import org.junit.Test;
12+
import org.junit.rules.ExpectedException;
13+
14+
import static feign.assertj.MockWebServerAssertions.assertThat;
15+
16+
public class FallbackFactoryTest {
17+
18+
interface TestInterface {
19+
@RequestLine("POST /") String invoke();
20+
}
21+
22+
@Rule
23+
public final ExpectedException thrown = ExpectedException.none();
24+
@Rule
25+
public final MockWebServer server = new MockWebServer();
26+
27+
@Test
28+
public void fallbackFactory_example_lambda() {
29+
server.enqueue(new MockResponse().setResponseCode(500));
30+
server.enqueue(new MockResponse().setResponseCode(404));
31+
32+
TestInterface api = target(cause -> () -> {
33+
assertThat(cause).isInstanceOf(FeignException.class);
34+
return ((FeignException) cause).status() == 500 ? "foo" : "bar";
35+
});
36+
37+
assertThat(api.invoke()).isEqualTo("foo");
38+
assertThat(api.invoke()).isEqualTo("bar");
39+
}
40+
41+
static class FallbackApiWithCtor implements TestInterface {
42+
final Throwable cause;
43+
44+
FallbackApiWithCtor(Throwable cause) {
45+
this.cause = cause;
46+
}
47+
48+
@Override public String invoke() {
49+
return "foo";
50+
}
51+
}
52+
53+
@Test
54+
public void fallbackFactory_example_ctor() {
55+
server.enqueue(new MockResponse().setResponseCode(500));
56+
57+
// method reference
58+
TestInterface api = target(FallbackApiWithCtor::new);
59+
60+
assertThat(api.invoke()).isEqualTo("foo");
61+
62+
server.enqueue(new MockResponse().setResponseCode(500));
63+
64+
// lambda factory
65+
api = target(throwable -> new FallbackApiWithCtor(throwable));
66+
67+
server.enqueue(new MockResponse().setResponseCode(500));
68+
69+
// old school
70+
api = target(new FallbackFactory<TestInterface>() {
71+
@Override public TestInterface create(Throwable cause) {
72+
return new FallbackApiWithCtor(cause);
73+
}
74+
});
75+
76+
assertThat(api.invoke()).isEqualTo("foo");
77+
}
78+
79+
// retrofit so people don't have to track 2 classes
80+
static class FallbackApiRetro implements TestInterface, FallbackFactory<FallbackApiRetro> {
81+
82+
@Override public FallbackApiRetro create(Throwable cause) {
83+
return new FallbackApiRetro(cause);
84+
}
85+
86+
final Throwable cause; // nullable
87+
88+
public FallbackApiRetro() {
89+
this(null);
90+
}
91+
92+
FallbackApiRetro(Throwable cause) {
93+
this.cause = cause;
94+
}
95+
96+
@Override public String invoke() {
97+
return cause != null ? cause.getMessage() : "foo";
98+
}
99+
}
100+
101+
@Test
102+
public void fallbackFactory_example_retro() {
103+
server.enqueue(new MockResponse().setResponseCode(500));
104+
105+
// method reference
106+
TestInterface api = target(new FallbackApiRetro());
107+
108+
assertThat(api.invoke()).isEqualTo("status 500 reading TestInterface#invoke()");
109+
}
110+
111+
@Test
112+
public void defaultFallbackFactory_delegates() {
113+
server.enqueue(new MockResponse().setResponseCode(500));
114+
115+
TestInterface api = target(new FallbackFactory.Default<>(() -> "foo"));
116+
117+
assertThat(api.invoke())
118+
.isEqualTo("foo");
119+
}
120+
121+
@Test
122+
public void defaultFallbackFactory_doesntLogByDefault() {
123+
server.enqueue(new MockResponse().setResponseCode(500));
124+
125+
Logger logger = new Logger("", null) {
126+
@Override public void log(Level level, String msg, Throwable thrown) {
127+
throw new AssertionError("logged eventhough not FINE level");
128+
}
129+
};
130+
131+
target(new FallbackFactory.Default<>(() -> "foo", logger)).invoke();
132+
}
133+
134+
@Test
135+
public void defaultFallbackFactory_logsAtFineLevel() {
136+
server.enqueue(new MockResponse().setResponseCode(500));
137+
138+
AtomicBoolean logged = new AtomicBoolean();
139+
Logger logger = new Logger("", null) {
140+
@Override public void log(Level level, String msg, Throwable thrown) {
141+
logged.set(true);
142+
143+
assertThat(msg).isEqualTo("fallback due to: status 500 reading TestInterface#invoke()");
144+
assertThat(thrown).isInstanceOf(FeignException.class);
145+
}
146+
};
147+
logger.setLevel(Level.FINE);
148+
149+
target(new FallbackFactory.Default<>(() -> "foo", logger)).invoke();
150+
assertThat(logged.get()).isTrue();
151+
}
152+
153+
TestInterface target(FallbackFactory<? extends TestInterface> factory) {
154+
return HystrixFeign.builder()
155+
.target(TestInterface.class, "http://localhost:" + server.getPort(), factory);
156+
}
157+
}

0 commit comments

Comments
 (0)