Naftuli Kay
Naftuli Kay

Reputation: 91740

Design pattern for "retrying" logic that failed?

I'm writing some reconnect logic to periodically attempt to establish a connection to a remote endpoint which went down. Essentially, the code looks like this:

public void establishConnection() {
    try {
        this.connection = newConnection();
    } catch (IOException e) {
        // connection failed, try again.
        try { Thread.sleep(1000); } catch (InterruptedException e) {};

        establishConnection();
    }
}

I've solved this general problem with code similar to the above on many occasions, but I feel largely unsatisfied with the result. Is there a design pattern designed for dealing with this issue?

Upvotes: 74

Views: 79104

Answers (12)

Mehmet Pekdemir
Mehmet Pekdemir

Reputation: 366

I have wrote my custom annotation. Maybe you can use this annotation.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryOperation {
 
    int retryCount();
 
    int waitSeconds();
}

@Slf4j
@Aspect
@Component
public class RetryOperationAspect {
 
    @Around(value = "@annotation(com.demo.infra.annotation.RetryOperation)")
    public Object retryOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        Object response = null;
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        RetryOperation annotation = method.getAnnotation(RetryOperation.class);
        int retryCount = annotation.retryCount();
        int waitSeconds = annotation.waitSeconds();
        boolean successful = false;
 
        do {
            try {
                response = joinPoint.proceed();
                successful = true;
            } catch (Exception e) {
                log.error("Operation failed, retries remaining: {}", retryCount);
                retryCount--;
                if (retryCount < 0) {
                    throw e;
                }
                if (waitSeconds > 0) {
                    log.warn("Waiting for {} second(s) before next retry", waitSeconds);
                    Thread.sleep(waitSeconds * 1000L);
                }
            }
        } while (!successful);
 
        return response;
    }
}


@RetryOperation(retryCount = 5, waitSeconds = 1)
public void method() {
         
}

Upvotes: 0

Vishal Kharde
Vishal Kharde

Reputation: 1727

Here's a another approach to perform the retry. No libraries, no annotations, no extra implementations. Import java.util.concurrent.TimeUnit;

public static void myTestFunc() {
        boolean retry = true;
        int maxRetries = 5;   //max no. of retries to be made
        int retries = 1;
        int delayBetweenRetries = 5;  // duration  between each retry (in seconds)
        int wait = 1;
    do {
        try {
            this.connection = newConnection();
            break;
        }
        catch (Exception e) {
            wait = retries * delayBetweenRetries;
            pause(wait);
            retries += 1;
            if (retries > maxRetries) {
                retry = false;
                log.error("Task failed on all of " + maxRetries + " retries");
            }
        }
    } while (retry);

}

public static void pause(int seconds) {
    long secondsVal = TimeUnit.MILLISECONDS.toMillis(seconds);

    try {
        Thread.sleep(secondsVal);
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
}

}

Upvotes: 1

Valeriy K.
Valeriy K.

Reputation: 2904

I'm using retry4j library. Test code example:

public static void main(String[] args) {
    Callable<Object> callable = () -> {
        doSomething();
        return null;
    };

    RetryConfig config = new RetryConfigBuilder()
            .retryOnAnyException()
            .withMaxNumberOfTries(3)
            .withDelayBetweenTries(5, ChronoUnit.SECONDS)
            .withExponentialBackoff()
            .build();

    new CallExecutorBuilder<>().config(config).build().execute(callable);
}

public static void doSomething() {
    System.out.println("Trying to connect");
    // some logic
    throw new RuntimeException("Disconnected"); // init error
    // some logic
}

Upvotes: 2

klusht
klusht

Reputation: 31

You can also create a wrapper function that just does a loop over the intended operation and when is done just break out of the loop.

public static void main(String[] args) {
    retryMySpecialOperation(7);
}

private static void retryMySpecialOperation(int retries) {
    for (int i = 1; i <= retries; i++) {
        try {
            specialOperation();
            break;
        }
        catch (Exception e) {
            System.out.println(String.format("Failed operation. Retry %d", i));
        }
    }
}

private static void specialOperation() throws Exception {
    if ((int) (Math.random()*100) % 2 == 0) {
        throw new Exception("Operation failed");
    }
    System.out.println("Operation successful");
}

Upvotes: 3

Dherik
Dherik

Reputation: 19100

I really like this Java 8 code from this blog and you don't need any extra library on your classpath.

