How to add Resiliency to any APIs

Resiliency Handler

Resiliency Handler

This simple generic class helps to add resiliency to any APIs with follow delay strategies

  1. Exponential backoff and jitter strategy.

  2. Fixed delay strategy.

ResiliencyHandler.java
package circuitbreaker;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * Generic ResiliencyHandler to simplify retry operations with fixed/exponential
*  backoff delay strategies
 *
 * <pre>
 *         Student student =
 *                 ResiliencyHandler.buildFor(Student.class)
 *                     .usingJitterStrategy(3, 10L * 1000L)
 *                     .tryWithResiliency(() -> studentService.getStudent())
 *                     .fallbackOnFail(() -> studentService.getDefaultStudent())
 *                     .getResult();// return result
 *
 * </pre>
 *
 * @param <R> ResultType
 *
 * @author ThirupathiReddy Vajjala
 */
@Slf4j
public class ResiliencyHandler<R> {

  /**
   * Expected Result
   */
  private R result;

  /**
   * builder instance
   */
  private Builder<R> builder;

  /**
   * @param builder builder
   */
  private ResiliencyHandler(Builder<R> builder) {
    this.builder = builder;
  }

  /**
   * @param clazz clazz
   * @param <R>   ofType
   *
   * @return Builder
   */
  public static <R> Builder<R> buildFor(Class<R> clazz) {
    log.info("Creating builder instance for clazz {}", clazz);
    return new Builder<>();
  }

  public static class ResiliencyException extends RuntimeException {

    private String message;

    public ResiliencyException(String message) {
      super(message);
      this.message = message;
    }

    public ResiliencyException(String message, Throwable throwable) {
      super(message, throwable);
      this.message = message;
    }

    public ResiliencyException(Throwable throwable) {
      super(throwable);
      this.message = throwable.getMessage();
    }

    @Override
    public String getMessage() {
      return message;
    }
  }

  RuntimeException runtimeException;

  /**
   * Retry function
   *
   * @param supplier Supplier
   *
   * @return ResiliencyHandler
   */
  public ResiliencyHandler<R> tryWithResiliency(Supplier<R> supplier) {
    int counter = 0;
    do {
      try {
        this.result = supplier.get();
        builder.setSuccess(true);
        return this;
      } catch (Exception exception) {
        log.debug("Failure occurred", exception);
        log.warn("Operation failed due to {}", exception.getMessage());
        this.runtimeException = new ResiliencyException(exception.getMessage(), exception);
      }

      if (counter++ == builder.getMaxAttempts()) {
        break;
      }
      builder.setAttemptsMade(counter);
      builder.waitForNextAttempt();
      log.info("Retry Attempt: {}", counter);
    } while (counter <= builder.getMaxAttempts());

    return this;
  }

  /**
   * Execute fallback behaviour if retry attempts fail
   *
   * @param supplier Supplier
   *
   * @return ResiliencyHandler
   */
  public ResiliencyHandler<R> fallbackOnFail(Supplier<R> supplier) {

    if (!builder.isSuccess()) {
      log.info("Executing fallback behaviour");
      this.result = supplier.get();
    }
    return this;
  }

  /**
   * Return result onCompletion
   *
   * @return result
   */

  public R getResultOrRethrow() {
    if (null == result && null != runtimeException) {
      throw runtimeException;
    }
    return result;
  }

  /**
   * @param customException must be RuntimeException when you work with java8 lamda/functional interfaces
   *
   * @return result if available
   *
   * @throws RuntimeException {@link RuntimeException}
   */
  public R getResultOrRethrow(RuntimeException customException) {
    if (null == result && null != customException) {
      throw customException;
    }
    return result;
  }

  /**
   * Return result or default value
   *
   * @param defaultResult defaultResult
   *
   * @return result
   */
  public R getResult(R defaultResult) {
    if (null == result) {
      return defaultResult;
    }
    return result;
  }

  public R getResult() {
    return result;
  }

