1212import dev .openfga .sdk .util .RetryStrategy ;
1313import java .io .IOException ;
1414import java .io .PrintStream ;
15- import java .net .http .HttpClient ;
16- import java .net .http .HttpRequest ;
17- import java .net .http .HttpResponse ;
15+ import java .net .http .*;
1816import java .nio .ByteBuffer ;
1917import java .nio .charset .StandardCharsets ;
20- import java .time .Duration ;
21- import java .util .HashMap ;
22- import java .util .Map ;
23- import java .util .Optional ;
18+ import java .time .*;
19+ import java .util .*;
2420import java .util .concurrent .*;
21+ import java .util .function .Function ;
2522
2623public class HttpRequestAttempt <T > {
2724 private final ApiClient apiClient ;
@@ -83,10 +80,10 @@ public CompletableFuture<ApiResponse<T>> attemptHttpRequest() throws ApiExceptio
8380 addTelemetryAttribute (Attributes .HTTP_REQUEST_METHOD , request .method ());
8481 addTelemetryAttribute (Attributes .USER_AGENT , configuration .getUserAgent ());
8582
86- return attemptHttpRequest (createClient (), 0 , null );
83+ return attemptHttpRequest (getHttpClient (), 0 , null );
8784 }
8885
89- private HttpClient createClient () {
86+ private HttpClient getHttpClient () {
9087 return apiClient .getHttpClient ();
9188 }
9289
@@ -99,10 +96,11 @@ private CompletableFuture<ApiResponse<T>> attemptHttpRequest(
9996 // Handle network errors (no HTTP response received)
10097 return handleNetworkError (throwable , retryNumber );
10198 }
102- // No network error, proceed with normal HTTP response handling
99+
100+ // Handle HTTP response (including error status codes)
103101 return processHttpResponse (response , retryNumber , previousError );
104102 })
105- .thenCompose (future -> future );
103+ .thenCompose (Function . identity () );
106104 }
107105
108106 private CompletableFuture <ApiResponse <T >> handleNetworkError (Throwable throwable , int retryNumber ) {
@@ -114,9 +112,7 @@ private CompletableFuture<ApiResponse<T>> handleNetworkError(Throwable throwable
114112 // Add telemetry for network error retry
115113 addTelemetryAttribute (Attributes .HTTP_REQUEST_RESEND_COUNT , String .valueOf (retryNumber + 1 ));
116114
117- // Create delayed client and retry asynchronously without blocking
118- HttpClient delayingClient = getDelayedHttpClient (retryDelay );
119- return attemptHttpRequest (delayingClient , retryNumber + 1 , throwable );
115+ return delayedRetry (retryDelay , retryNumber + 1 , throwable );
120116 } else {
121117 // Max retries exceeded, fail with the network error
122118 return CompletableFuture .failedFuture (new ApiException (throwable ));
@@ -129,9 +125,30 @@ private CompletableFuture<ApiResponse<T>> handleHttpErrorRetry(
129125 Duration retryDelay =
130126 RetryStrategy .calculateRetryDelay (retryAfterDelay , retryNumber , configuration .getMinimumRetryDelay ());
131127
132- // Create delayed client and retry asynchronously without blocking
133- HttpClient delayingClient = getDelayedHttpClient (retryDelay );
134- return attemptHttpRequest (delayingClient , retryNumber + 1 , error );
128+ return delayedRetry (retryDelay , retryNumber + 1 , error );
129+ }
130+
131+ /**
132+ * Performs a delayed retry using CompletableFuture.delayedExecutor().
133+ * This method centralizes the common delay logic used by both network error and HTTP error retries.
134+ *
135+ * @param retryDelay The duration to wait before retrying
136+ * @param nextRetryNumber The next retry attempt number (1-based)
137+ * @param previousError The previous error that caused the retry
138+ * @return CompletableFuture that completes after the delay with the retry attempt
139+ */
140+ private CompletableFuture <ApiResponse <T >> delayedRetry (
141+ Duration retryDelay , int nextRetryNumber , Throwable previousError ) {
142+ // Use CompletableFuture.delayedExecutor() to delay the retry attempt itself
143+ return CompletableFuture .runAsync (
144+ () -> {
145+ // No-op task, we only care about the delay timing
146+ },
147+ CompletableFuture .delayedExecutor (retryDelay .toNanos (), TimeUnit .NANOSECONDS ))
148+ .thenCompose (ignored -> {
149+ // Get HttpClient when needed (just returns cached instance)
150+ return attemptHttpRequest (getHttpClient (), nextRetryNumber , previousError );
151+ });
135152 }
136153
137154 private CompletableFuture <ApiResponse <T >> processHttpResponse (
@@ -150,7 +167,6 @@ private CompletableFuture<ApiResponse<T>> processHttpResponse(
150167 // Check if we should retry based on the new strategy
151168 if (RetryStrategy .shouldRetry (statusCode )) {
152169 return handleHttpErrorRetry (retryAfterDelay , retryNumber , error );
153- } else {
154170 }
155171 }
156172
@@ -196,18 +212,6 @@ private CompletableFuture<T> deserializeResponse(HttpResponse<String> response)
196212 }
197213 }
198214
199- private HttpClient getDelayedHttpClient (Duration retryDelay ) {
200- if (retryDelay == null || retryDelay .isZero () || retryDelay .isNegative ()) {
201- // Fallback to minimum retry delay if invalid
202- retryDelay = configuration .getMinimumRetryDelay ();
203- }
204-
205- return apiClient
206- .getHttpClientBuilder ()
207- .executor (CompletableFuture .delayedExecutor (retryDelay .toNanos (), TimeUnit .NANOSECONDS ))
208- .build ();
209- }
210-
211215 private static class BodyLogger implements Flow .Subscriber <ByteBuffer > {
212216 private final PrintStream out ;
213217 private final String target ;
0 commit comments