Talk about Retryer of feign.

  feign

Order

This article mainly studies feign’s Retryer.

Retryer

feign-core-10.2.3-sources.jar! /feign/Retryer.java

public interface Retryer extends Cloneable {

  /**
   * if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception.
   */
  void continueOrPropagate(RetryableException e);

  Retryer clone();

  class Default implements Retryer {

    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
    int attempt;
    long sleptForMillis;

    public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }

    public Default(long period, long maxPeriod, int maxAttempts) {
      this.period = period;
      this.maxPeriod = maxPeriod;
      this.maxAttempts = maxAttempts;
      this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
      return System.currentTimeMillis();
    }

    public void continueOrPropagate(RetryableException e) {
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
        throw e;
      }
      sleptForMillis += interval;
    }

    /**
     * Calculates the time interval to a retry attempt. <br>
     * The interval increases exponentially with each attempt, at a rate of nextInterval *= 1.5
     * (where 1.5 is the backoff factor), to the maximum interval.
     *
     * @return time in nanoseconds from now until the next attempt.
     */
    long nextMaxInterval() {
      long interval = (long) (period * Math.pow(1.5, attempt - 1));
      return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
      return new Default(period, maxPeriod, maxAttempts);
    }
  }

  /**
   * Implementation that never retries request. It propagates the RetryableException.
   */
  Retryer NEVER_RETRY = new Retryer() {

    @Override
    public void continueOrPropagate(RetryableException e) {
      throw e;
    }

    @Override
    public Retryer clone() {
      return this;
    }
  };
}
  • Retryer inherits the Cloneable interface, which defines the continueOrPropagate and clone methods. It has a built-in implementation named Default and named NEVER_RETRY
  • Default has period, maxPeriod, maxattents parameters that can be set. the period used by the default constructor is 100, maxPeriod is 1000, maxattents is 5; The continueOrPropagate method first judges whether the attempt reaches the threshold, and throws an exception if it reaches the threshold; otherwise, it further calculates the interval, and then performs sleep
  • The continueOrPropagate of NEVER_RETRY throws an exception directly, while the clone method returns the current instance directly

SynchronousMethodHandler

feign-core-10.2.3-sources.jar! /feign/SynchronousMethodHandler.java

final class SynchronousMethodHandler implements MethodHandler {
    //......

  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }


    //......
}
  • SynchronousMethodHandler’s invoke method first creates a retryer using retryer.clone (), and then executes retrier.continuorpropagate (e) when a RetryableException is captured

RetryableException

feign-core-10.2.3-sources.jar! /feign/RetryableException.java

public class RetryableException extends FeignException {

  private static final long serialVersionUID = 1L;

  private final Long retryAfter;
  private final HttpMethod httpMethod;

  /**
   * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header.
   */
  public RetryableException(int status, String message, HttpMethod httpMethod, Throwable cause,
      Date retryAfter) {
    super(status, message, cause);
    this.httpMethod = httpMethod;
    this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
  }

  /**
   * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header.
   */
  public RetryableException(int status, String message, HttpMethod httpMethod, Date retryAfter) {
    super(status, message);
    this.httpMethod = httpMethod;
    this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
  }

  /**
   * Sometimes corresponds to the {@link feign.Util#RETRY_AFTER} header present in {@code 503}
   * status. Other times parsed from an application-specific response. Null if unknown.
   */
  public Date retryAfter() {
    return retryAfter != null ? new Date(retryAfter) : null;
  }

  public HttpMethod method() {
    return this.httpMethod;
  }
}
  • RetryableException inherits FeignException, and its constructor receives retryAfter, which can be null

FeignException

feign-core-10.2.3-sources.jar! /feign/FeignException.java

public class FeignException extends RuntimeException {
    //......

  static FeignException errorReading(Request request, Response response, IOException cause) {
    return new FeignException(
        response.status(),
        format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()),
        cause,
        request.requestBody().asBytes());
  }

    //......
}
  • FeignException defines the errorReading static method, which creates a FeignException.

ErrorDecoder

feign-core-10.2.3-sources.jar! /feign/codec/ErrorDecoder.java

public interface ErrorDecoder {
    //......

  public static class Default implements ErrorDecoder {

    private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder();

    @Override
    public Exception decode(String methodKey, Response response) {
      FeignException exception = errorStatus(methodKey, response);
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        return new RetryableException(
            response.status(),
            exception.getMessage(),
            response.request().httpMethod(),
            exception,
            retryAfter);
      }
      return exception;
    }

    private <T> T firstOrNull(Map<String, Collection<T>> map, String key) {
      if (map.containsKey(key) && !map.get(key).isEmpty()) {
        return map.get(key).iterator().next();
      }
      return null;
    }
  }

  static class RetryAfterDecoder {

    static final DateFormat RFC822_FORMAT =
        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US);
    private final DateFormat rfc822Format;

    RetryAfterDecoder() {
      this(RFC822_FORMAT);
    }

    RetryAfterDecoder(DateFormat rfc822Format) {
      this.rfc822Format = checkNotNull(rfc822Format, "rfc822Format");
    }

    protected long currentTimeMillis() {
      return System.currentTimeMillis();
    }

    /**
     * returns a date that corresponds to the first time a request can be retried.
     *
     * @param retryAfter String in
     *        <a href="https://tools.ietf.org/html/rfc2616#section-14.37" >Retry-After format</a>
     */
    public Date apply(String retryAfter) {
      if (retryAfter == null) {
        return null;
      }
      if (retryAfter.matches("^[0-9]+$")) {
        long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter));
        return new Date(currentTimeMillis() + deltaMillis);
      }
      synchronized (rfc822Format) {
        try {
          return rfc822Format.parse(retryAfter);
        } catch (ParseException ignored) {
          return null;
        }
      }
    }
  }

    //......
}
  • Errordecoder provides the Default implementation of Default. Its Decode method uses retryAfter to calculate the RetryAfter value and returns RetryableExceptionï¼› when the value is not null. The apply method of the retryAfterDecoder calculates the retryAfter date based on the retryAfter parameter, which is read from the header named RetryAfter in the response.

Summary

  • Retryer inherits the Cloneable interface, which defines the continueOrPropagate and clone methods. It has a built-in implementation named Default and named NEVER_RETRY
  • Default has period, maxPeriod, maxattents parameters that can be set. the period used by the default constructor is 100, maxPeriod is 1000, maxattents is 5; The continueOrPropagate method first judges whether the attempt reaches the threshold, and throws an exception if it reaches the threshold; otherwise, it further calculates the interval, and then performs sleep
  • The continueOrPropagate of NEVER_RETRY throws an exception directly, while the clone method returns the current instance directly

doc