This commit is contained in:
2026-09-29 17:55:54 +08:00
parent 41d8387cd5
commit 54ad26c607
111 changed files with 1837 additions and 962 deletions
@@ -0,0 +1,31 @@
package com.baiyue.common.core.retry;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
/** Bounded exponential retry schedule; callers persist failed or expired attempts. */
public record BoundedRetryPolicy(int maxAttempts, long baseDelaySeconds, long maxDelaySeconds, double jitter) {
public BoundedRetryPolicy {
if (maxAttempts < 1 || baseDelaySeconds < 1 || maxDelaySeconds < baseDelaySeconds
|| maxDelaySeconds > 86400 || !Double.isFinite(jitter) || jitter < 0 || jitter > 0.5) {
throw new IllegalArgumentException("Invalid bounded retry policy");
}
}
public boolean exhausted(int failedAttempts) {
return failedAttempts >= maxAttempts;
}
public Duration delayAfterFailure(int failedAttempts) {
return delayAfterFailure(failedAttempts, ThreadLocalRandom.current().nextDouble());
}
public Duration delayAfterFailure(int failedAttempts, double random) {
if (failedAttempts < 1 || !Double.isFinite(random) || random < 0 || random > 1) {
throw new IllegalArgumentException("Invalid retry attempt or jitter sample");
}
long seconds = baseDelaySeconds;
for (int i = 1; i < failedAttempts && seconds < maxDelaySeconds; i++) {
seconds = Math.min(maxDelaySeconds, seconds * 2);
}
long millis = Math.round(seconds * 1000 * (1 - jitter + 2 * jitter * random));
return Duration.ofMillis(Math.min(maxDelaySeconds * 1000, Math.max(1, millis)));
}
}
@@ -0,0 +1,31 @@
package com.baiyue.common.core;
import com.baiyue.common.core.retry.BoundedRetryPolicy;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class BoundedRetryPolicyTest {
@Test void startsAtFiveSecondsDoublesAndCapsAtThirtyMinutes() {
var policy = new BoundedRetryPolicy(12, 5, 1800, 0);
assertThat(policy.delayAfterFailure(1).toSeconds()).isEqualTo(5);
assertThat(policy.delayAfterFailure(2).toSeconds()).isEqualTo(10);
assertThat(policy.delayAfterFailure(9).toSeconds()).isEqualTo(1280);
assertThat(policy.delayAfterFailure(10).toSeconds()).isEqualTo(1800);
assertThat(policy.delayAfterFailure(Integer.MAX_VALUE).toSeconds()).isEqualTo(1800);
assertThat(policy.exhausted(11)).isFalse();
assertThat(policy.exhausted(12)).isTrue();
}
@Test void jitterStaysBoundedEvenAtMaximumDelay() {
var policy = new BoundedRetryPolicy(12, 5, 1800, 0.1);
assertThat(policy.delayAfterFailure(1, 0).toMillis()).isEqualTo(4500);
assertThat(policy.delayAfterFailure(1, 1).toMillis()).isEqualTo(5500);
assertThat(policy.delayAfterFailure(20, 1).toSeconds()).isEqualTo(1800);
assertThat(policy.delayAfterFailure(20, 0).toSeconds()).isEqualTo(1620);
}
@Test void invalidConfigurationFailsClosed() {
assertThatThrownBy(() -> new BoundedRetryPolicy(0, 5, 1800, 0.1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new BoundedRetryPolicy(12, 0, 1800, 0.1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new BoundedRetryPolicy(12, 5, 3, 0.1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new BoundedRetryPolicy(12, 5, 1800, Double.NaN)).isInstanceOf(IllegalArgumentException.class);
}
}