4
4
*/
5
5
package com .ibm .watsonx .ai .core .http .interceptors ;
6
6
7
+ import static com .ibm .watsonx .ai .core .http .BaseHttpClient .REQUEST_ID_HEADER ;
7
8
import static java .util .Objects .isNull ;
8
9
import static java .util .Objects .requireNonNull ;
9
10
import static java .util .Objects .requireNonNullElse ;
@@ -34,11 +35,15 @@ public final class RetryInterceptor implements SyncHttpInterceptor, AsyncHttpInt
34
35
35
36
private static final Logger logger = LoggerFactory .getLogger (RetryInterceptor .class );
36
37
38
+ private record RetryOn (Class <? extends Throwable > clazz , Optional <Predicate <Throwable >> predicate ) {}
39
+
40
+ private final Duration retryInterval ;
41
+ private final List <RetryOn > retryOn ;
42
+ private final boolean exponentialBackoff ;
43
+ private final Integer maxRetries ;
44
+
37
45
/**
38
46
* Checks whether a {@link WatsonxException} is retryable due to an expired authentication token.
39
- * <p>
40
- * This condition is met if the HTTP status code is 401 and at least one error in the exception's details has the code
41
- * {@code AUTHENTICATION_TOKEN_EXPIRED}.
42
47
*
43
48
* @param maxRetries the maximum number of retry attempts
44
49
* @return a configured {@link RetryInterceptor} instance that handles token expiration retries
@@ -59,28 +64,19 @@ public static RetryInterceptor onTokenExpired(int maxRetries) {
59
64
).build ();
60
65
}
61
66
62
- private record RetryOn (Class <? extends Throwable > clazz , Optional <Predicate <Throwable >> predicate ) {}
63
-
64
- private final Duration retryInterval ;
65
- private final List <RetryOn > retryOn ;
66
- private final boolean exponentialBackoff ;
67
- private Integer maxRetries ;
68
- private Duration timeout ;
69
-
70
67
/**
71
68
* Creates a new {@code RetryInterceptor} using the provided builder.
72
69
*
73
70
* @param builder the builder instance
74
71
*/
75
72
public RetryInterceptor (Builder builder ) {
76
73
requireNonNull (builder );
77
- this .retryInterval = requireNonNullElse (builder .retryInterval , Duration .ofMillis (0 ));
78
- this .timeout = this .retryInterval ;
79
- this .maxRetries = requireNonNullElse (builder .maxRetries , 1 );
80
- this .retryOn = builder .retryOn ;
81
- this .exponentialBackoff = builder .exponentialBackoff ;
82
- if (isNull (retryOn ) || retryOn .isEmpty ())
83
- throw new RuntimeException ("At least one exception must be specified" );
74
+ retryInterval = requireNonNullElse (builder .retryInterval , Duration .ofMillis (0 ));
75
+ maxRetries = requireNonNullElse (builder .maxRetries , 1 );
76
+ retryOn = requireNonNull (builder .retryOn , "At least one exception must be specified" );
77
+ exponentialBackoff = builder .exponentialBackoff ;
78
+ if (exponentialBackoff && !retryInterval .isPositive ())
79
+ throw new IllegalArgumentException ("Retry interval must be positive when exponential backoff is enabled" );
84
80
}
85
81
86
82
@ Override
@@ -89,15 +85,23 @@ public <T> HttpResponse<T> intercept(HttpRequest request, BodyHandler<T> bodyHan
89
85
90
86
Throwable exception = null ;
91
87
88
+ String requestId = request .headers ()
89
+ .firstValue (REQUEST_ID_HEADER )
90
+ .orElseThrow (); // This should never happen. The SyncHttpClient and AsyncHttpClient add this header if it is not present.
91
+
92
+ Duration timeout = Duration .from (retryInterval );
93
+
92
94
for (int attempt = 0 ; attempt <= maxRetries ; attempt ++) {
93
95
94
96
try {
95
97
96
- if (attempt > 0 )
97
- Thread .sleep (timeout .toMillis ());
98
+ if (attempt > 0 && timeout .isPositive ()) {
99
+ logger .debug ("Retry request \" {}\" after {} ms" , requestId , timeout .toMillis ());
100
+ Thread .sleep (timeout );
101
+ }
98
102
99
103
var res = chain .proceed (request , bodyHandler );
100
- this . timeout = this . retryInterval ;
104
+ timeout = Duration . from ( retryInterval ) ;
101
105
return res ;
102
106
103
107
} catch (Exception e ) {
@@ -117,54 +121,39 @@ public <T> HttpResponse<T> intercept(HttpRequest request, BodyHandler<T> bodyHan
117
121
timeout = timeout .multipliedBy (2 );
118
122
}
119
123
if (attempt > 0 ) {
120
- logger .debug ("Retrying request ({}/{}) after failure: {}" , attempt , maxRetries ,
124
+ logger .debug ("Retrying request \" {} \" ({}/{}) after failure: {}" , requestId , attempt , maxRetries ,
121
125
exception .getMessage ());
122
126
}
123
127
chain .resetToIndex (index + 1 );
124
128
continue ;
125
129
}
126
130
127
- this . timeout = this . retryInterval ;
131
+ timeout = Duration . from ( retryInterval ) ;
128
132
throw e ;
129
133
}
130
134
}
131
135
132
- this .timeout = this .retryInterval ;
133
- logger .debug ("Max retries reached" );
134
-
135
- throw new RuntimeException ("Max retries reached" , isNull (exception ) ? new Exception () : exception );
136
+ timeout = Duration .from (retryInterval );
137
+ throw new RuntimeException ("Max retries reached for request [%s]" .formatted (requestId ), isNull (exception ) ? new Exception () : exception );
136
138
}
137
139
138
140
@ Override
139
141
public <T > CompletableFuture <HttpResponse <T >> intercept (HttpRequest request , BodyHandler <T > bodyHandler ,
140
142
Executor executor , int index , AsyncChain chain ) {
141
- return executeWithRetry (request , bodyHandler , executor , index , 0 , chain );
143
+ return executeWithRetry (request , bodyHandler , executor , index , 0 , Duration . from ( retryInterval ), chain );
142
144
}
143
145
144
- /**
145
- * The current timeout interval.
146
- *
147
- * @return the timeout duration
148
- */
149
- public Duration getTimeout () {
150
- return timeout ;
151
- }
152
-
153
- /**
154
- * Returns a new {@link Builder} instance.
155
- *
156
- * @return {@link Builder} instance.
157
- */
158
- public static Builder builder () {
159
- return new Builder ();
160
- }
161
146
162
147
private <T > CompletableFuture <HttpResponse <T >> executeWithRetry (HttpRequest request , BodyHandler <T > bodyHandler ,
163
- Executor executor , int index , int attempt , AsyncChain chain ) {
148
+ Executor executor , int index , int attempt , Duration timeout , AsyncChain chain ) {
164
149
165
150
return chain .proceed (request , bodyHandler , executor )
166
151
.exceptionallyCompose (throwable -> {
167
152
153
+ String requestId = request .headers ()
154
+ .firstValue (REQUEST_ID_HEADER )
155
+ .orElseThrow (); // This should never happen. The SyncHttpClient and AsyncHttpClient add this header if it is not present.
156
+
168
157
Throwable cause = throwable .getCause () != null ? throwable .getCause () : throwable ;
169
158
170
159
var shouldRetry =
@@ -178,27 +167,36 @@ private <T> CompletableFuture<HttpResponse<T>> executeWithRetry(HttpRequest requ
178
167
179
168
if (!shouldRetry || attempt >= maxRetries ) {
180
169
CompletableFuture <HttpResponse <T >> failed = new CompletableFuture <>();
181
- logger .debug ("Max retries reached" );
170
+ logger .debug ("Max retries reached for request \" {} \" " , requestId );
182
171
failed .completeExceptionally (cause );
183
- this .timeout = this .retryInterval ;
184
172
return failed ;
185
173
}
186
174
187
- if (exponentialBackoff && attempt > 0 )
188
- timeout = timeout .multipliedBy (2 );
175
+ Duration nextTimeout = exponentialBackoff ? timeout .multipliedBy (2 ) : timeout ;
189
176
190
- logger .debug ("Retrying request ({}/{}) after failure: {}" , attempt + 1 , maxRetries , cause .getMessage ());
177
+ if (timeout .isPositive ())
178
+ logger .debug ("Retry request \" {}\" after {} ms" , requestId , nextTimeout .toMillis ());
191
179
192
180
return CompletableFuture .supplyAsync (
193
181
() -> {
182
+ logger .debug ("Retrying request \" {}\" ({}/{}) after failure: {}" , requestId , attempt + 1 , maxRetries , cause .getMessage ());
194
183
chain .resetToIndex (index + 1 );
195
- return executeWithRetry (request , bodyHandler , executor , index , attempt + 1 , chain );
184
+ return executeWithRetry (request , bodyHandler , executor , index , attempt + 1 , nextTimeout , chain );
196
185
},
197
- CompletableFuture .delayedExecutor (timeout .toMillis (), TimeUnit .MILLISECONDS , executor )
186
+ CompletableFuture .delayedExecutor (nextTimeout .toMillis (), TimeUnit .MILLISECONDS , executor )
198
187
).thenCompose (Function .identity ());
199
188
});
200
189
}
201
190
191
+ /**
192
+ * Returns a new {@link Builder} instance.
193
+ *
194
+ * @return {@link Builder} instance.
195
+ */
196
+ public static Builder builder () {
197
+ return new Builder ();
198
+ }
199
+
202
200
/**
203
201
* Builder for {@link RetryInterceptor}.
204
202
*/
0 commit comments