/
RetryInterceptor.java
327 lines (284 loc) · 12 KB
/
RetryInterceptor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
package io.apimatic.okhttpclient.adapter.interceptors;
import java.io.IOException;
import java.net.SocketException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import io.apimatic.coreinterfaces.http.ClientConfiguration;
import io.apimatic.coreinterfaces.http.HttpMethodType;
import io.apimatic.coreinterfaces.http.request.Request;
import io.apimatic.coreinterfaces.http.request.configuration.CoreEndpointConfiguration;
import io.apimatic.coreinterfaces.http.response.Response;
import io.apimatic.okhttpclient.adapter.OkClient;
import okhttp3.Interceptor;
/**
* RetryInterceptor intercepts and retry requests if failed based on configuration.
*/
public class RetryInterceptor implements Interceptor {
/**
* Maximum Back off interval.
*/
private static final int RANDOM_NUMBER_MULTIPLIER = 100;
/**
* Maximum Retry interval.
*/
private static final int TO_MILLISECOND_MULTIPLIER = 1000;
/**
* RFC Date Time Formatter.
*/
private static final DateTimeFormatter RFC1123_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z").withZone(ZoneId.of("GMT"));
/**
* To keep track of requests being sent and its current state.
*/
private final ConcurrentMap<okhttp3.Request, RequestState> requestEntries;
/**
* User specified retry configurations.
*/
private final ClientConfiguration httpClientConfiguration;
/**
* Default Constructor, Initializes the httpClientConfiguration attribute.
* @param httpClientConfig the user specified configurations.
*/
public RetryInterceptor(final ClientConfiguration httpClientConfig) {
this.httpClientConfiguration = httpClientConfig;
requestEntries = new ConcurrentHashMap<>();
}
/**
* Intercepts and retry requests if failed based on configuration.
* @see okhttp3.Interceptor#intercept(okhttp3.Interceptor.Chain)
*/
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Request request = chain.request();
RequestState requestState = getRequestState(request);
boolean isWhitelistedRequestMethod = this.httpClientConfiguration.getHttpMethodsToRetry()
.contains(HttpMethodType.valueOf(request.method()));
boolean isRetryAllowedForRequest = requestState.endpointConfiguration.getRetryOption()
.isRetryAllowed(isWhitelistedRequestMethod);
okhttp3.Response response = null;
IOException timeoutException = null;
boolean shouldRetry = false;
do {
try {
response = getResponse(chain, request, response, true);
timeoutException = null;
} catch (IOException ioException) {
timeoutException = ioException;
response = null;
if (!httpClientConfiguration.shouldRetryOnTimeout()) {
break;
}
}
shouldRetry = isRetryAllowedForRequest
&& needToRetry(requestState, response, timeoutException != null);
if (shouldRetry) {
// Performing wait time calculation.
calculateWaitTime(requestState, response);
// Checking total wait time against allowed max back-off time
if (hasWaitTimeLimitExceeded(requestState)) {
break;
}
// Waiting before making next request
holdExecution(requestState.currentWaitInMilliSeconds);
// Incrementing retry attempt count
requestState.retryCount++;
}
} while (shouldRetry);
this.requestEntries.remove(request);
if (timeoutException != null) {
throw timeoutException;
}
return response;
}
/**
* Get the response Recursively since we have to handle the SocketException gracefully.
* @param chain the interceptor chain.
* @param request the HTTP request.
* @param response the HTTP response.
* @param shouldCloseResponse whether to close the response or not.
* @return the HTTP response.
* @throws IOException exception to be thrown in case of timeout.
*/
private okhttp3.Response getResponse(Chain chain, okhttp3.Request request,
okhttp3.Response response, boolean shouldCloseResponse) throws IOException {
try {
if (shouldCloseResponse && response != null) {
response.close();
}
return chain.proceed(request);
} catch (SocketException socketException) {
return getResponse(chain, request, response, false);
} catch (IOException exception) {
throw exception;
}
}
/**
* Checks if the retry request is to be made against provided response.
* @param requestState The current state of request entry.
* @param response The HTTP response.
* @param isTimeoutException We are retrying because of timeout or not
* @return true If request is needed to be retried.
*/
private boolean needToRetry(RequestState requestState, okhttp3.Response response,
boolean isTimeoutException) {
boolean isValidAttempt =
requestState.retryCount < this.httpClientConfiguration.getNumberOfRetries();
boolean isValidResponseToRetry =
response != null && (this.httpClientConfiguration.getHttpStatusCodesToRetry()
.contains(response.code()) || hasRetryAfterHeader(response));
return isValidAttempt && (isTimeoutException || isValidResponseToRetry);
}
/**
* Checks if the overall wait time has reached to its limit.
* @param requestState the current state of request entry.
* @return true if total wait time exceeds maximum back-off time.
*/
private boolean hasWaitTimeLimitExceeded(RequestState requestState) {
return this.httpClientConfiguration.getMaximumRetryWaitTime() > 0
&& toMilliseconds(this.httpClientConfiguration
.getMaximumRetryWaitTime()) < requestState.totalWaitTimeInMilliSeconds;
}
/**
* Calculates the wait time for next request.
* @param requestState The current state of request entry.
* @param response The HTTP response.
*/
private void calculateWaitTime(RequestState requestState, okhttp3.Response response) {
long retryAfterHeaderValue = 0;
if (response != null && hasRetryAfterHeader(response)) {
retryAfterHeaderValue = getCalculatedHeaderValue(response.header("Retry-After"));
}
long calculatedBackOffInMilliSeconds = getCalculatedBackOffValue(requestState);
requestState.currentWaitInMilliSeconds =
Math.max(retryAfterHeaderValue, calculatedBackOffInMilliSeconds);
requestState.totalWaitTimeInMilliSeconds += requestState.currentWaitInMilliSeconds;
}
/**
* Checks if the response contains Retry-After header.
* @param response The HTTP response.
* @return true If response contains Retry-After header.
*/
private boolean hasRetryAfterHeader(okhttp3.Response response) {
String retryAfter = response.header("Retry-After");
return retryAfter != null && !retryAfter.isEmpty();
}
/**
* Analyzes the header value and checks the header if it contains date in proper format or
* seconds. If header value is date then it calculates the delta time in milliseconds.
* @param headerValue The retry-after header value.
* @return long value of calculated wait time in milliseconds.
*/
private long getCalculatedHeaderValue(String headerValue) {
try {
return toMilliseconds(Long.parseLong(headerValue));
} catch (NumberFormatException nfe) {
long requestAtValueInSeconds = LocalDateTime
.parse(headerValue, RFC1123_DATE_TIME_FORMATTER).toEpochSecond(ZoneOffset.UTC);
long currentDateTimeInSeconds =
LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC);
return toMilliseconds(requestAtValueInSeconds - currentDateTimeInSeconds);
}
}
/**
* Calculates the back-off value based on a formula which uses back-off factor and retry Count.
* @param requestState The current state of request entry.
* @return long value of back-off time based on formula in milliseconds.
*/
private long getCalculatedBackOffValue(RequestState requestState) {
return (long) (TO_MILLISECOND_MULTIPLIER * this.httpClientConfiguration.getRetryInterval()
* Math.pow(this.httpClientConfiguration.getBackOffFactor(), requestState.retryCount)
+ Math.random() * RANDOM_NUMBER_MULTIPLIER);
}
/**
* Holds the execution for stored wait time in milliseconds of this thread.
* @param milliSeconds The wait time in milli seconds.
*/
private void holdExecution(long milliSeconds) {
try {
TimeUnit.MILLISECONDS.sleep(milliSeconds);
} catch (InterruptedException e) {
// No handler needed
}
}
/**
* Converts the seconds to milliseconds.
* @param seconds The seconds to convert.
* @return long value of milliseconds.
*/
private long toMilliseconds(long seconds) {
return seconds * TO_MILLISECOND_MULTIPLIER;
}
/**
* Adds entry into Request entry map.
* @param okHttpRequest The OK HTTP Request.
* @param endpointConfiguration The overridden endpointConfiguration for request.
* @param request The core interface Request
*/
public void addRequestEntry(okhttp3.Request okHttpRequest,
CoreEndpointConfiguration endpointConfiguration, Request request) {
this.requestEntries.put(okHttpRequest, new RequestState(endpointConfiguration, request));
}
/**
* getter for current request state entry from map.
* @param okHttpRequest The OK HTTP Request.
* @return RequestEntry The current request entry.
*/
private RequestState getRequestState(okhttp3.Request okHttpRequest) {
return this.requestEntries.get(okHttpRequest);
}
/**
* Logs the response.
* @param requestState The current state of request.
* @param response The OKhttp Response.
*/
@SuppressWarnings("unused")
private void logResponse(RequestState requestState, okhttp3.Response response) {
Response httpResponse = null;
try {
httpResponse = OkClient.convertResponse(requestState.httpRequest, response,
requestState.endpointConfiguration.hasBinaryResponse());
} catch (IOException ioException) {
// log error
}
}
/**
* Class to hold the request info until request completes.
*/
private final class RequestState {
/**
* The internal HTTP request.
*/
private Request httpRequest;
/**
* To keep track of requests count.
*/
private int retryCount = 0;
/**
* To store the wait time for next request.
*/
private long currentWaitInMilliSeconds = 0;
/**
* To keep track of overall wait time.
*/
private long totalWaitTimeInMilliSeconds = 0;
/**
* To keep track of request endpoint configurations.
*/
private CoreEndpointConfiguration endpointConfiguration;
/**
* Default Constructor.
* @param coreEndpointConfiguration The end point configuration
* @param request The client request
*/
private RequestState(final CoreEndpointConfiguration coreEndpointConfiguration,
final Request request) {
this.endpointConfiguration = coreEndpointConfiguration;
this.httpRequest = request;
}
}
}