Yet Another Try
Features
- Configure:
- max number of attempts
- timeout
- delay between attempts
- which exceptions should be retried and which should not.
- whenever or not use the invocation thread for the first attempt
- Implements plain java ExecutorService, therefore it is fully compatible with the code, which uses ExecutorService directly.
- Uses CompletableFuture as a return type.
- Has both asynchronous and synchronous versions.
- Collects statistics about successful and failed attempts if requested.
Dependencies and prerequisites
The library requires Java 8+. Use the following code snippets to add the library to your project:
- Gradle
dependencies {
compile "com.github.sorokinigor:yet-another-try:1.1.0"
}
repositories {
mavenCentral()
}
- Maven
<dependency>
<groupId>com.github.sorokinigor</groupId>
<artifactId>yet-another-try</artifactId>
<version>1.1.0</version>
</dependency>
Usage
The main entry point is Retry utility class.
/*
Uses the current thread for the first attempt
and passed ScheduledExecutorService for the subsequent attempts,
does not retry on malformed request.
*/
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.retryOnce()
.runFirstAttemptInInvocationThread()
.terminateOn(IllegalArgumentException.class)
.terminateOn(HttpGenericException.class, e -> e.statusCode() == 400)
.build();
CompletableFuture<String> future = executor.submit(() -> faultyResource("malformedRequest"));
future.whenComplete((response, exception) -> System.out.println(
"Response '" + response + "', exception '" + exception + "'."
));
//Uses default lazy singleton instance of AsyncRetryExecutor
Retry.async()
.submit(() -> faultyResource("request"))
.thenAccept(response -> System.out.println("Response is '" + response + "'."));
/*
Uses the current thread for task invocation.
Tries 2 times with fixed rate between attempts.
*/
SyncRetryExecutor syncExecutor = Retry.sync()
.maxAttempts(2)
.backOff(Backoffs.fixedRate(1L, TimeUnit.SECONDS))
.build();
String response = syncExecutor.execute(() -> faultyResource("syncRequest"));
/*
Shortcut for ad hoc synchronous execution.
Completes with exception on timeout.
*/
String result = Retry.sync()
.timeout(5L, TimeUnit.SECONDS)
.execute(() -> faultyResource("adhoc request"));
Asynchronous
Any arbitrary ScheduledExecutorService should be passed in order to use asynchronous executor. Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.maxAttempts(3)
.timeout(10, TimeUnit.SECONDS)
.backOff(Backoffs.fixedDelay(1L, TimeUnit.SECONDS))
.retryOn(NotYetConnectedException.class)
.terminateOn(NullPointerException.class)
.build();
CompletableFuture<Integer> result = executor.submit(() -> {
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("music.yandex.ru", 80))) {
socket.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(10);
return socket.read(buffer);
}
})
.thenApply(numberOfBytesRead -> numberOfBytesRead / 2);
Please note that by default AsyncRetryExecutor manages the lifecycle of the passed ScheduledExecutorService. Consequently, the AsyncRetryExecutor will shutdown underlying ScheduledExecutorService. If you want to prevent it, use:
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
AsyncRetryExecutor executor = Retry.async(executorService)
.doNotShutdownExecutors()
.build();
Timeout
This code snippet shows how you can specify a timeout for a task:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.timeout(10L, TimeUnit.SECONDS)
.build();
After the timeout is expired, the result CompletableFuture is completed with TimeoutException. Since then, there would be no retries of the task.
By default, the same executor is used for both task execution and timeout handling, but it is configurable:
ScheduledExecutorService taskExecutor = Executors.newSingleThreadScheduledExecutor();
ScheduledExecutorService timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
AsyncRetryExecutor executor = Retry.async(taskExecutor)
.timeout(5L, TimeUnit.SECONDS)
.timeoutExecutorService(timeoutExecutor)
.build();
Delay
The library itself contains exponential (default and preferable), fixed delay and fixed rate backoffs for delay calculation. But, feel free to implement your own backoff strategy, as the Backoff interface is a part of the public API.
In order to instantiate the built-in backoff strategies use Backoffs utility class.
Exponential
The delay is exponentially increases until it reaches the upper bound for the delay or the number of attempts. After the calculation of the exponential backoff, it also adds an additional random delay based on the passed random factor. For instance, 0.2
adds up to 20%
delay. Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.backOff(Backoffs.exponential(3L, 30L, TimeUnit.SECONDS, 0.2D))
.build();
Fixed delay
It always uses the same delay for each attempt. Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.backOff(Backoffs.fixedDelay(1L, TimeUnit.SECONDS))
.build();
Fixed rate
It subtracts the task execution time from the delay. If the execution time is greater than or equal the delay, the delay is 0. Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.backOff(Backoffs.fixedRate(1L, TimeUnit.SECONDS))
.build();
Exceptions
The library provides the ability to retry only specific type exception and the exception matching the predicate. Also, it is possible to configure to stop retrying after a specific exception (by type or predicate too). Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.retryOn(SocketException.class)
.retryOn(HttpGenericException.class, e -> e.statusCode() == 500)
.terminateOn(IllegalStateException.class)
.terminateOn(HttpGenericException.class, e -> e.statusCode() == 400)
.terminatePredicate(e -> e instanceof BindException && e.getMessage().contains("in use"))
.build();
Notice that the task is retried only if:
- Any of the retry predicates returns
true
or you didn't specify any (in that case there is a default retry predicate, which always returnstrue
). - None of the terminate predicates returns
true
or you didn't specify any (in that case there is a default terminate predicate, which always returnsfalse
).
Default executor
A default lazy singleton instance of asynchronous executor is available via Retry.async() method. Example:
CompletableFuture<String> future = Retry.async()
.submit(() -> faultyResource("request"));
It is lazily instantiated on first usage and creates a shutdown hook for the internal ScheduledExecutorService shutting down.
Statistics
There is a simple wrapper for the asynchronous executor, which collects the number of failed attempts and the number of successful and failed tasks. Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.maxAttempts(2)
.build();
StatisticsExecutorService statsExecutor = Retry.gatherStatisticFor(executor);
CompletableFuture<String> successful = statsExecutor.submit(() -> "successful");
CompletableFuture<String> failed = statsExecutor.submit(() -> { throw new Exception(); });
successful.thenAcceptBoth(failed, (ignored1, ignored2) -> {})
.whenComplete((ignored, ignoredException) -> {
System.out.println(statsExecutor.stats());
//Stats{successful=1, failed=1, failedAttempts=2}
});
Synchronous
The synchronous executor does not use any thread pool, instead, it uses the current thread for task execution. It has approximately the same configuration as asynchronous one, except the settings related to ScheduledExecutorService. Example:
SyncRetryExecutor executor = Retry.sync()
.maxAttempts(3)
.timeout(10, TimeUnit.SECONDS)
.backOff(Backoffs.fixedDelay(1L, TimeUnit.SECONDS))
.retryOn(NotYetConnectedException.class)
.terminateOn(NullPointerException.class)
.build();
int numberOfBytesRead = executor.execute(() -> {
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("music.yandex.ru", 80))) {
socket.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(10);
return socket.read(buffer);
}
});
If you do not want to store a reference to the executor, you can use a shortcut:
String response = Retry.sync()
.withoutDelay()
.terminateOn(IllegalArgumentException.class)
.terminateOn(UnsupportedOperationException.class)
.execute(() -> faultyResource("request"));