async-retry

Asynchronous retrying
for Java 8


Tomasz Nurkiewicz

nurkiewicz.com | @tnurkiewicz


Website: github.com/nurkiewicz/async-retry

Slides: nurkiewicz.github.io/talks

Quick example


ScheduledExecutorService scheduler = //...
RetryExecutor executor = new AsyncRetryExecutor(scheduler);

CompletableFuture<Socket> future = executor.getWithRetry(() ->
  new Socket("localhost", 8080)
);

future.thenAccept(socket ->
  System.out.println("Connected! " + socket)
);
          

Logging


TRACE Retry 0 failed after 3ms, scheduled next retry in 508ms (Sun Jul 21 21:01:12 CEST 2013)
java.net.ConnectException: Connection refused
    ...
TRACE Retry 1 failed after 0ms, scheduled next retry in 934ms (Sun Jul 21 21:01:13 CEST 2013)
java.net.ConnectException: Connection refused
    ...
TRACE Retry 2 failed after 0ms, scheduled next retry in 1919ms (Sun Jul 21 21:01:15 CEST 2013)
java.net.ConnectException: Connection refused
    ...
TRACE Successful after 2 retries, took 0ms and returned: Socket[addr=localhost/127.0.0.1,port=8080,localport=46332]

Connected! Socket[addr=localhost/127.0.0.1,port=8080,localport=46332]
          

Setup - immutable!


ScheduledExecutorService scheduler = 
  Executors.newSingleThreadScheduledExecutor();

RetryExecutor executor = new AsyncRetryExecutor(scheduler)
    .retryOn(SocketException.class)
    .withExponentialBackoff(500, 2)  //500ms times 2 after each retry
    .withUniformJitter()             //add between +/- 100 ms randomly
    .withMaxDelay(10_000)            //10 seconds
    .withMaxRetries(20);
          

Rationale

  • Non-blocking
  • Scalable
  • Easy to use
  • Flexible
  • Embrace immutability
  • Used Java 8, before it was cool

See also: RetryTemplate from Spring

Composable


CompletableFuture<String> f1 = 
    executor.getWithRetry(ctx -> unreliable());
CompletableFuture<String> f2 = 
    executor.getWithRetry(ctx -> slow());

f1.acceptEither(f2, first -> {
    //first to succeed
});
          

abortOn() / retryOn()


executor.
    retryOn(IOException.class).
    abortOn(FileNotFoundException.class).
    retryOn(SQLException.class).
    abortOn(DataTruncation.class).
    getWithRetry(ctx -> dao.load(42));
          

OptimisticLock Exception


executor.
    retryOn(OptimisticLockException.class).
    withNoDelay().
    getWithRetry(ctx -> dao.optimistic());
          

Retry predicates


executor.
    abortIf(throwable ->
        throwable instanceof SQLException &&
            throwable.getMessage().contains("ORA-00911")    
    ).
    retryIf(t -> t.getCause() != null);
          

Delays (backoff)

  • No delay between retries
  • Fixed delay
  • Growing linearly
  • Growing exponentially
  • Min/max
  • Optionally keep fixed rate

Random jitter


executor.withUniformJitter(100)     //ms
          

...or:


executor.withProportionalJitter(0.1)        //10%
          

RetryContext


executor.
    getWithRetry(ctx -> 
        new Socket("localhost", 
                   8080 + ctx.getRetryCount())).
    thenAccept(System.out::println);
          

In Spring framework


@Bean
public RetryExecutor retryExecutor() {
    return new AsyncRetryExecutor(scheduler()).
        retryOn(SocketException.class).
        withExponentialBackoff(500, 2);
}

@Bean(destroyMethod = "shutdownNow")
public ScheduledExecutorService scheduler() {
    return Executors.newSingleThreadScheduledExecutor();
}
          

Thank you!


<dependency>
    <groupId>com.nurkiewicz.asyncretry</groupId>
    <artifactId>asyncretry</artifactId>
    <version>0.0.5</version>
</dependency>
          

Website: github.com/nurkiewicz/async-retry

Slides: nurkiewicz.github.io/talks