Consul JAX RS is a Java library that provides a Consul-backed service discovery framework for JAX RS-based clients. Using a provided JAX RS client implementation and a Consul client, this service will maintain a pool of available Jersey clients, with the ability to revoke a client for a period of time, removing it from the selection process.
The design goal behind this library is to allow services that use a JAX RS-based client structure to utilise client-side service discovery and fault tolerance handling in combination with the Consul system.
The library also provides a basic retry framework, allowing requests to be retried and revoking client instances on errors appropriately. However, if you want to use a more robust solution, then a library like Failsafe might be more suitable.
While this library proved useful in the past for a consumer-based load balancing, more traditional load balancing techniques and technologies utilising Consul proved to be more robust and consistently managable.
- Removed the
RequestFailureException
error wrapper used by the retry functionality. The original error will now be returned. - Converted the retry functionality to use RxJava, allowing the creating of
Observables
around fault-tolerant retry loops. - Added backoff retry interval functionality.
<dependency>
<groupId>net.ozwolf</groupId>
<artifactId>consul-jaxrs</artifactId>
<version>2.0.0</version>
</dependency>
compile 'net.ozwolf:consul-jaxrs:2.0.0'
The following dependencies need to be provided by the consumer of the library:
- JAX RS Client - Any implementation of the JAX RS framework, such as the Jersey2 library.
- SLF4J Implementation - Any implementation of the SLF4J logging framework, such as the QOS Logback library.
This library utilises the Consul Client library provided by Orbitz Worldwilde.
When creating a new pool, an instantiated instance of the Consul
class needs to be provided, appropriately configured to work against your own Consul ecosystem.
The base class of this library is the ConsulJaxRsClientPool
object. This object needs to be provided three things:
serviceId
- the consul-registered service ID the pool instance will be related to. You can create aConsulJaxRsClientPool
per service you wish to use instances of.client
- Any JAX RS client implementation.consul
- An instantiated instance of theConsul
object configured to work with your Consul ecosystem.
Client client = new JerseyClientBuilder().withConfig(new ClientConfig(JacksonJaxbJsonProvider.class)).build();
Consul consul = Consul.builder().withHostAndPort(HostAndPort.fromParts("consul.local", 8500)).build();
ConsulJaxRsClientPool pool = new ConsulJaxRsClientPool("my-service", client, consul);
The client pool uses a ServiceHealthCache
to monitor the state of the service instances behind the scenes. By default, the pool will create it's own default service health cache that will retrieve all instances (including critical instances) and a default service health key. For example:
ServiceHealthCache cache = ServiceHealthCache.newCache(consul.healthClient(), serviceId, false, CatalogOptions.BLANK, pollRate)
The pool also provides an option to provide an implementation of the ServiceHealthCacheProvider
interface to create your own cache, allowing for more custom cache creation when needed (eg. if wanting to customise the ServiceHealthKey
implementation).
To get a client from the pool, simply call the .next()
method. If no state is provided, the pool will automatically try and randomly select an instance that isn't revoked and either has a PASS
or WARN
state (see below for further information).
The returned client is an implementation of
ConsulJaxRsClient client = pool.next();
try {
return client.target("/path/to/something")
.request()
.get(String.class);
} catch (ServerException e) {
client.revoke(5, TimeUnit.MINUTES);
}
The above example will randomly provide the next instance client for use. If the request receives a server exception (ie. 5xx
response code), we will revoke that instance for 5 minutes, effectively removing it as an available instance in the pool.
This library makes no attempt to implement RxJava functionality around singular uses of Client
instances as you can adhere to standard RxJava functionality. For example:
return Observable.fromCallable(() -> {
ConsulJaxRsClient client = pool.next();
try {
return client.target("/path/to/something")
.request()
.get(String.class);
} catch (ServerException | IOException e) {
client.revoke(5, TimeUnit.MINUTES);
throw e;
}
});
The above code will run a single client call with revoke functionality within an Observable block.
If you wish to use the JAX-RS asynchronous features directly with RxJava, you can use the Observable.from(Future)
method to create your observable.
The library provides an option to undertake retries against instances of services. The idea behind this is to allow fault tolerance when instances may not be available and re-attempt requests. This follows the simple premise of fluently describing on what exceptions to revoke a client instance (eg. server errors or network errors) and on what exceptions to break the retry loop on (eg. client errors).
The retry handler accepts a RetryAction
implementation that can be executed immediately or returned as an Observable
.
The retry handler uses a backing-off retry policy. The defaults for this are an initial delay of 100ms with a 2.0 backoff factor.
This can modified with the .
Immediate execution effectively runs Observable.toBlocking().single()
straight away, forcing the observable to complete on the current thread.
return pool.retry(3)
.revokeOn(ServerException.class, 5, TimeUnit.MINUTES)
.revokeOn(IOException.class, 5, TimeUnit.MINUTES)
.breakOn(ClientException.class)
.execute(c ->
c.target("/path/to/something")
.request()
.get(String.class)
);
In the above example, we have directed the pool to retry our request 3 times. If we receive a server exception (ie. 5xx
response code) or something indicating a network exception, we will revoke that instance, effectively providing a new client instance on the next attempt.
If we receive a client exception (ie. 4xx
response code), we want the request to not retry and break from the loop (as it kind of means the error is on our end anyway).
Observable<String> observable = pool.retry(3)
.revokeOn(ServerException.class, 5, TimeUnit.MINUTES)
.revokeOn(IOException.class, 5, TimeUnit.MINUTES)
.breakOn(ClientException.class)
.observe(c ->
c.target("/path/to/something")
.request()
.get(String.class)
);
MySubscriber subscriber = new MySubscriber();
Subscription subscription = observable.subscribe(subscriber);
...
The setup for the observable is the same as above with regards to what will cause a revoke on a client and a break in the retry. This response though will return a standard RxJava Observable
instance of your return type.
Below are particular details around instance clients and their states, behaviour and how they are selected.
The service instance clients all use the provided JAX RS client instance assigned to the pool as their delegate client implementation.
This means any ConsulJaxRsClient will use the configuration of the provided base class, including timeout, keep alive and retry policies. It will also mean any registered request or response filters will still apply.
The primary purpose of the service instance client is to provide the scheme, host and port of the request mapped to the instance provided from Consul.
Instance clients maintain a state of either PASS
, WARN
or FAIL
as defined by the State
class from the consul client library. This state is derived using the following rules:
PASS
- all health checks associated to the service instance are passingFAIL
- all health checks associated to the service instance are failingWARN
- there are no health checks available, or the health checks are a mixture ofPASS
,WARN
andFAIL
.
Instance clients are selected using a combination of a weight random lottery and whether or not they are revoked.
The client pool can be provided with a fixed URI for it to fallback to if no service instances have been registered with Consul. This URI could be a direct reference to a singular instance or might point at a more rudimentary load-balanced
An instance client's weighting in the random lottery is based on their last reported state. These weightings can be adjusted from the default using the withStateWeightingOf(...)
method.
Below are the default weightings:
PASS
-1.0
WARN
-0.5
FAIL
-0.1
This means that while other states can be randomly chosen, your service is more likely to get a instance higher up the health chain.
When using the .next()
or .retry()
methods on the pool, by default it will try and select instances that either have a PASS
or WARN
state. This can be overridden by providing a state parameter to these methods.
It is possible for a process to reject a service instance when it fails expected tolerance rules (ie. if client calls wrapped in Hystrix aren't responding in time and causing timeouts). Revoked instance clients will be excluded from the selection process with one caveat.
If there are no available services with the desired minimum state that haven't been revoked, the revoked service instances will be re-introduced for that selection only. This means that revoked statuses remain active and if an instance that is not revoked becomes available again, revoked instances will once again be ignored.