Skip to content

Commit f3dcd4b

Browse files
authored
feat: support user-defined retry interceptors (#169)
This commit provides a pluggable way for users of the java core to supply their own retry interceptor implementation in order to customize the conditions under which retries are automatically attempted. Included in these changes is the notion of a "retry strategy", which is a factory for creating retry interceptor instances. Users of the java core (e.g. an SDK project) can implement their own retry strategy and then set it with the HttpClientSingleton.setRetryStrategy() static method. Once set, the registered factory's createRetryInterceptor() method will be called whenever the java core needs to initialize an http client instance when retries are enabled. The factory's createRetryInterceptor() method returns an implementation of the IRetryInterceptor interface (which is itself a subclass of the okhttp3 Interceptor interface). User's might find it convenient to define their retry interceptor class as a subclass of the java core's RetryInterceptor class, thereby relying on RetryInterceptor for most of the functionality, while overriding specific methods as needed.
1 parent d6131a8 commit f3dcd4b

File tree

8 files changed

+329
-45
lines changed

8 files changed

+329
-45
lines changed

.secrets.baseline

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "package-lock.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2021-11-17T14:53:46Z",
6+
"generated_at": "2022-03-16T14:20:41Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -150,15 +150,15 @@
150150
"hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc",
151151
"is_secret": false,
152152
"is_verified": false,
153-
"line_number": 62,
153+
"line_number": 63,
154154
"type": "Secret Keyword",
155155
"verified_result": null
156156
},
157157
{
158158
"hashed_secret": "2207d3f3ac68743290fe4affc71c10bec4962232",
159159
"is_secret": false,
160160
"is_verified": false,
161-
"line_number": 63,
161+
"line_number": 64,
162162
"type": "Secret Keyword",
163163
"verified_result": null
164164
}
@@ -352,7 +352,7 @@
352352
}
353353
]
354354
},
355-
"version": "0.13.1+ibm.46.dss",
355+
"version": "0.13.1+ibm.47.dss",
356356
"word_list": {
357357
"file": null,
358358
"hash": null
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* (C) Copyright IBM Corp. 2022. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package com.ibm.cloud.sdk.core.http;
15+
16+
import com.ibm.cloud.sdk.core.security.Authenticator;
17+
18+
/**
19+
* This is the java core's default retry strategy.
20+
* It will return instances of our RetryInterceptor implementation class.
21+
*/
22+
public class DefaultRetryStrategy implements IRetryStrategy {
23+
24+
@Override
25+
public RetryInterceptor createRetryInterceptor(int maxRetries, int maxRetryInterval, Authenticator authenticator) {
26+
return new RetryInterceptor(maxRetries, maxRetryInterval, authenticator);
27+
}
28+
}

src/main/java/com/ibm/cloud/sdk/core/http/HttpClientSingleton.java

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import com.ibm.cloud.sdk.core.http.HttpConfigOptions.LoggingLevel;
1717
import com.ibm.cloud.sdk.core.http.gzip.GzipRequestInterceptor;
18-
import com.ibm.cloud.sdk.core.service.BaseService;
1918
import com.ibm.cloud.sdk.core.service.security.DelegatingSSLSocketFactory;
2019
import okhttp3.Authenticator;
2120
import okhttp3.ConnectionSpec;
@@ -59,7 +58,21 @@
5958
public class HttpClientSingleton {
6059
private static HttpClientSingleton instance = null;
6160

62-
private static final Logger LOG = Logger.getLogger(BaseService.class.getName());
61+
private static final Logger LOG = Logger.getLogger(HttpClientSingleton.class.getName());
62+
63+
// retryStrategy serves as a factory for creating retry interceptors.
64+
private static IRetryStrategy retryStrategy = new DefaultRetryStrategy();
65+
66+
/**
67+
* Sets the factory to be used to construct retry interceptor instances.
68+
* @param strategy the IRetryStrategy implementation to be set as the factory
69+
* @return the previous factory
70+
*/
71+
public static synchronized IRetryStrategy setRetryStrategy(IRetryStrategy strategy) {
72+
IRetryStrategy previousStrategy = retryStrategy;
73+
retryStrategy = strategy;
74+
return previousStrategy;
75+
}
6376

6477
/**
6578
* TrustManager for disabling SSL verification, which essentially lets everything through.
@@ -310,7 +323,7 @@ private void setupTLSProtocol(final OkHttpClient.Builder builder) {
310323
* Sets a new list of interceptors for the specified {@link OkHttpClient} instance by removing the specified
311324
* interceptor and returns a new instance with the interceptors configured as requested.
312325
*
313-
* @param client the {@link OkHttpClient} instance to set the proxy authenticator on
326+
* @param client the {@link OkHttpClient} instance to remove the interceptors from
314327
* @param interceptorToRemove the class name of the interceptor to remove
315328
* @return the new {@link OkHttpClient} instance with the new list of interceptors
316329
*/
@@ -320,9 +333,33 @@ private OkHttpClient reconfigureClientInterceptors(OkHttpClient client, String i
320333
if (!builder.interceptors().isEmpty()) {
321334
for (Iterator<Interceptor> iter = builder.interceptors().iterator(); iter.hasNext(); ) {
322335
Interceptor element = iter.next();
323-
String currentInterceptor = element.getClass().getSimpleName();
324-
if (currentInterceptor.equals(interceptorToRemove)) {
325-
LOG.log(Level.FINE, "Removing interceptor " + currentInterceptor + " from http client instance.");
336+
if (interceptorToRemove.equals(element.getClass().getSimpleName())) {
337+
LOG.log(Level.FINE, "Removing interceptor " + element.getClass().getName() + " from http client instance.");
338+
iter.remove();
339+
}
340+
}
341+
}
342+
343+
return builder.build();
344+
}
345+
346+
/**
347+
* Sets a new list of interceptors for the specified {@link OkHttpClient} instance by removing any interceptors
348+
* that implement "interfaceToRemove".
349+
*
350+
* @param client the {@link OkHttpClient} instance to remove the interceptors from
351+
* @param interfaceToRemove the specific interface for which interceptor instances should be removed
352+
* @return the new {@link OkHttpClient} instance with the updated list of interceptors
353+
*/
354+
private OkHttpClient reconfigureClientInterceptors(OkHttpClient client,
355+
Class<? extends Interceptor> interfaceToRemove) {
356+
OkHttpClient.Builder builder = client.newBuilder();
357+
358+
if (!builder.interceptors().isEmpty()) {
359+
for (Iterator<Interceptor> iter = builder.interceptors().iterator(); iter.hasNext(); ) {
360+
Interceptor element = iter.next();
361+
if (interfaceToRemove.isAssignableFrom(element.getClass())) {
362+
LOG.log(Level.FINE, "Removing interceptor " + element.getClass().getName() + " from http client instance.");
326363
iter.remove();
327364
}
328365
}
@@ -381,11 +418,17 @@ public OkHttpClient configureClient(OkHttpClient client, HttpConfigOptions optio
381418
// Configure the retry interceptor.
382419
Boolean enableRetries = options.getRetries();
383420
if (enableRetries != null) {
384-
client = reconfigureClientInterceptors(client, "RetryInterceptor");
421+
client = reconfigureClientInterceptors(client, IRetryInterceptor.class);
385422
if (enableRetries.booleanValue()) {
386-
client = client.newBuilder().addInterceptor(
387-
new RetryInterceptor(options.getMaxRetries(), options.getMaxRetryInterval(), options.getAuthenticator()))
388-
.build();
423+
IRetryInterceptor retryInterceptor =
424+
retryStrategy.createRetryInterceptor(options.getMaxRetries(), options.getMaxRetryInterval(),
425+
options.getAuthenticator());
426+
if (retryInterceptor != null) {
427+
client = client.newBuilder().addInterceptor(retryInterceptor).build();
428+
} else {
429+
LOG.log(Level.WARNING,
430+
"The retry interceptor factory returned a null IRetryInterceptor instance. Retries are disabled.");
431+
}
389432
}
390433
}
391434

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* (C) Copyright IBM Corp. 2022. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package com.ibm.cloud.sdk.core.http;
15+
16+
import okhttp3.Interceptor;
17+
18+
/**
19+
* This is a marker interface used to identify retry interceptor implementations
20+
* within the java core library.
21+
*/
22+
public interface IRetryInterceptor extends Interceptor {
23+
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* (C) Copyright IBM Corp. 2022. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package com.ibm.cloud.sdk.core.http;
15+
16+
import com.ibm.cloud.sdk.core.security.Authenticator;
17+
18+
/**
19+
* IRetryStrategy is an interface that is implemented by retry interceptor factories.
20+
* This interface is used by the java core to create a retry interceptor implementation when
21+
* automatic retries are enabled.
22+
* The java core defines a default implementation of this interface in the
23+
* DefaultRetryStrategy class.
24+
* Users can implement their own factory in order to supply their own retry interceptor
25+
* implementation, perhaps to customize the criteria under which failed requests will be retried.
26+
*/
27+
public interface IRetryStrategy {
28+
29+
/**
30+
* Return an implementation of the {@link IRetryInterceptor} interface
31+
* that is capable of retrying failed requests.
32+
*
33+
* @param maxRetries the maximum number of retries to be attempted by the retry interceptor
34+
* @param maxRetryInterval the maximum interval (in seconds)
35+
* @param authenticator the Authenticator instance to be used to authenticate retried requests
36+
* @return an okhttp3.Interceptor instance
37+
*/
38+
IRetryInterceptor createRetryInterceptor(int maxRetries, int maxRetryInterval, Authenticator authenticator);
39+
}

src/main/java/com/ibm/cloud/sdk/core/http/RetryInterceptor.java

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* (C) Copyright IBM Corp. 2021.
2+
* (C) Copyright IBM Corp. 2021, 2022.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
55
* the License. You may obtain a copy of the License at
@@ -30,9 +30,16 @@
3030
import org.apache.commons.lang3.StringUtils;
3131

3232
/**
33-
* This interceptor checks the responses and retries the request if it's possible.
33+
* This class is an okhttp Interceptor implementation that will try to automatically retry
34+
* failed requests, based on the type of failure that occurred.
35+
* This class is configured with the following:
36+
* <ul>
37+
* <li>the maximum number of retries to attempt for a failed request
38+
* <li>the maximum retry interval (in seconds) to wait between retry attempts
39+
* <li>the {@link Authenticator} instance to use to authenticate each retry attempt
40+
* </ul>
3441
*/
35-
public class RetryInterceptor implements Interceptor {
42+
public class RetryInterceptor implements IRetryInterceptor {
3643

3744
private static final Logger LOG = Logger.getLogger(RetryInterceptor.class.getName());
3845

@@ -43,7 +50,7 @@ public class RetryInterceptor implements Interceptor {
4350
private int maxRetries;
4451
private int maxRetryInterval;
4552

46-
private class RetryContext {
53+
public class RetryContext {
4754
private int retryCount;
4855

4956
private RetryContext() {
@@ -59,13 +66,36 @@ private boolean incCountAndCheck() {
5966
}
6067
}
6168

69+
// Hide the default ctor to force the use of the non-default ctor below.
70+
protected RetryInterceptor() { }
71+
72+
/**
73+
* This ctor configures the RetryInterceptor instance with the max retries,
74+
* retry interval and an authenticator.
75+
* @param maxRetries the maximum number of retries to attempt for a failed request
76+
* @param maxRetryInterval the maximum retry interval (in seconds) to wait between retry attempts
77+
* @param authenticator the {@link Authenticator} instance to use to authenticate retried requests
78+
*/
6279
public RetryInterceptor(int maxRetries, int maxRetryInterval, Authenticator authenticator) {
6380
this.authenticator = authenticator;
6481
this.maxRetries = maxRetries;
6582
// Convert the interval from seconds to milliseconds.
6683
this.maxRetryInterval = maxRetryInterval * 1000;
6784
}
6885

86+
/**
87+
* The "intercept()" method is the primary method of the interceptor.
88+
* The chain of interceptors registered for a particular okhttp Client instance
89+
* is in the form of an ordered list. When a request is invoked, each interceptor's
90+
* "intercept" method is invoked and is passed the interceptor chain.
91+
* The interceptor can inspect the request to determine how to proceed, and will invoke the interceptor
92+
* chain's "proceed()" method to call the next interceptor in the chain.
93+
* When the last interceptor invokes the chain's "proceed()" method, the request is sent over the wire
94+
* and the response is returned via the return value of the proceed() method.
95+
* The interceptor can then inspect the response and determine how to proceed.
96+
* Ultimately the response is returned from this intercept() method and is ultimately propagated
97+
* back through the chain's "proceed()" methods.
98+
*/
6999
@Override
70100
public Response intercept(Interceptor.Chain chain) throws IOException {
71101
// Make the first request.
@@ -89,7 +119,7 @@ public Response intercept(Interceptor.Chain chain) throws IOException {
89119
builder.tag(RetryContext.class, new RetryContext());
90120
}
91121

92-
// If we have a valid authenticator authenticate the request.
122+
// If we have a valid authenticator, then authenticate the request.
93123
// This is mostly here for backward compatibility.
94124
if (authenticator != null) {
95125
authenticator.authenticate(builder);
@@ -103,8 +133,13 @@ public Response intercept(Interceptor.Chain chain) throws IOException {
103133
return response;
104134
}
105135

106-
// Get the time we should wait before fire the next request in milliseconds.
107-
private int getInterval(Response response, Request request) {
136+
/**
137+
* Determine the retry interval to wait before attempting the next retry.
138+
* @param response the response from the previously attempted request
139+
* @param request the previously attempted request
140+
* @return the retry interval in milliseconds
141+
*/
142+
protected int getInterval(Response response, Request request) {
108143
Integer interval = null;
109144

110145
String headerVal = response.header("Retry-After");
@@ -143,9 +178,13 @@ private int getInterval(Response response, Request request) {
143178
return interval;
144179
}
145180

146-
// Check the response and the retry context then decide should we have make
147-
// another request.
148-
private boolean shouldRetry(Response response, Request request) {
181+
/**
182+
* Determine whether or not to attempt a retry of the specified request.
183+
* @param response the response obtained from the previously-attempted request
184+
* @param request the previously-attempted request
185+
* @return true if the specified request should be retried, false otherwise
186+
*/
187+
protected boolean shouldRetry(Response response, Request request) {
149188
// First check the response.
150189
if (response.code() == 429 || (response.code() >= 500 && response.code() != 501)) {
151190
// Now check if we exhausted the max number of retries or not.
@@ -160,10 +199,13 @@ private boolean shouldRetry(Response response, Request request) {
160199
return false;
161200
}
162201

163-
// Calculate the back off time in milliseconds based on the retry count.
164-
// This calculation is based on what the go-retryablehttp package does in its
165-
// DefaultBackoff function.
166-
private int calculateBackoff(int retryCount) {
202+
/**
203+
* Compute the "backoff" time (retry interval) in milleseconds based on the retry count.
204+
* This calculation is based on the go-retryablehttp package's "DefaultBackoff()" function.
205+
* @param retryCount the retry count for which we need to compute the backoff time
206+
* @return the retry interval to use for retry number "retryCount"
207+
*/
208+
protected int calculateBackoff(int retryCount) {
167209
// Exponential interval calculation based on the number of retries.
168210
double newInterval = (Math.pow(2, Double.valueOf(retryCount))) * RetryInterceptor.DEFAULT_RETRY_INTERVAL;
169211
if (newInterval > this.maxRetryInterval) {

0 commit comments

Comments
 (0)