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 package org.apache.hc.client5.http.impl;
29
30 import java.io.IOException;
31 import java.io.InterruptedIOException;
32 import java.net.ConnectException;
33 import java.net.NoRouteToHostException;
34 import java.net.UnknownHostException;
35 import java.time.Instant;
36 import java.util.Arrays;
37 import java.util.Collection;
38 import java.util.HashSet;
39 import java.util.Set;
40
41 import javax.net.ssl.SSLException;
42
43 import org.apache.hc.client5.http.HttpRequestRetryStrategy;
44 import org.apache.hc.client5.http.config.RequestConfig;
45 import org.apache.hc.client5.http.protocol.HttpClientContext;
46 import org.apache.hc.client5.http.utils.DateUtils;
47 import org.apache.hc.core5.annotation.Contract;
48 import org.apache.hc.core5.annotation.ThreadingBehavior;
49 import org.apache.hc.core5.concurrent.CancellableDependency;
50 import org.apache.hc.core5.http.ConnectionClosedException;
51 import org.apache.hc.core5.http.Header;
52 import org.apache.hc.core5.http.HttpHeaders;
53 import org.apache.hc.core5.http.HttpRequest;
54 import org.apache.hc.core5.http.HttpResponse;
55 import org.apache.hc.core5.http.HttpStatus;
56 import org.apache.hc.core5.http.Method;
57 import org.apache.hc.core5.http.protocol.HttpContext;
58 import org.apache.hc.core5.util.Args;
59 import org.apache.hc.core5.util.TimeValue;
60 import org.apache.hc.core5.util.Timeout;
61
62
63
64
65
66
67 @Contract(threading = ThreadingBehavior.STATELESS)
68 public class DefaultHttpRequestRetryStrategy implements HttpRequestRetryStrategy {
69
70
71
72
73 public static final DefaultHttpRequestRetryStrategy INSTANCE = new DefaultHttpRequestRetryStrategy();
74
75
76
77
78 private final int maxRetries;
79
80
81
82
83 private final TimeValue defaultRetryInterval;
84
85
86
87
88 private final Set<Class<? extends IOException>> nonRetriableIOExceptionClasses;
89
90
91
92
93 private final Set<Integer> retriableCodes;
94
95 protected DefaultHttpRequestRetryStrategy(
96 final int maxRetries,
97 final TimeValue defaultRetryInterval,
98 final Collection<Class<? extends IOException>> clazzes,
99 final Collection<Integer> codes) {
100 Args.notNegative(maxRetries, "maxRetries");
101 Args.notNull(defaultRetryInterval, "defaultRetryInterval");
102 Args.check(TimeValue.isNonNegative(defaultRetryInterval), "Default retry interval is negative");
103 this.maxRetries = maxRetries;
104 this.defaultRetryInterval = defaultRetryInterval;
105 this.nonRetriableIOExceptionClasses = new HashSet<>(clazzes);
106 this.retriableCodes = new HashSet<>(codes);
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 public DefaultHttpRequestRetryStrategy(
133 final int maxRetries,
134 final TimeValue defaultRetryInterval) {
135 this(maxRetries, defaultRetryInterval,
136 Arrays.asList(
137 InterruptedIOException.class,
138 UnknownHostException.class,
139 ConnectException.class,
140 ConnectionClosedException.class,
141 NoRouteToHostException.class,
142 SSLException.class),
143 Arrays.asList(
144 HttpStatus.SC_TOO_MANY_REQUESTS,
145 HttpStatus.SC_SERVICE_UNAVAILABLE));
146 }
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166 public DefaultHttpRequestRetryStrategy() {
167 this(1, TimeValue.ofSeconds(1L));
168 }
169
170 @Override
171 public boolean retryRequest(
172 final HttpRequest request,
173 final IOException exception,
174 final int execCount,
175 final HttpContext context) {
176 Args.notNull(request, "request");
177 Args.notNull(exception, "exception");
178
179 if (execCount > this.maxRetries) {
180
181 return false;
182 }
183 if (this.nonRetriableIOExceptionClasses.contains(exception.getClass())) {
184 return false;
185 }
186 for (final Class<? extends IOException> rejectException : this.nonRetriableIOExceptionClasses) {
187 if (rejectException.isInstance(exception)) {
188 return false;
189 }
190 }
191 if (request instanceof CancellableDependency && ((CancellableDependency) request).isCancelled()) {
192 return false;
193 }
194
195
196 return handleAsIdempotent(request);
197 }
198
199 @Override
200 public boolean retryRequest(
201 final HttpResponse response,
202 final int execCount,
203 final HttpContext context) {
204 Args.notNull(response, "response");
205
206 if (context != null) {
207 final HttpClientContext clientContext = HttpClientContext.cast(context);
208 final RequestConfig requestConfig = clientContext.getRequestConfigOrDefault();
209 final Timeout responseTimeout = requestConfig.getResponseTimeout();
210 if (responseTimeout != null && defaultRetryInterval.compareTo(responseTimeout) > 0) {
211 return false;
212 }
213 }
214 return execCount <= this.maxRetries && retriableCodes.contains(response.getCode());
215 }
216
217 @Override
218 public TimeValue getRetryInterval(
219 final HttpResponse response,
220 final int execCount,
221 final HttpContext context) {
222 Args.notNull(response, "response");
223
224 final Header header = response.getFirstHeader(HttpHeaders.RETRY_AFTER);
225 TimeValue retryAfter = null;
226 if (header != null) {
227 final String value = header.getValue();
228 try {
229 retryAfter = TimeValue.ofSeconds(Long.parseLong(value));
230 } catch (final NumberFormatException ignore) {
231 final Instant retryAfterDate = DateUtils.parseStandardDate(value);
232 if (retryAfterDate != null) {
233 retryAfter =
234 TimeValue.ofMilliseconds(retryAfterDate.toEpochMilli() - System.currentTimeMillis());
235 }
236 }
237
238 if (TimeValue.isPositive(retryAfter)) {
239 return retryAfter;
240 }
241 }
242 return this.defaultRetryInterval;
243 }
244
245 protected boolean handleAsIdempotent(final HttpRequest request) {
246 return Method.isIdempotent(request.getMethod());
247 }
248
249 }