Once you have a layer of indirection take advantage of it! Hystrix acts as a dependency circuit breaker: it throttles the calls to the client when the client service is not healthy. However, if an external service is not responding at all or too late we should provide an alternative result, the so-called 'fallback'. You will learn in this exercise how to leverage and configure Hystrix provided features.
The task is to provide a fallback implementation that is automatically called by Hystrix when the User service returns unexpected errors (e.g. HTTP status code 500) or does not respond in a specified time.
In this exercise we will test the GetUserCommand
class, which is used internally by the UserServiceClient
class.
As the UserServiceClient
in the test is provided just as a mocked implementation (see Exercise 16), we will test directly against GetUserCommand
.
Continue with your solution of the last exercise. If this does not work, you can checkout the branch origin/solution-17-Integrate-Hystrix
.
-
In order to make outgoing request call substitutable by the test, we first need to prepare the
GetUserCommand
class: Use Eclipse refactoring tools to extract the code that does the request (ResponseEntity<User> responseEntity = restTemplate.getForEntity(url, User.class);
) from therun
method, into a protected methodsendRequest
. With that we can easily provoke long running and failing requests within the test. -
As part of the
src/test/java
source folder create a new classGetUserCommandTest
in the packagecom.sap.bulletinboard.ads.services
and copy the code from here.
In this step we provide the required fallback implementation. This is necessary because our code relies on the response of the wrapped Hystrix command. Use the tests implemented in the GetUserCommandTest
class to drive the implementation.
- Integrate the test case
responseTimedOutFallback
that is commented out. Try to understand what the test does, run the test and analyze why it fails. - Fix the failing test by overriding the
getFallback()
function within yourGetUserCommand
class. - Integrate the test case
responseErrorFallback
that is commented out. We expect that this test runs if a fallback function is implemented. - As of now it is not transparent in the log, whether the
getFallback
method is executed or not and for which reason it is called (timeout, error or full thread pool). In theGetUserCommand
class make use of the following methods:isResponseTimedOut()
,isFailedExecution()
andisResponseRejected()
to log the information respectively. When running your Unit tests again, make sure that the log messages are written in a readable way.
Note: Here we concentrate on the technical issue of providing a fallback. In practice a lot of thought has to go into the decision what exactly to return when a fallback is used. One good idea might be to return a default (unauthorized) user. Another idea is to clearly communicate errors to the end user (without provoking a long timeout).
Have also a look into the last commit of the branch solution 18 to see our sample solution.
Beside of providing a fallback implementation you also might want to overrule the default timeout setting of Hystrix (1000ms) for this command (group). We can do that programmatically and / or we can provide a configuration that is evaluated dynamically at runtime.
In order to define command specific properties, you need to remove the argument DEFAULT_TIMEOUT_MS
from the super constructor and follow the instructions to specify a HystrixCommandKey
like "User.getById":
public GetUserCommand(/*...*/) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("User"))
.andCommandKey(HystrixCommandKey.Factory.asKey("User.getById"))); //<-- new
// ...
}
You can define a command specific property programmatically. In your GetUserCommand
constructor you need to pass a Setter in a manner similar to this:
public GetUserCommand(/*...*/) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("User"))
.andCommandKey(HystrixCommandKey.Factory.asKey("User.getById"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(1500))); //<-- new
// ...
}
With that Hystrix should not run into a timeout i.e. does not return an unauthorized fallback User. Don't forget to test this code change by creating some advertisements via Postman
or by executing your tests.
You can define a command specific property dynamically, that gets loaded during runtime. Create a file named config.properties
in the package src/main/resources
with the following content:
# HystrixCommandKey = User.getById
hystrix.command.User.getById.execution.isolation.thread.timeoutInMilliseconds=2000
Note: Hystrix identifies our particular command via the HystrixCommandKey
, which is User.getById
in our case. The property value overrules the one defined in the code ("Alternative 1").
You can also define default properties for all (other) commands by specifying default
instead of User.getById
: hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
There are mainly two exceptions exposed by a HystrixCommand
:
-
HystrixRuntimeException
that is thrown when aHystrixCommand
fails. In case you raise an exception within yourrun
method this gets wrapped in aHystrixRuntimeException
with failure typeFailureType.COMMAND_EXCEPTION
and your exception as cause. Note that theHystrixRuntimeException
, raised by therun
method is not exposed by theexecute
method when the fallback succeeded. -
HystrixBadRequestException
that is the Hystrix’s equivalent ofIllegalArgumentException
. Unlike all other exceptions thrown by aHystrixCommand
this does not trigger thegetFallback
method and does not count for circuit breaking. Note thatHystrixBadRequestException
does not extendHystrixRuntimeException
.
You can make use of the HystrixBadRequestException
for all outgoing requests that return a 4xx
http status code as also implemented here
(test case responseHystrixBadRequest
).
When the same Hystrix command has different callers or is called in different contexts, the caller might wish to define a fallback that fits into its context. For providing a Java callback function we make use of lambda expressions (Java 8), which can be easily overwritten by the caller (in our case the AdvertisementController
).
The easiest way to make the Hystrix fallback configurable is to add a function parameter which returns the specified dummy User object.
- You can extend the
GetUserCommand
constructor by an additional parameterSupplier<User> fallbackFunction
, which represents aFunctionalInterface
. - Within your
GetUserCommand.getFallback()
implementation you can then call it viafallbackFunction.get()
; - On the consumer side (test) you can make use of the following lambda expressions:
this::dummyUser
- to reference a function of namedummyUser
User::new
or() -> { return new User(); }
- to return a newUser
object when called.
A Lambda example can be found in the branch solution-18-2-Make-Fallback-Configurable-using-Lambda
GetUserCommand
(GetUserCommandTest).
- Hystrix - How to use fallbacks
- Hystrix - Configuration
- Java8 Lambda Functional Programming
- Java8 Lambda Tutorial
-
© 2018 SAP SE