Retrier

Build Status

Java library to simplify retries of any function or code block (piece of code wrapped in lambda expression) without changing the execution flow from developer perspective. They can rely on the return value of the function/code block in retry, etc..

Getting Started

Add the following maven dependency to your project pom.xml:

<dependency>
    <groupId>io.github.ravichaturvedi</groupId>
    <artifactId>retrier</artifactId>
    <version>0.2</version>
</dependency>
import static io.github.ravichaturvedi.retrier.Retry.*;

// Retrying on some piece of code which may return value or may throw exception.
// Result will be of same type as returned by the lambda expression.
Map<String, String> result = retry(() -> {
    // some operation which may throw exception
    return new HashMap<String, String>();
});


// Retrying on some piece of code which doesn't return any value but can throw exception
retry(() -> {
    // some operation which may throw exception
});

Background

Generally, we call an external system/service using some RPC mechanism (REST/GRPC/etc..) but we immediately inform about the failure to the client/user. But in case if it would have been some transient failure (network issue/connectivity issue/etc..) then we could have retried and get success next time. That would be a much nice experience to the client/user calling your service.

These kind of situation occurs in distributed system, microservice, mobile application and even within the same process space if some method/code block is throwing exception transiently.

There are many strategies to deal with retries and generally due to the induced complexity in the code, developer tends not to do it in while writing the first version of the program/software. This library fills that gap by providing a library, which in turn provides an elegant abstraction to deal with retries of any function/code-block.

The whole idea behind building thins library is to encourage developers to build more resilient systems.

Features

Drawbacks

Safety

Keep in mind any side effects that may occur if the operation is executed multiple times. In general, the it is safe to retry only idempotent operations. Idempotent operations are those which do not change the result when applied repeatedly.

Examples of idempotent operation:

Examples of non-idempotent operation:

Usages

Specifying below some typical retry use cases covered by the Retrier.

Assuming there are methods named foo and bar that throws Exception and following imports are in place:

import static io.github.ravichaturvedi.retrier.Retriers.*;
import static io.github.ravichaturvedi.retrier.Retry.*;

void foo() throws Exception;
<T> T bar(T) throws Exception;
  1. Retry number of times
Retrier retrier = create(withRetryCount(3));

// Retring in lambda that doesn't return anything
retrier.retry(on(Exception.class), () -> foo());

// Retrying on lambda that returns integer. 
int result = retrier.retry(on(Exception.class), () -> bar(2));
  1. Retry number of times until timeout expires
Retrier retrier = create(withRetryCount(3), 
                        withTimeout(Duration.of(15, ChronoUnit.SECONDS)));
                        
// Retring in lambda that doesn't return anything                        
retrier.retry(on(Exception.class), () -> foo());

// Retrying on lambda that returns Object. 
Object result = retrier.retry(on(Exception.class), () -> bar(new Object()));
  1. Retry unlimited number of times until timeout expires.
Retrier retrier = create(withTimeout(Duration.of(15, ChronoUnit.SECONDS)));

// Retring in lambda that doesn't return anything 
retrier.retry(on(Exception.class), () -> foo());

// Retrying on lambda that returns a map. 
Map<String, String> result = retrier.retry(on(Exception.class), () -> {
    return new HashMap<String, String>();
});
  1. Retry unlimited number of times with exponential backoff delay.
Retrier retrier = create(withExpBackoff(Duration.of(3, ChronoUnit.SECONDS))));

// Retring in lambda that doesn't return anything 
retrier.retry(on(Exception.class), () -> foo());

// Retrying on lambda that returns a long. 
long result = retrier.retry(on(Exception.class), () -> {
    return 2L;
});
  1. Retry unlimited number of times with exponential backoff delay and max backoff delay.
Retrier retrier = create(withExpBackoff(Duration.of(3, ChronoUnit.SECONDS), Duration.of(9, ChronoUnit.SECONDS)));

// Retring in lambda that doesn't return anything 
retrier.retry(on(Exception.class), () -> foo());

// Retrying on lambda that returns a long. 
long result = retrier.retry(on(Exception.class), () -> {
    return 2L;
});
  1. Retry unlimited number of times with exponential backoff delay and max backoff delay but on nested exception.
Retrier retrier = create(withExpBackoff(Duration.of(3, ChronoUnit.SECONDS), Duration.of(9, ChronoUnit.SECONDS)));

// Retring in lambda that doesn't return anything 
retrier.retry(onNested(IllegalArgumentException.class), () -> foo());

// Retrying on lambda that returns a long. 
long result = retrier.retry(onNested(IllegalArgumentException.class), () -> {
    return 2L;
});
  1. Retry number of times with timeout and exponential backoff with initial delay.
Retrier retrier = create(withRetryCount(3), 
                        withTimeout(Duration.of(15, ChronoUnit.SECONDS)),
                        withExpBackoff(Duration.of(3, ChronoUnit.SECONDS)));
              
// Retring in lambda that doesn't return anything                         
retrier.retry(on(Exception.class), () -> foo());

// Retrying on lambda that returns a long.
long result = retrier.retry(on(Exception.class), () -> bar(2L));
  1. Retry number of times with timeout, exponential backoff with initial delay and max backoff delay.
Retrier retrier = create(withRetryCount(3), 
                        withTimeout(Duration.of(15, ChronoUnit.SECONDS)),
                        withExpBackoff(Duration.of(3, ChronoUnit.SECONDS), Duration.of(9, ChronoUnit.SECONDS)));
                        
retrier.retry(on(Exception.class), () -> foo());
Map<String, String> result = retrier.retry(on(Exception.class), () -> bar(new HashMap<String, String>()));
  1. Retry number of times with timeout, exponential backoff with initial delay, max backoff delay and trace to see how retrier itself is working.

    Since withTrace accepts any lambda which takes java.lang.String as only parameter, so can be hooked with any type of logging infrastructure.

Retrier retrier = create(withRetryCount(3), 
                        withTimeout(Duration.of(15, ChronoUnit.SECONDS)),
                        withExpBackoff(Duration.of(3, ChronoUnit.SECONDS), Duration.of(9, ChronoUnit.SECONDS)),
                        withTrace(System.out::println));
                        
retrier.retry(on(Exception.class), () -> foo());
Set<String> result = retrier.retry(on(Exception.class), () -> bar(new HashSet<String>()));
  1. Retry number of times with timeout, exponential backoff with initial delay, max backoff delay and trace to see how retrier itself is working. Also if specific exception occurs then execute some piece of code.

Since withTrace accepts any lambda which takes java.lang.String as only parameter, so can be hooked with any type of logging infrastructure.

Retrier retrier = create(withRetryCount(3), 
                        withTimeout(Duration.of(15, ChronoUnit.SECONDS)),
                        withExpBackoff(Duration.of(3, ChronoUnit.SECONDS), Duration.of(9, ChronoUnit.SECONDS)),
                        withTrace(System.out::println));
                        
retrier.retry(() -> {
    foo();
}, on(on(IllegalStateException.class), on(IllegalArgumentException.class, ()-> {
       // Some handling of exception
})));

retrier.retry(() -> {
    return bar();
}, on(on(IllegalStateException.class), on(IllegalArgumentException.class, ()-> {
       // Some handling of exception
})));

Credits