Retry.java

package com.maybeitssquid.retry.resilience4j;

import com.maybeitssquid.retry.LimitRetryAfter;
import com.maybeitssquid.retry.RetryStatusCodes;
import io.github.resilience4j.core.IntervalBiFunction;
import io.github.resilience4j.retry.RetryConfig;
import jakarta.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.function.Consumer;

/** Factories for common configurations. */
public interface Retry {

  /**
   * Adds a {@link java.util.function.Predicate} that decides whether to retry based on the HTTP
   * status code in the response.
   *
   * @param retry status codes that allow retry, in addition to the defaults for idempotent
   *     functions provided by {@link RetryStatusCodes}
   * @return consumer that uses HTTP status code for retry decisions.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> idempotent(final int... retry) {
    return builder -> builder.retryOnResult(RetryStatusCodes.idempotent(retry));
  }

  /**
   * Adds a {@link java.util.function.Predicate} that decides whether to retry based on the HTTP
   * status code in the response.
   *
   * @param retry status codes that allow retry, in addition to the defaults for non-idempotent
   *     functions provided by {@link RetryStatusCodes}
   * @return consumer that uses HTTP status code for retry decisions.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> nonIdempotent(
      final int... retry) {
    return builder -> builder.retryOnResult(RetryStatusCodes.nonIdempotent(retry));
  }

  /**
   * Adds a {@link java.util.function.Predicate} that decides whether to retry based on the HTTP
   * status code in the response.
   *
   * @param retry complete list of status codes that allow retry
   * @return consumer that uses HTTP status code for retry decisions.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> onlyCodes(final int... retry) {
    return builder -> builder.retryOnResult(RetryStatusCodes.only(retry));
  }

  /**
   * Adds a {@link java.util.function.Predicate} that decides whether to retry based on the HTTP
   * status code in the response and whether any {code Retry-After} header allows for a retry within
   * an acceptable interval, and adds an {@link IntervalBiFunction} to extend the wait interval as
   * needed.
   *
   * @param limit the maximum wait interval that will be allowed by a {@code Retry-After}.
   * @param retry status codes that allow retry, in addition to the defaults for idempotent
   *     functions provided by {@link RetryStatusCodes}
   * @return consumer that uses HTTP status code and Retry-After for retry decisions and waits.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> idempotent(
      final Duration limit, final int... retry) {
    return limitAndCodes(limit, RetryStatusCodes.idempotent(retry));
  }

  /**
   * Adds a {@link java.util.function.Predicate} that decides whether to retry based on the HTTP
   * status code in the response and whether any {code Retry-After} header allows for a retry within
   * an acceptable interval, and adds an {@link IntervalBiFunction} to extend the wait interval as
   * needed.
   *
   * @param limit the maximum wait interval that will be allowed by a {@code Retry-After}.
   * @param retry status codes that allow retry, in addition to the defaults for non-idempotent
   *     functions provided by {@link RetryStatusCodes}
   * @return consumer that uses HTTP status code and Retry-After for retry decisions and waits.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> nonIdempotent(
      final Duration limit, final int... retry) {
    return limitAndCodes(limit, RetryStatusCodes.nonIdempotent(retry));
  }

  /**
   * Adds a {@link java.util.function.Predicate} that decides whether to retry based on the HTTP
   * status code in the response and whether any {code Retry-After} header allows for a retry within
   * an acceptable interval, and adds an {@link IntervalBiFunction} to extend the wait interval as
   * needed.
   *
   * @param limit the maximum wait interval that will be allowed by a {@code Retry-After}.
   * @param retry complete list of status codes that allow retry
   * @return consumer that uses HTTP status code and Retry-After for retry decisions and waits.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> onlyCodes(
      final Duration limit, final int... retry) {
    return limitAndCodes(limit, RetryStatusCodes.only(retry));
  }

  /**
   * Adds an {@link IntervalBiFunction} that respects any {@code Retry-After} header provided in the
   * response, without limit.
   *
   * <p><strong>CAUTION:</strong> The response may specify a {@code Retry-After} interval years in
   * the future. Skew between the local system clock and the clock on the service that generated the
   * header also may produce unexpectedly long wait durations. Implementations that do not set a
   * limit alternatively can defend against an unacceptable wait interval using e.g. a TimeLimiter
   * to terminate excessive waits and a Bulkhead to prevent too many outstanding requests.
   *
   * @return consumer that uses Retry-After for retry waits.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> retryAfter() {
    return Retry::heedRetryAfter;
  }

  /**
   * Adds an {@link IntervalBiFunction} that respects any {@code Retry-After} header provided in the
   * response, and adds a predicate to ensure the wait does not exceed the limit.
   *
   * @param limit the longest the client should wait.
   * @return consumer that uses Retry-After for retry decisions and waits.
   */
  public static Consumer<RetryConfig.Builder<HttpServletResponse>> retryAfter(
      final Duration limit) {
    return builder -> {
      builder.retryOnResult(LimitRetryAfter.maximum(limit));
      heedRetryAfter(builder);
    };
  }

  /**
   * Common code for several variations.
   *
   * @param limit the maximum wait interval that will be allowed by a {@code Retry-After}.
   * @param codes the HTTP status codes to retry
   * @return consumer that adds HTTP status code and Retry-After support.
   */
  private static Consumer<RetryConfig.Builder<HttpServletResponse>> limitAndCodes(
      final Duration limit, final RetryStatusCodes codes) {
    return builder -> {
      final LimitRetryAfter maximum = LimitRetryAfter.maximum(limit);
      builder.retryOnResult(codes.and(maximum));
      heedRetryAfter(builder);
    };
  }

  /**
   * Ugly way to decorate the existing {@link IntervalBiFunction}.
   *
   * @param builder the builder to decorate.
   */
  private static void heedRetryAfter(final RetryConfig.Builder<HttpServletResponse> builder) {
    // Builder lacks accessors, so have to instantiate
    final IntervalBiFunction<HttpServletResponse> original =
        builder.build().getIntervalBiFunction();
    builder.intervalBiFunction(HeedRetryAfter.heed(original));
  }
}