RetryPolicy.java
package com.shengzhiai.yugu;
import java.util.Random;
/**
* Retry policy of the REST calls (requirement A-02).
*
* <pre>
* delay(n) = min(maxDelayMs, initialDelayMs * multiplier^(n-1)) * (1 + U(-jitter, +jitter)) n = 1..maxRetries
* with Retry-After and respectRetryAfter: delay = max(delay, min(retryAfterMs, maxRetryAfterMs))
* a retry whose wait would end after the total timeout is not made; the last error is thrown
* </pre>
*
* <p>Defaults: 2 retries (3 attempts), 200 ms, factor 2, at most 4000 ms, jitter 0.3, Retry-After
* honoured up to 30 s. That gives about 200 ms and 400 ms, each plus or minus 30 %.
*
* <p>Only calls with an idempotency key (generated by default) and the naturally idempotent report
* query are retried, and only for errors where {@link com.shengzhiai.yugu.errors.YuguErrors#isRetryable}
* is true. Every retry is reported to {@link EventListener#onRetry} and logged at WARN.
*/
public final class RetryPolicy {
private static final RetryPolicy DEFAULTS = builder().build();
private static final RetryPolicy NONE = builder().maxRetries(0).build();
private final int maxRetries;
private final long initialDelayMs;
private final double multiplier;
private final long maxDelayMs;
private final double jitter;
private final boolean respectRetryAfter;
private final long maxRetryAfterMs;
private RetryPolicy(Builder b) {
if (b.maxRetries < 0) {
throw new IllegalArgumentException("maxRetries must be >= 0");
}
if (b.initialDelayMs < 0 || b.maxDelayMs < 0 || b.maxRetryAfterMs < 0) {
throw new IllegalArgumentException("delays must be >= 0");
}
if (b.multiplier < 1.0) {
throw new IllegalArgumentException("multiplier must be >= 1");
}
if (b.jitter < 0 || b.jitter > 1) {
throw new IllegalArgumentException("jitter must be in [0, 1]");
}
this.maxRetries = b.maxRetries;
this.initialDelayMs = b.initialDelayMs;
this.multiplier = b.multiplier;
this.maxDelayMs = b.maxDelayMs;
this.jitter = b.jitter;
this.respectRetryAfter = b.respectRetryAfter;
this.maxRetryAfterMs = b.maxRetryAfterMs;
}
/** @return the default policy */
public static RetryPolicy defaults() {
return DEFAULTS;
}
/** @return a policy without retries */
public static RetryPolicy none() {
return NONE;
}
/** @return a builder with the defaults */
public static Builder builder() {
return new Builder();
}
/** @return a builder initialised with this policy */
public Builder toBuilder() {
Builder b = new Builder();
b.maxRetries = maxRetries;
b.initialDelayMs = initialDelayMs;
b.multiplier = multiplier;
b.maxDelayMs = maxDelayMs;
b.jitter = jitter;
b.respectRetryAfter = respectRetryAfter;
b.maxRetryAfterMs = maxRetryAfterMs;
return b;
}
/**
* Backoff before retry {@code n} without jitter: {@code min(maxDelayMs, initialDelayMs * multiplier^(n-1))}.
*
* @param n retry number, from 1
* @return delay in milliseconds
*/
public long baseDelayMs(int n) {
double d = initialDelayMs * Math.pow(multiplier, Math.max(0, n - 1));
return (long) Math.min((double) maxDelayMs, d);
}
/**
* Full delay before retry {@code n}.
*
* @param n retry number, from 1
* @param unitRandom uniform random value in [0, 1); 0.5 means no jitter
* @param retryAfterMs server Retry-After in milliseconds, -1 when absent
* @return delay in milliseconds
*/
public long delayMs(int n, double unitRandom, long retryAfterMs) {
double u = (unitRandom * 2 - 1) * jitter;
long delay = Math.round(baseDelayMs(n) * (1 + u));
if (respectRetryAfter && retryAfterMs >= 0) {
delay = Math.max(delay, Math.min(retryAfterMs, maxRetryAfterMs));
}
return Math.max(0, delay);
}
/**
* @param n retry number, from 1
* @param random random source
* @param retryAfterMs server Retry-After in milliseconds, -1 when absent
* @return delay in milliseconds
*/
public long delayMs(int n, Random random, long retryAfterMs) {
return delayMs(n, random.nextDouble(), retryAfterMs);
}
/** @return retries after the first attempt */
public int getMaxRetries() {
return maxRetries;
}
/** @return first backoff */
public long getInitialDelayMs() {
return initialDelayMs;
}
/** @return backoff factor */
public double getMultiplier() {
return multiplier;
}
/** @return backoff cap before jitter */
public long getMaxDelayMs() {
return maxDelayMs;
}
/** @return relative jitter */
public double getJitter() {
return jitter;
}
/** @return whether Retry-After is honoured */
public boolean isRespectRetryAfter() {
return respectRetryAfter;
}
/** @return cap applied to Retry-After */
public long getMaxRetryAfterMs() {
return maxRetryAfterMs;
}
@Override
public String toString() {
return "RetryPolicy{maxRetries=" + maxRetries + ", initialDelayMs=" + initialDelayMs + ", multiplier=" + multiplier
+ ", maxDelayMs=" + maxDelayMs + ", jitter=" + jitter + ", respectRetryAfter=" + respectRetryAfter
+ ", maxRetryAfterMs=" + maxRetryAfterMs + '}';
}
/** Builder for {@link RetryPolicy}. */
public static final class Builder {
private int maxRetries = 2;
private long initialDelayMs = 200;
private double multiplier = 2.0;
private long maxDelayMs = 4000;
private double jitter = 0.3;
private boolean respectRetryAfter = true;
private long maxRetryAfterMs = 30000;
private Builder() {
}
/** @param v retries after the first attempt, default 2 @return this */
public Builder maxRetries(int v) {
this.maxRetries = v;
return this;
}
/** @param v first backoff, default 200 ms @return this */
public Builder initialDelayMs(long v) {
this.initialDelayMs = v;
return this;
}
/** @param v backoff factor, default 2.0 @return this */
public Builder multiplier(double v) {
this.multiplier = v;
return this;
}
/** @param v backoff cap before jitter, default 4000 ms @return this */
public Builder maxDelayMs(long v) {
this.maxDelayMs = v;
return this;
}
/** @param v relative jitter in [0, 1], default 0.3 @return this */
public Builder jitter(double v) {
this.jitter = v;
return this;
}
/** @param v honour Retry-After, default true @return this */
public Builder respectRetryAfter(boolean v) {
this.respectRetryAfter = v;
return this;
}
/** @param v cap for Retry-After, default 30000 ms @return this */
public Builder maxRetryAfterMs(long v) {
this.maxRetryAfterMs = v;
return this;
}
/** @return the policy */
public RetryPolicy build() {
return new RetryPolicy(this);
}
}
}