Yet Another Try

The library provides an asynchronous executor with an extensible retry policy.

License

License

Categories

Categories

Other General Purpose Libraries
GroupId

GroupId

com.github.sorokinigor
ArtifactId

ArtifactId

yet-another-try
Last Version

Last Version

1.1.0
Release Date

Release Date

Type

Type

jar
Description

Description

Yet Another Try
The library provides an asynchronous executor with an extensible retry policy.
Project URL

Project URL

https://github.com/sorokinigor/yet-another-try
Source Code Management

Source Code Management

https://github.com/sorokinigor/yet-another-try/tree/master

Download yet-another-try

How to add to project

<!-- https://jarcasting.com/artifacts/com.github.sorokinigor/yet-another-try/ -->
<dependency>
    <groupId>com.github.sorokinigor</groupId>
    <artifactId>yet-another-try</artifactId>
    <version>1.1.0</version>
</dependency>
// https://jarcasting.com/artifacts/com.github.sorokinigor/yet-another-try/
implementation 'com.github.sorokinigor:yet-another-try:1.1.0'
// https://jarcasting.com/artifacts/com.github.sorokinigor/yet-another-try/
implementation ("com.github.sorokinigor:yet-another-try:1.1.0")
'com.github.sorokinigor:yet-another-try:jar:1.1.0'
<dependency org="com.github.sorokinigor" name="yet-another-try" rev="1.1.0">
  <artifact name="yet-another-try" type="jar" />
</dependency>
@Grapes(
@Grab(group='com.github.sorokinigor', module='yet-another-try', version='1.1.0')
)
libraryDependencies += "com.github.sorokinigor" % "yet-another-try" % "1.1.0"
[com.github.sorokinigor/yet-another-try "1.1.0"]

Dependencies

compile (1)

Group / Artifact Type Version
org.slf4j : slf4j-api jar 1.7.22

test (4)

Group / Artifact Type Version
org.testng : testng jar 6.10
org.assertj : assertj-core jar 3.6.2
org.mockito : mockito-core jar 2.7.5
ch.qos.logback : logback-classic jar 1.1.7

Project Modules

There are no modules declared in this project.

Yet Another Try

Build Status codecov Maven Central

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 returns true).
  • None of the terminate predicates returns true or you didn't specify any (in that case there is a default terminate predicate, which always returns false).

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"));

Versions

Version
1.1.0
1.0.0