Skip to content

Commit 1542bb0

Browse files
committed
feat: improve retry strategy
1 parent 59ee8dc commit 1542bb0

File tree

4 files changed

+125
-6
lines changed

4 files changed

+125
-6
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,9 +965,11 @@ fgaClient.writeAssertions(assertions, options).get();
965965
966966
### Retries
967967
968-
If a network request fails with a 429 or 5xx error from the server, the SDK will automatically retry the request up to 3 times with a minimum wait time of 100 milliseconds between each attempt.
968+
If a network request fails with a 429 or 5xx error from the server, the SDK will automatically retry the request up to 3 times.
969+
When the response includes a `Retry-After` header, the delay specified by the header will be used (up to 30 minutes).
970+
Otherwise the wait time follows an exponential backoff with jitter starting at 100 milliseconds and capped at 120 seconds.
969971
970-
To customize this behavior, call `maxRetries` and `minimumRetryDelay` on the `ClientConfiguration` builder. `maxRetries` determines the maximum number of retries (up to 15), while `minimumRetryDelay` sets the minimum wait time between retries in milliseconds.
972+
To customize this behavior, call `maxRetries` and `minimumRetryDelay` on the `ClientConfiguration` builder. `maxRetries` determines the maximum number of retries (up to 15), while `minimumRetryDelay` sets the base delay used to calculate the exponential backoff.
971973
972974
```java
973975
import com.fasterxml.jackson.databind.ObjectMapper;

src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import dev.openfga.sdk.telemetry.Attribute;
99
import dev.openfga.sdk.telemetry.Attributes;
1010
import dev.openfga.sdk.telemetry.Telemetry;
11+
import dev.openfga.sdk.util.Retry;
1112
import java.io.IOException;
1213
import java.io.PrintStream;
1314
import java.net.http.HttpClient;
@@ -102,7 +103,8 @@ private CompletableFuture<ApiResponse<T>> attemptHttpRequest(
102103
if (HttpStatusCode.isRetryable(error.getStatusCode())
103104
&& retryNumber < configuration.getMaxRetries()) {
104105

105-
HttpClient delayingClient = getDelayedHttpClient();
106+
Duration retryDelay = computeRetryDelay(response, retryNumber);
107+
HttpClient delayingClient = getDelayedHttpClient(retryDelay);
106108

107109
return attemptHttpRequest(delayingClient, retryNumber + 1, error);
108110
}
@@ -151,15 +153,30 @@ private CompletableFuture<T> deserializeResponse(HttpResponse<String> response)
151153
}
152154
}
153155

154-
private HttpClient getDelayedHttpClient() {
155-
Duration retryDelay = configuration.getMinimumRetryDelay();
156-
156+
private HttpClient getDelayedHttpClient(Duration retryDelay) {
157157
return apiClient
158158
.getHttpClientBuilder()
159159
.executor(CompletableFuture.delayedExecutor(retryDelay.toNanos(), TimeUnit.NANOSECONDS))
160160
.build();
161161
}
162162

163+
private Duration computeRetryDelay(HttpResponse<String> response, int retryNumber) {
164+
Duration retryAfter = response.headers()
165+
.firstValue("Retry-After")
166+
.map(Retry::parseRetryAfter)
167+
.orElse(null);
168+
169+
if (retryAfter != null) {
170+
return retryAfter;
171+
}
172+
173+
Duration baseDelay = configuration.getMinimumRetryDelay();
174+
if (baseDelay == null) {
175+
baseDelay = Duration.ofMillis(100);
176+
}
177+
return Retry.computeExponentialDelay(baseDelay, retryNumber);
178+
}
179+
163180
private static class BodyLogger implements Flow.Subscriber<ByteBuffer> {
164181
private final PrintStream out;
165182
private final String target;

src/main/java/dev/openfga/sdk/errors/FgaError.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import dev.openfga.sdk.api.configuration.Configuration;
66
import dev.openfga.sdk.api.configuration.CredentialsMethod;
7+
import dev.openfga.sdk.util.Retry;
78
import java.net.http.HttpHeaders;
89
import java.net.http.HttpRequest;
910
import java.net.http.HttpResponse;
@@ -17,6 +18,7 @@ public class FgaError extends ApiException {
1718
private String grantType = null;
1819
private String requestId = null;
1920
private String apiErrorCode = null;
21+
private Long retryAfterSeconds = null;
2022

2123
public FgaError(String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
2224
super(message, cause, code, responseHeaders, responseBody);
@@ -57,6 +59,10 @@ public static Optional<FgaError> getError(
5759
error = new FgaError(name, previousError, status, headers, body);
5860
}
5961

62+
headers.firstValue("Retry-After")
63+
.flatMap(value -> Optional.ofNullable(Retry.parseRetryAfterSeconds(value)))
64+
.ifPresent(error::setRetryAfterSeconds);
65+
6066
error.setMethod(request.method());
6167
error.setRequestUrl(configuration.getApiUrl());
6268

@@ -126,4 +132,12 @@ public void setApiErrorCode(String apiErrorCode) {
126132
public String getApiErrorCode() {
127133
return apiErrorCode;
128134
}
135+
136+
public void setRetryAfterSeconds(Long retryAfterSeconds) {
137+
this.retryAfterSeconds = retryAfterSeconds;
138+
}
139+
140+
public Long getRetryAfterSeconds() {
141+
return retryAfterSeconds;
142+
}
129143
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* OpenFGA
3+
* A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar.
4+
*
5+
* The version of the OpenAPI document: 1.x
6+
* Contact: community@openfga.dev
7+
*
8+
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
9+
* https://openapi-generator.tech
10+
* Do not edit the class manually.
11+
*/
12+
13+
package dev.openfga.sdk.util;
14+
15+
import java.time.Duration;
16+
import java.time.OffsetDateTime;
17+
import java.time.format.DateTimeFormatter;
18+
import java.time.format.DateTimeParseException;
19+
import java.util.concurrent.ThreadLocalRandom;
20+
21+
/**
22+
* Helper methods for retry strategies.
23+
*/
24+
public class Retry {
25+
private Retry() {}
26+
27+
/**
28+
* Parse the Retry-After header value to a Duration in seconds.
29+
* Returns null if the value is invalid or outside the allowed range (1s-30min).
30+
*/
31+
public static Duration parseRetryAfter(String headerValue) {
32+
if (StringUtil.isNullOrWhitespace(headerValue)) {
33+
return null;
34+
}
35+
36+
String value = headerValue.trim();
37+
// Try integer seconds first
38+
try {
39+
long secs = Long.parseLong(value);
40+
if (secs < 1 || secs > 1800) {
41+
return null;
42+
}
43+
return Duration.ofSeconds(secs);
44+
} catch (NumberFormatException ex) {
45+
// try HTTP date
46+
try {
47+
OffsetDateTime date = OffsetDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);
48+
long secs = Duration.between(OffsetDateTime.now(), date).getSeconds();
49+
if (secs < 1 || secs > 1800) {
50+
return null;
51+
}
52+
return Duration.ofSeconds(secs);
53+
} catch (DateTimeParseException e) {
54+
return null;
55+
}
56+
}
57+
}
58+
59+
/**
60+
* Parse Retry-After header value returning seconds as Long. Returns null when invalid.
61+
*/
62+
public static Long parseRetryAfterSeconds(String headerValue) {
63+
Duration d = parseRetryAfter(headerValue);
64+
return d != null ? d.getSeconds() : null;
65+
}
66+
67+
/**
68+
* Compute an exponential backoff with jitter based on the retry number.
69+
* The result is capped at 120 seconds.
70+
*/
71+
public static Duration computeExponentialDelay(Duration baseDelay, int retryNumber) {
72+
long baseMillis = baseDelay.toMillis();
73+
long minMillis = (long) Math.pow(2, retryNumber) * baseMillis;
74+
long maxMillis = (long) Math.pow(2, retryNumber + 1) * baseMillis;
75+
long cap = 120_000L; // 120 seconds
76+
minMillis = Math.min(minMillis, cap);
77+
maxMillis = Math.min(maxMillis, cap);
78+
long delay;
79+
if (minMillis == maxMillis) {
80+
delay = minMillis;
81+
} else {
82+
delay = ThreadLocalRandom.current().nextLong(minMillis, maxMillis + 1);
83+
}
84+
return Duration.ofMillis(delay);
85+
}
86+
}

0 commit comments

Comments
 (0)