Riptide: Failsafe adds Failsafe support to Riptide. It offers retries and a circuit breaker to every remote call.
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(circuitBreaker)
.withPolicy(new RetryRequestPolicy(retryPolicy)))
.build();
- seamlessly integrates Riptide with Failsafe
- Riptide Core
- Failsafe
Add the following dependency to your project:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>riptide-failsafe</artifactId>
<version>${riptide.version}</version>
</dependency>
The failsafe plugin will not perform retries nor apply circuit breakers unless they were explicitly configured:
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(
new RetryRequestPolicy(
RetryPolicy.<ClientHttpResponse>builder()
.withDelay(Duration.ofMillis(25))
.withDelayFn(new RetryAfterDelayFunction(clock))
.withMaxRetries(4)
.build())
.withListener(myRetryListener))
.withPolicy(
CircuitBreaker.<ClientHttpResponse>builder()
.withFailureThreshold(3, 10)
.withSuccessThreshold(5)
.withDelay(Duration.ofMinutes(1))
.build()))
.build();
Please visit the Failsafe readme in order to see possible configurations.
Beware when using retryOn
to retry conditionally on certain exception types.
You'll need to register RetryException
in order for the retry()
route to work:
RetryPolicy.<ClientHttpResponse>builder()
.handle(SocketTimeoutException.class)
.handle(RetryException.class)
.build();
By default, you can use RetryException in your routes to retry the request:
retryClient.get()
.dispatch(
series(), on(CLIENT_ERROR).call(
response -> {
if (specificCondition(response)) {
throw new RetryException(response); // we will retry this one
} else {
throw new AnyOtherException(response); // we wont retry this one
}
}
)
).join()
Failsafe supports dynamically computed delays using a custom function.
Riptide: Failsafe offers implementations that understand:
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(RetryPolicy.<ClientHttpResponse>builder()
.withDelayFn(new CompositeDelayFunction<>(Arrays.asList(
new RetryAfterDelayFunction(clock),
new RateLimitResetDelayFunction(clock)
)))
.withMaxDuration(Duration.ofSeconds(5))
.build()))
.build();
Make sure you check out zalando/failsafe-actuator for a seamless integration of Failsafe and Spring Boot.
You can use org.springframework.http.client.ClientHttpRequestFactory
configuration to set up proper
connection timeout, socket timeout and connection time to live.
In addition you can use FailsafePlugin
with dev.failsafe.Timeout
policy to control the entire duration
from sending the request to processing the response. See the use cases in the FailsafePluginTimeoutTest
test.
Configuration example:
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(Timeout.of(Duration.ofSeconds(5))))
.build();
The BackupRequest
policy implements the backup request pattern, also known as hedged requests:
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(new BackupRequest(1, SECONDS)))
.build();
The withExecutor
method allows to specify a custom ExecutorService
being used to perform asynchronous executions and listen for callbacks:
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(
CircuitBreaker.<ClientHttpResponse>builder()
.withFailureThreshold(3, 10)
.withSuccessThreshold(5)
.withDelay(Duration.ofMinutes(1))
.build())
.withExecutor(Executors.newFixedThreadPool(2)))
.build();
If no executor is specified, the default executor configured by Failsafe
is used. See Failsafe DelegatingScheduler class,
and also Failsafe documentation for more information.
Beware when specifying a custom ExecutorService
:
- The
ExecutorService
should have a core pool size or parallelism of at least 2 in order for timeouts to work - In general, it is not recommended to specify the same
ExecutorService
for multipleHttp
clients
Given the failsafe plugin was configured as shown in the last section: A regular call like the following will now be retried up to 4 times if the server did not respond within the socket timeout.
http.get("/users/me")
.dispatch(series(),
on(SUCCESSFUL).call(User.class, this::greet),
anySeries().call(problemHandling()))
Handling certain technical issues automatically, like socket timeouts, is quite useful.
But there might be cases where the server did respond, but the response indicates something that is worth
retrying, e.g. a 409 Conflict
or a 503 Service Unavailable
. Use the predefined retry
route that comes with the
failsafe plugin:
http.get("/users/me")
.dispatch(series(),
on(SUCCESSFUL).call(User.class, this::greet),
on(CLIENT_ERROR).dispatch(status(),
on(CONFLICT).call(retry())),
on(SERVER_ERROR).dispatch(status(),
on(SERVICE_UNAVAILABLE).call(retry())),
anySeries().call(problemHandling()))
Only safe and idempontent methods are retried by default. The following request methods can be detected:
- Standard HTTP method
- HTTP method override
- Conditional Requests
Idempotency-Key
header
You also have the option to declare any request to be idempotent
by setting the respective request attribute. This is
useful in situation where none of the options are above would detect it but based on the contract of the API you may know
that a certain operation is in fact idempotent.
http.post("/subscriptions/{id}/cursors", subscriptionId)
.attribute(MethodDetector.IDEMPOTENT, true)
.header("X-Nakadi-StreamId", streamId)
.body(cursors)
.dispatch(series(),
on(SUCCESSFUL).call(pass()),
anySeries().call(problemHandling()))
In case those options are insufficient you may specify your own method detector:
Http.builder().requestFactory(new HttpComponentsClientHttpRequestFactory())
.plugin(new FailsafePlugin()
.withPolicy(retryPolicy)
.withDecorator(new CustomIdempotentMethodDetector()))
.build();
If you have questions, concerns, bug reports, etc., please file an issue in this repository's Issue Tracker.
To contribute, simply open a pull request and add a brief description (1-2 sentences) of your addition or change. For more details, check the contribution guidelines.