  /**
   * Builder class
   *
   * @param <R>
   */
  public static class Builder<R> {

    /**
     * flag to check retry success
     */
    private boolean isSuccess;

    public boolean isSuccess() {
      return isSuccess;
    }

    public void setSuccess(boolean success) {
      isSuccess = success;
    }

    /**
     * default max retry attempts= 3
     */
    private int maxAttempts = 3;

    public int getMaxAttempts() {
      return maxAttempts;
    }

    /**
     * Default fixedDelay after each retry (5 seconds)
     */
    private long fixedDelayInMillis = TimeUnit.MILLISECONDS.toSeconds(5L * 1000L);

    /**
     * Default maximum delay with jitter config 25 seconds
     */
    private long maxDelayInMillis = TimeUnit.MILLISECONDS.toSeconds(25L * 1000L);

    /**
     * Total attempts made (used runtime)
     */
    private int attemptsMade;

    /**
     * Returns attempts made so far
     *
     * @return attempts made so far
     */
    public int getAttemptsMade() {
      return attemptsMade;
    }

    /**
     * flag to check if jitterConfiguration requested by client
     */
    private boolean isJitterEnabled;

    /**
     * @return
     */
    public boolean isJitterEnabled() {
      return isJitterEnabled;
    }

    /**
     * Used jitterConfiguration with exponential backoff used
     *
     * @param maxAttempts      maxAttempts
     * @param maxDelayInMillis maxDelayInMillis after each retry,
     *                         wait time increases exponentially until it reaches maxDelay
     *
     * @return ResiliencyHandler instance
     */
    public ResiliencyHandler<R> usingJitterStrategy(int maxAttempts, long maxDelayInMillis) {
      this.isJitterEnabled = true;
      this.maxAttempts = maxAttempts;
      this.maxDelayInMillis = maxDelayInMillis;
      return new ResiliencyHandler<>(this);
    }

    /**
     * With default jitter configuration used
     *
     * @return ResiliencyHandler instance
     */
    public ResiliencyHandler<R> usingDefaultJitterStrategy() {
      this.isJitterEnabled = true;
      return new ResiliencyHandler<>(this);
    }

    /**
     * Fixed delay configuration
     *
     * @param maxAttempts        maxAttempts
     * @param fixedDelayInMillis fixedDelayInMillis
     *
     * @return ResiliencyHandler instance
     */
    public ResiliencyHandler<R> usingFixedDelayStrategy(int maxAttempts, long fixedDelayInMillis) {
      this.maxAttempts = maxAttempts;
      this.fixedDelayInMillis = fixedDelayInMillis;
      return new ResiliencyHandler<>(this);
    }

    public void setAttemptsMade(int attemptsMade) {
      this.attemptsMade = attemptsMade;
    }

    private void waitForNextAttempt() {
      long delayDuration = isJitterEnabled() ? jitterDelay() : fixedDelay();
      try {
        log.info("Retry attempted after {} ms", delayDuration);
        TimeUnit.MILLISECONDS.sleep(delayDuration);
      } catch (InterruptedException interruptedException) {
        log.debug("Failed to wait for next attempt", interruptedException);
      }
    }

    private long fixedDelay() {
      return fixedDelayInMillis;
    }

    /**
     * Exponential back-off strategy
     *
     * @return delay in seconds
     */
    public long exponentialDelay() {

      long delay = (long) Math.pow(2, getAttemptsMade());
      delay *= 1000;
      if (delay <= 0) {
        return maxDelayInMillis;
      }
      return Math.min(delay, maxDelayInMillis);
    }

    /**
     * Exponential with jitter
     *
     * @return delay in seconds
     */
    public long jitterDelay() {
      double random = Math.random();
      long jitterValue = Math.round(random * 1000);
      return exponentialDelay() + jitterValue;
    }

  }

}

Comments

Popular posts from this blog

IBM Datapower GatewayScript

Spring boot Kafka Integration

Spring boot SOAP Web Service Performance