Skip to content

Commit 57bfd19

Browse files
committed
minimum retry delay config should be used as the base delay
1 parent f04d224 commit 57bfd19

File tree

6 files changed

+206
-35
lines changed

6 files changed

+206
-35
lines changed

src/main/java/dev/openfga/sdk/api/configuration/Configuration.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ public class Configuration implements BaseConfiguration {
3737
private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(10);
3838
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
3939
private static final int DEFAULT_MAX_RETRIES = 3;
40-
private static final Duration DEFAULT_MINIMUM_RETRY_DELAY = Duration.ofMillis(100);
4140
private static final int MAX_ALLOWABLE_RETRIES = 15;
4241

42+
/**
43+
* Default minimum retry delay of 100ms.
44+
* This value is also used as the default base delay for exponential backoff calculations.
45+
*/
46+
public static final Duration DEFAULT_MINIMUM_RETRY_DELAY = Duration.ofMillis(100);
47+
4348
private String apiUrl;
4449
private Credentials credentials;
4550
private String userAgent;

src/main/java/dev/openfga/sdk/util/ExponentialBackoff.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@
1919
* Utility class for calculating exponential backoff delays with jitter.
2020
*
2121
* Implements the retry strategy specified in GitHub issue #155:
22-
* - Base delay: 2^retryCount * 100ms
22+
* - Base delay: 2^retryCount * baseDelay (configurable)
2323
* - Jitter: Random value between base and 2 * base
2424
* - Maximum delay: 120 seconds (capped after 10th retry)
2525
*/
2626
public class ExponentialBackoff {
2727

28-
private static final int BASE_DELAY_MS = 100;
2928
private static final int MAX_DELAY_SECONDS = 120;
3029
private static final Random RANDOM = new Random();
3130

@@ -37,37 +36,45 @@ private ExponentialBackoff() {
3736
* Calculates the exponential backoff delay with jitter for a given retry attempt.
3837
*
3938
* @param retryCount The current retry attempt (0-based, so first retry is 0)
39+
* @param baseDelay The base delay to use for exponential calculation
4040
* @return Duration representing the delay before the next retry
4141
*/
42-
public static Duration calculateDelay(int retryCount) {
43-
return calculateDelay(retryCount, RANDOM);
42+
public static Duration calculateDelay(int retryCount, Duration baseDelay) {
43+
return calculateDelay(retryCount, baseDelay, RANDOM);
4444
}
4545

4646
/**
4747
* Calculates the exponential backoff delay with jitter using a provided Random instance.
4848
* This method is primarily for testing purposes to ensure deterministic behavior.
4949
*
5050
* @param retryCount The current retry attempt (0-based, so first retry is 0)
51+
* @param baseDelay The base delay to use for exponential calculation
5152
* @param random Random instance to use for jitter calculation
5253
* @return Duration representing the delay before the next retry
5354
*/
54-
static Duration calculateDelay(int retryCount, Random random) {
55+
static Duration calculateDelay(int retryCount, Duration baseDelay, Random random) {
5556
if (retryCount < 0) {
5657
return Duration.ZERO;
5758
}
5859

59-
// Calculate base delay: 2^retryCount * 100ms
60-
long baseDelayMs = (long) Math.pow(2, retryCount) * BASE_DELAY_MS;
60+
// Use provided base delay (caller must provide a valid value)
61+
if (baseDelay == null) {
62+
throw new IllegalArgumentException("baseDelay cannot be null");
63+
}
64+
long baseDelayMs = baseDelay.toMillis();
65+
66+
// Calculate exponential delay: 2^retryCount * baseDelay
67+
long exponentialDelayMs = (long) Math.pow(2, retryCount) * baseDelayMs;
6168

6269
// Cap at maximum delay
6370
long maxDelayMs = MAX_DELAY_SECONDS * 1000L;
64-
if (baseDelayMs > maxDelayMs) {
65-
baseDelayMs = maxDelayMs;
71+
if (exponentialDelayMs > maxDelayMs) {
72+
exponentialDelayMs = maxDelayMs;
6673
}
6774

68-
// Add jitter: random value between baseDelay and 2 * baseDelay
69-
long minDelayMs = baseDelayMs;
70-
long maxDelayMsWithJitter = Math.min(baseDelayMs * 2, maxDelayMs);
75+
// Add jitter: random value between exponentialDelay and 2 * exponentialDelay
76+
long minDelayMs = exponentialDelayMs;
77+
long maxDelayMsWithJitter = Math.min(exponentialDelayMs * 2, maxDelayMs);
7178

7279
// Generate random delay within the jitter range
7380
long jitterRange = maxDelayMsWithJitter - minDelayMs;

src/main/java/dev/openfga/sdk/util/RetryStrategy.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
package dev.openfga.sdk.util;
1414

15+
import dev.openfga.sdk.api.configuration.Configuration;
1516
import dev.openfga.sdk.errors.HttpStatusCode;
1617
import java.time.Duration;
1718
import java.util.Optional;
@@ -60,7 +61,7 @@ public static boolean shouldRetry(int statusCode) {
6061
*
6162
* @param retryAfterDelay Optional delay from Retry-After header
6263
* @param retryCount Current retry attempt (0-based)
63-
* @param minimumRetryDelay Minimum delay to enforce (can be null)
64+
* @param minimumRetryDelay Minimum delay to enforce (also used as base delay for exponential backoff)
6465
* @return Duration representing the delay before the next retry
6566
*/
6667
public static Duration calculateRetryDelay(
@@ -75,11 +76,8 @@ public static Duration calculateRetryDelay(
7576
return retryAfterValue;
7677
}
7778

78-
// Otherwise, use exponential backoff with jitter, respecting minimum retry delay
79-
Duration exponentialDelay = ExponentialBackoff.calculateDelay(retryCount);
80-
if (minimumRetryDelay != null && minimumRetryDelay.compareTo(exponentialDelay) > 0) {
81-
return minimumRetryDelay;
82-
}
83-
return exponentialDelay;
79+
// Otherwise, use exponential backoff with jitter, respecting minimum retry delay
80+
Duration baseDelay = minimumRetryDelay != null ? minimumRetryDelay : Configuration.DEFAULT_MINIMUM_RETRY_DELAY;
81+
return ExponentialBackoff.calculateDelay(retryCount, baseDelay);
8482
}
8583
}

src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,10 @@ void shouldUseExponentialBackoffWhenNoRetryAfterHeader() throws Exception {
283283
// Verify both requests were made
284284
wireMockServer.verify(2, getRequestedFor(urlEqualTo("/test")));
285285

286-
// Verify some delay occurred (exponential backoff should add at least 100ms for first retry)
286+
// Verify some delay occurred (exponential backoff with 10ms minimum delay)
287287
// Note: Using a generous range due to test timing variability
288-
assertThat(endTime - startTime).isGreaterThan(400L);
288+
// With minimumRetryDelay=10ms, first retry should be at least 10ms
289+
assertThat(endTime - startTime).isGreaterThan(8L);
289290
}
290291

291292
@Test

src/test/java/dev/openfga/sdk/util/ExponentialBackoffTest.java

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package dev.openfga.sdk.util;
1414

1515
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1617

1718
import java.time.Duration;
1819
import java.util.Random;
@@ -24,10 +25,11 @@ class ExponentialBackoffTest {
2425
void calculateDelay_withRetryCountZero_shouldReturnBaseDelay() {
2526
// Given
2627
int retryCount = 0;
28+
Duration baseDelay = Duration.ofMillis(100);
2729
Random fixedRandom = new Random(42); // Fixed seed for deterministic testing
2830

2931
// When
30-
Duration result = ExponentialBackoff.calculateDelay(retryCount, fixedRandom);
32+
Duration result = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom);
3133

3234
// Then
3335
// For retry count 0: 2^0 * 100ms = 100ms base
@@ -39,10 +41,11 @@ void calculateDelay_withRetryCountZero_shouldReturnBaseDelay() {
3941
void calculateDelay_withRetryCountOne_shouldReturnDoubledDelay() {
4042
// Given
4143
int retryCount = 1;
44+
Duration baseDelay = Duration.ofMillis(100);
4245
Random fixedRandom = new Random(42); // Fixed seed for deterministic testing
4346

4447
// When
45-
Duration result = ExponentialBackoff.calculateDelay(retryCount, fixedRandom);
48+
Duration result = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom);
4649

4750
// Then
4851
// For retry count 1: 2^1 * 100ms = 200ms base
@@ -54,10 +57,11 @@ void calculateDelay_withRetryCountOne_shouldReturnDoubledDelay() {
5457
void calculateDelay_withRetryCountTwo_shouldReturnQuadrupledDelay() {
5558
// Given
5659
int retryCount = 2;
60+
Duration baseDelay = Duration.ofMillis(100);
5761
Random fixedRandom = new Random(42); // Fixed seed for deterministic testing
5862

5963
// When
60-
Duration result = ExponentialBackoff.calculateDelay(retryCount, fixedRandom);
64+
Duration result = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom);
6165

6266
// Then
6367
// For retry count 2: 2^2 * 100ms = 400ms base
@@ -69,10 +73,11 @@ void calculateDelay_withRetryCountTwo_shouldReturnQuadrupledDelay() {
6973
void calculateDelay_withHighRetryCount_shouldCapAtMaximum() {
7074
// Given
7175
int retryCount = 10; // This would normally result in 2^10 * 100ms = 102400ms
76+
Duration baseDelay = Duration.ofMillis(100);
7277
Random fixedRandom = new Random(42); // Fixed seed for deterministic testing
7378

7479
// When
75-
Duration result = ExponentialBackoff.calculateDelay(retryCount, fixedRandom);
80+
Duration result = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom);
7681

7782
// Then
7883
// Should be capped at 120 seconds (120000ms)
@@ -85,7 +90,7 @@ void calculateDelay_withNegativeRetryCount_shouldReturnZero() {
8590
int retryCount = -1;
8691

8792
// When
88-
Duration result = ExponentialBackoff.calculateDelay(retryCount);
93+
Duration result = ExponentialBackoff.calculateDelay(retryCount, Duration.ofMillis(100));
8994

9095
// Then
9196
assertThat(result).isEqualTo(Duration.ZERO);
@@ -97,7 +102,7 @@ void calculateDelay_withoutRandomParameter_shouldReturnValidRange() {
97102
int retryCount = 1;
98103

99104
// When
100-
Duration result = ExponentialBackoff.calculateDelay(retryCount);
105+
Duration result = ExponentialBackoff.calculateDelay(retryCount, Duration.ofMillis(100));
101106

102107
// Then
103108
// For retry count 1: 2^1 * 100ms = 200ms base
@@ -111,9 +116,9 @@ void calculateDelay_shouldProduceVariousResults() {
111116
int retryCount = 1;
112117

113118
// When - call multiple times to ensure randomness
114-
Duration result1 = ExponentialBackoff.calculateDelay(retryCount);
115-
Duration result2 = ExponentialBackoff.calculateDelay(retryCount);
116-
Duration result3 = ExponentialBackoff.calculateDelay(retryCount);
119+
Duration result1 = ExponentialBackoff.calculateDelay(retryCount, Duration.ofMillis(100));
120+
Duration result2 = ExponentialBackoff.calculateDelay(retryCount, Duration.ofMillis(100));
121+
Duration result3 = ExponentialBackoff.calculateDelay(retryCount, Duration.ofMillis(100));
117122

118123
// Then - all should be in valid range but likely different
119124
assertThat(result1.toMillis()).isBetween(200L, 400L);
@@ -125,12 +130,13 @@ void calculateDelay_shouldProduceVariousResults() {
125130
void calculateDelay_withFixedRandom_shouldBeDeterministic() {
126131
// Given
127132
int retryCount = 1;
133+
Duration baseDelay = Duration.ofMillis(100);
128134
Random fixedRandom1 = new Random(123);
129135
Random fixedRandom2 = new Random(123); // Same seed
130136

131137
// When
132-
Duration result1 = ExponentialBackoff.calculateDelay(retryCount, fixedRandom1);
133-
Duration result2 = ExponentialBackoff.calculateDelay(retryCount, fixedRandom2);
138+
Duration result1 = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom1);
139+
Duration result2 = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom2);
134140

135141
// Then
136142
assertThat(result1).isEqualTo(result2);
@@ -139,13 +145,14 @@ void calculateDelay_withFixedRandom_shouldBeDeterministic() {
139145
@Test
140146
void calculateDelay_progressionTest_shouldFollowExponentialPattern() {
141147
// Given
148+
Duration baseDelay = Duration.ofMillis(100);
142149
Random fixedRandom = new Random(42);
143150

144151
// When & Then - test the progression follows expected pattern
145152
for (int i = 0; i < 8; i++) {
146153
// Reset the random seed for consistent results across iterations
147154
fixedRandom.setSeed(42);
148-
Duration delay = ExponentialBackoff.calculateDelay(i, fixedRandom);
155+
Duration delay = ExponentialBackoff.calculateDelay(i, baseDelay, fixedRandom);
149156
long expectedBaseMs = (long) Math.pow(2, i) * 100;
150157
long expectedMaxMs = Math.min(expectedBaseMs * 2, 120000);
151158

@@ -160,13 +167,43 @@ void calculateDelay_progressionTest_shouldFollowExponentialPattern() {
160167
void calculateDelay_atCapThreshold_shouldCapCorrectly() {
161168
// Given - retry count that would exceed 120s base delay
162169
int retryCount = 11; // 2^11 * 100ms = 204800ms > 120000ms
170+
Duration baseDelay = Duration.ofMillis(100);
163171
Random fixedRandom = new Random(42);
164172

165173
// When
166-
Duration result = ExponentialBackoff.calculateDelay(retryCount, fixedRandom);
174+
Duration result = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom);
167175

168176
// Then - should be capped at 120s maximum
169177
assertThat(result.toMillis()).isLessThanOrEqualTo(120000L);
170178
assertThat(result.toMillis()).isGreaterThanOrEqualTo(120000L); // Should be exactly at cap for base delay
171179
}
180+
181+
@Test
182+
void calculateDelay_withCustomBaseDelay_shouldUseConfigurableBase() {
183+
// Given - custom base delay of 500ms
184+
int retryCount = 1;
185+
Duration baseDelay = Duration.ofMillis(500);
186+
Random fixedRandom = new Random(42);
187+
188+
// When
189+
Duration result = ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom);
190+
191+
// Then
192+
// For retry count 1 with 500ms base: 2^1 * 500ms = 1000ms base
193+
// With jitter: between 1000ms and 2000ms
194+
assertThat(result.toMillis()).isBetween(1000L, 2000L);
195+
}
196+
197+
@Test
198+
void calculateDelay_withNullBaseDelay_shouldThrowException() {
199+
// Given
200+
int retryCount = 0;
201+
Duration baseDelay = null;
202+
Random fixedRandom = new Random(42);
203+
204+
// When & Then
205+
assertThatThrownBy(() -> ExponentialBackoff.calculateDelay(retryCount, baseDelay, fixedRandom))
206+
.isInstanceOf(IllegalArgumentException.class)
207+
.hasMessage("baseDelay cannot be null");
208+
}
172209
}

0 commit comments

Comments
 (0)