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.
*
* <p><strong>Apply these factories after setting the wait interval.</strong> The factories that
* heed the {@code Retry-After} header decorate the builder's {@link IntervalBiFunction}. {@link
* RetryConfig.Builder#waitDuration(Duration)} and {@link
* RetryConfig.Builder#intervalFunction(io.github.resilience4j.core.IntervalFunction)} replace that
* function rather than composing with it, so calling either one afterwards discards the {@code
* Retry-After} support without warning: the retry then waits the configured interval and ignores
* the header. Other builder methods, including {@link RetryConfig.Builder#maxAttempts(int)}, are
* unaffected and may be called in any order.
*
* <pre>{@code
* // Correct - wait interval first, factory last
* RetryConfig.Builder<HttpServletResponse> builder = RetryConfig.custom();
* builder.maxAttempts(3).waitDuration(Duration.ofSeconds(5));
* Retry.idempotent(Duration.ofSeconds(30)).accept(builder);
*
* // Wrong - waitDuration() discards the Retry-After support installed above
* Retry.idempotent(Duration.ofSeconds(30)).accept(builder);
* builder.waitDuration(Duration.ofSeconds(5)); // header now silently ignored
* }</pre>
*/
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.
* @apiNote Apply after {@link RetryConfig.Builder#waitDuration(Duration)}; see the class
* documentation for why order matters.
*/
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.
* @apiNote Apply after {@link RetryConfig.Builder#waitDuration(Duration)}; see the class
* documentation for why order matters.
*/
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.
* @apiNote Apply after {@link RetryConfig.Builder#waitDuration(Duration)}; see the class
* documentation for why order matters.
*/
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. A delay too large to express is
* clamped to {@link com.maybeitssquid.retry.RetryAfterParser#MAX_DELAY}, so without a limit a
* single hostile or misconfigured response can park a retry effectively forever. 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.
* @apiNote Apply after {@link RetryConfig.Builder#waitDuration(Duration)}; see the class
* documentation for why order matters.
*/
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.
* @apiNote Apply after {@link RetryConfig.Builder#waitDuration(Duration)}; see the class
* documentation for why order matters.
*/
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));
}
}