Skip to content

Commit

Permalink
Merge pull request ReactiveX#199 from pwlan/master
Browse files Browse the repository at this point in the history
Initial suggestion for a feign <-> resilience4j module
  • Loading branch information
storozhukBM authored Nov 25, 2018
2 parents c32fe1f + 81d8b95 commit e40660c
Show file tree
Hide file tree
Showing 18 changed files with 1,357 additions and 1 deletion.
7 changes: 6 additions & 1 deletion libraries.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ext {
ratpackVersion = '1.5.4'
spockVersion = '1.1-groovy-2.4-rc-4'
retrofitVersion = '2.3.0'
feignVersion = '10.0.1'
prometheusSimpleClientVersion = '0.3.0'
reactorVersion = '3.1.5.RELEASE'
reactiveStreamsVersion = '1.0.2'
Expand Down Expand Up @@ -86,7 +87,11 @@ ext {
retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}",
retrofit_test: "com.squareup.retrofit2:converter-scalars:${retrofitVersion}",
retrofit_wiremock: "com.github.tomakehurst:wiremock:1.58",


// Feign addon
feign: "io.github.openfeign:feign-core:${feignVersion}",
feign_wiremock: "com.github.tomakehurst:wiremock:1.58",

// Metrics addon
metrics: "io.dropwizard.metrics:metrics-core:${metricsVersion}",

Expand Down
104 changes: 104 additions & 0 deletions resilience4j-feign/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
= resilience4j-feign

Resilience4j decorators for https://github.com/OpenFeign/feign[feign].
Similar to https://github.com/OpenFeign/feign/tree/master/hystrix[HystrixFeign],
resilience4j-feign makes it easy to incorporate "fault tolerance" patterns into the feign framework, such as
the CircuitBreaker and RateLimiter.


== Current Features
* CircuitBreaker
* RateLimiter
* Fallback


== Decorating Feign Interfaces

The `Resilience4jFeign.builder` is the main class for creating fault tolerance instances of feign.
It extends the `Feign.builder` and can be configured in the same way with the exception of adding a custom
`InvocationHandlerFactory`. Resilience4jFeign uses its own `InvocationHandlerFactory` to apply the decorators.
Decorators can be built using the `FeignDecorators` class. Multiple decorators can be combined

The following example shows how to decorate a feign interface with a RateLimiter and CircuitBreaker:
``` java
public interface MyService {
@RequestLine("GET /greeting")
String getGreeting();

@RequestLine("POST /greeting")
String createGreeting();
}

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
RateLimiter rateLimiter = RateLimiter.ofDefaults("backendName");
FeignDecorators decorators = FeignDecorators.builder()
.withRateLimiter(rateLimiter)
.withCircuitBreaker(circuitBreaker)
.build();
MyService myService = Resilience4jFeign.builder(decorators).target(MyService.class, "http://localhost:8080/");
```

Calling any method of the `MyService` instance will invoke a CircuitBreaker and then a RateLimiter.
If one of these mechanisms take affect, then the corresponding RuntimeException will be thrown, for example, `CircuitBreakerOpenException` or `RequestNotPermitted` (Hint: These do not extend the `FeignException` class).

The following diagram illustrates how the decorators are stacked:
image::feign-decorators.png[]


== Ordering of Decorators
The order in which decorators are applied correspond to the order in which they are declared.
When building `FeignDecorators`, it is important to be wary of this, since the order affects the resulting behavior.

For example,
``` java
FeignDecorators decoratorsA = FeignDecorators.builder()
.withCircuitBreaker(circuitBreaker)
.withRateLimiter(rateLimiter)
.build();

FeignDecorators decoratorsB = FeignDecorators.builder()
.withRateLimiter(rateLimiter)
.withCircuitBreaker(circuitBreaker)
.build();
```

With `decoratorsA` the RateLimiter will be called before the CircuitBreaker. That means that even if the CircuitBreaker is open, the RateLimiter will still limit the rate of calls.
`decoratorsB` applies the reserve order. Meaning that once the CircuitBreaker is open, the RateLimiter will no longer be in affect.


== Fallback
Fallbacks can be defined that are called when Exceptions are thrown. Exceptions can occur when the HTTP request fails, but also when one of the `FeignDecorators` activates, for example, the CircuitBreaker.

``` java
public interface MyService {
@RequestLine("GET /greeting")
String greeting();
}

MyService requestFailedFallback = () -> "fallback greeting";
MyService circuitBreakerFallback = () -> "CircuitBreaker is open!";
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
FeignDecorators decorators = FeignDecorators.builder()
.withFallback(requestFailedFallback, FeignException.class)
.withFallback(circuitBreakerFallback, CircuitBreakerOpenException.class)
.build();
MyService myService = Resilience4jFeign.builder(decorators).target(MyService.class, "http://localhost:8080/", fallback);
```
In this example, the `requestFailedFallback` is called when a `FeignException` is thrown (usually when the HTTP request fails), whereas
the `circuitBreakerFallback` is only called in the case of a `CircuitBreakerOpenException`.
Check the `FeignDecorators` class for more ways to filter fallbacks.

All fallbacks must implement the same interface that is declared in the "target" (Resilience4jFeign.Builder#target) method, otherwise an IllegalArgumentException will be thrown.
Multiple fallbacks can be assigned to handle the same Exception with the next fallback being called when the previous one fails.



== License

Copyright 2018

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
6 changes: 6 additions & 0 deletions resilience4j-feign/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies {
compile ( libraries.feign )
compile project(':resilience4j-circuitbreaker')
compile project(':resilience4j-ratelimiter')
testCompile ( libraries.feign_wiremock )
}
Binary file added resilience4j-feign/feign-decorators.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
*
* Copyright 2018
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*
*/
package io.github.resilience4j.feign;

import static feign.Util.checkNotNull;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
import io.vavr.CheckedFunction1;

/**
* An instance of {@link InvocationHandler} that uses {@link FeignDecorator}s to enhance the
* invocations of methods.
*/
class DecoratorInvocationHandler implements InvocationHandler {

private final Target<?> target;
private final Map<Method, CheckedFunction1<Object[], Object>> decoratedDispatch;

public DecoratorInvocationHandler(Target<?> target,
Map<Method, MethodHandler> dispatch,
FeignDecorator invocationDecorator) {
this.target = checkNotNull(target, "target");
checkNotNull(dispatch, "dispatch");
this.decoratedDispatch = decorateMethodHandlers(dispatch, invocationDecorator, target);
}

/**
* Applies the specified {@link FeignDecorator} to all specified {@link MethodHandler}s and
* returns the result as a map of {@link CheckedFunction1}s. Invoking a {@link CheckedFunction1}
* will therefore invoke the decorator which, in turn, may invoke the corresponding
* {@link MethodHandler}.
*
* @param dispatch a map of the methods from the feign interface to the {@link MethodHandler}s.
* @param invocationDecorator the {@link FeignDecorator} with which to decorate the
* {@link MethodHandler}s.
* @param target the target feign interface.
* @return a new map where the {@link MethodHandler}s are decorated with the
* {@link FeignDecorator}.
*/
private Map<Method, CheckedFunction1<Object[], Object>> decorateMethodHandlers(Map<Method, MethodHandler> dispatch,
FeignDecorator invocationDecorator, Target<?> target) {
final Map<Method, CheckedFunction1<Object[], Object>> map = new HashMap<>();
for (final Map.Entry<Method, MethodHandler> entry : dispatch.entrySet()) {
final Method method = entry.getKey();
final MethodHandler methodHandler = entry.getValue();
map.put(method, invocationDecorator.decorate(methodHandler::invoke, method, methodHandler, target));
}
return map;
}

@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
switch (method.getName()) {
case "equals":
return equals(args.length > 0 ? args[0] : null);

case "hashCode":
return hashCode();

case "toString":
return toString();

default:
break;
}

return decoratedDispatch.get(method).apply(args);
}

@Override
public boolean equals(Object obj) {
Object compareTo = obj;
if (compareTo == null) {
return false;
}
if (Proxy.isProxyClass(compareTo.getClass())) {
compareTo = Proxy.getInvocationHandler(compareTo);
}
if (compareTo instanceof DecoratorInvocationHandler) {
final DecoratorInvocationHandler other = (DecoratorInvocationHandler) compareTo;
return target.equals(other.target);
}
return false;
}

@Override
public int hashCode() {
return target.hashCode();
}

@Override
public String toString() {
return target.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
*
* Copyright 2018
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*
*/
package io.github.resilience4j.feign;

import static java.util.Objects.requireNonNull;

import java.lang.reflect.Method;
import java.util.function.Predicate;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
import io.vavr.CheckedFunction1;

/**
* Decorator that calls a fallback in the case that an exception is thrown.
*/
class FallbackDecorator<T> implements FeignDecorator {

private final T fallback;
private Predicate<Exception> filter;

/**
* Creates a fallback that will be called for every {@link Exception}.
*/
public FallbackDecorator(T fallback) {
this(fallback, ex -> true);
}

/**
* Creates a fallback that will only be called for the specified {@link Exception}.
*/
public FallbackDecorator(T fallback, Class<? extends Exception> filter) {
this(fallback, filter::isInstance);
requireNonNull(filter, "Filter cannot be null!");
}

/**
* Creates a fallback that will only be called if the specified {@link Predicate} returns
* <code>true</code>.
*/
public FallbackDecorator(T fallback, Predicate<Exception> filter) {
this.fallback = requireNonNull(fallback, "Fallback cannot be null!");
this.filter = requireNonNull(filter, "Filter cannot be null!");
}

/**
* Calls the fallback if the invocationCall throws an {@link Exception}.
*
* @throws IllegalArgumentException if the fallback object does not have a corresponding
* fallback method.
*/
@Override
public CheckedFunction1<Object[], Object> decorate(CheckedFunction1<Object[], Object> invocationCall,
Method method,
MethodHandler methodHandler,
Target<?> target) {
final Method fallbackMethod;
validateFallback(method);
fallbackMethod = getFallbackMethod(method);
return args -> {
try {
return invocationCall.apply(args);
} catch (final Exception exception) {
if (filter.test(exception)) {
return fallbackMethod.invoke(fallback, args);
}
throw exception;
}
};
}

private void validateFallback(Method method) {
if (fallback.getClass().isAssignableFrom(method.getDeclaringClass())) {
throw new IllegalArgumentException("Cannot use the fallback ["
+ fallback.getClass() + "] for ["
+ method.getDeclaringClass() + "]!");
}
}

private Method getFallbackMethod(Method method) {
Method fallbackMethod;
try {
fallbackMethod = fallback.getClass().getMethod(method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException | SecurityException e) {
throw new IllegalArgumentException("Cannot use the fallback ["
+ fallback.getClass() + "] for ["
+ method.getDeclaringClass() + "]", e);
}
return fallbackMethod;
}

}
Loading

0 comments on commit e40660c

Please sign in to comment.