You only need to pass a function to the retry class.

@Slf4j
public class RetryCommand<T> {

    private int maxRetries;

    RetryCommand(int maxRetries)
    {
        this.maxRetries = maxRetries;
    }

    // Takes a function and executes it, if fails, passes the function to the retry command
    public T run(Supplier<T> function) {
        try {
            return function.get();
        } catch (Exception e) {
            log.error("FAILED - Command failed, will be retried " + maxRetries + " times.");
            return retry(function);
        }
    }

    private T retry(Supplier<T> function) throws RuntimeException {

        int retryCounter = 0;
        while (retryCounter < maxRetries) {
            try {
                return function.get();
            } catch (Exception ex) {
                retryCounter++;
                log.error("FAILED - Command failed on retry " + retryCounter + " of " + maxRetries, ex);
                if (retryCounter >= maxRetries) {
                    log.error("Max retries exceeded.");
                    break;
                }
            }
        }
        throw new RuntimeException("Command failed on all of " + maxRetries + " retries");
    }
}

And to use it:

new RetryCommand<>(5).run(() -> client.getThatThing(id));

Upvotes: 17

Ravipati Praveen
Ravipati Praveen

Reputation: 498

If you are using java 8, this may helps.

import java.util.function.Supplier;

public class Retrier {
public static <T> Object retry(Supplier<T> function, int retryCount) throws Exception {
     while (0<retryCount) {
        try {
            return function.get();
        } catch (Exception e) {
            retryCount--;
            if(retryCount == 0) {
                throw e;
            }
        }
    }
    return null;
}

public static void main(String[] args) {
    try {
        retry(()-> {
            System.out.println(5/0);
            return null;
        }, 5);
    } catch (Exception e) {
        System.out.println("Exception : " + e.getMessage());
    }
}
}

Thanks,

Praveen R.

Upvotes: 2

yegor256
yegor256

Reputation: 105133

I'm using AOP and Java annotations. There is a ready-made mechanism in jcabi-aspects (I'm a developer):

@RetryOnFailure(attempts = 3, delay = 1, unit = TimeUnit.SECONDS)
public void establishConnection() {
  this.connection = newConnection();
}

ps. You can also try RetryScalar from Cactoos.

Upvotes: 12

Jonathan
Jonathan

Reputation: 5117

Using Failsafe (author here):

RetryPolicy retryPolicy = new RetryPolicy()
  .retryOn(IOException.class)
  .withMaxRetries(5)
  .withDelay(1, TimeUnit.SECONDS);

Failsafe.with(retryPolicy).run(() -> newConnection());

No annotations, no magic, doesn't need to be a Spring app, etc. Just straightforward and simple.

Upvotes: 13

db80
db80

Reputation: 4427

You can try spring-retry, it has a clean interface and it's easy to use.

Example:

 @Retryable(maxAttempts = 4, backoff = @Backoff(delay = 500))
 public void establishConnection() {
    this.connection = newConnection();
 } 

In case of exception, it will retry (call) up to 4 times the method establishConnection() with a backoff policy of 500ms

Upvotes: 7

JB Nizet
JB Nizet

Reputation: 691953

Shameless plug: I have implemented some classes to allow retrying operations. The library is not made available yet, but you may fork it on github. And a fork exists.

It allows building a Retryer with various flexible strategies. For example:

Retryer retryer = 
    RetryerBuilder.newBuilder()
                  .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECOND))
                  .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                  .retryIfExceptionOfType(IOException.class)
                  .build();

And you can then execute a callable (or several ones) with the Retryer:

retryer.call(new Callable<Void>() {
    public Void call() throws IOException {
        connection = newConnection();
        return null;
    }
}

Upvotes: 33

Andrey Borisov
Andrey Borisov

Reputation: 3170

there is nothing special in retrying at all - take this class as example http://www.docjar.com/html/api/org/springframework/jms/listener/DefaultMessageListenerContainer.java.html As you can see even spring developers still writing code for retry-ing - line 791... there is no such special pattern AFAIK..

What i can advice to deal with resources is to take apache commons pool library - check this http://commons.apache.org/pool/apidocs/org/apache/commons/pool/impl/GenericObjectPool.html and visit http://commons.apache.org/pool

Upvotes: 0

GalacticJello
GalacticJello

Reputation: 11445

You could try the Idempotent Retry Pattern.

enter image description here

Upvotes: 30

Related Questions