具有可配置策略的异步重试执行程序。
The library requires Java 8+. Use the following code snippets to add the library to your project:
repositories {
mavenCentral()
}
* Maven
```xml
<dependency>
<groupId>com.github.sorokinigor</groupId>
<artifactId>yet-another-try</artifactId>
<version>1.1.0</version>
</dependency>
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"));
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();
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();
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.
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();
It
always uses the same delay for each attempt. Example:
AsyncRetryExecutor executor = Retry.async(Executors.newSingleThreadScheduledExecutor())
.backOff(Backoffs.fixedDelay(1L, TimeUnit.SECONDS))
.build();
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();
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:
true
or you didn’t specify any (in that case there is a default retry predicate,true
).true
or you didn’t specify any (in that case there is afalse
).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.
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}
});
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"));