-
Notifications
You must be signed in to change notification settings - Fork 328
Expand file tree
/
Copy pathHttpRetryPolicy.java
More file actions
184 lines (161 loc) · 6.03 KB
/
HttpRetryPolicy.java
File metadata and controls
184 lines (161 loc) · 6.03 KB
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
package datadog.communication.http;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A policy which encapsulates retry rules for HTTP calls. Whether to retry and how long to wait
* before the next attempt is determined by received HTTP response.
*
* <p>Logic is the following:
*
* <ul>
* <li>if there was no response, or response code is <b>5XX</b>, wait for specified delay time and
* retry (there is exponential backoff, so each consecutive retry takes longer than the
* previous one). Maximum number of retries and delay multiplication factor are provided
* during policy instantiation
* <li>if response code is <b>429 (Too Many Requests)</b>, try to get wait time from
* <b>x-ratelimit-reset</b> response header. Depending on the result:
* <ul>
* <li>if time is less than 10 seconds, wait for the specified amount + random(0, 0.4sec)
* (there will be only one retry in this case)
* <li>if time is more than 10 seconds, do not retry
* <li>if time was not provided or could not be parsed, do a regular retry (same logic as
* for 5XX responses)
* </ul>
* <li>in all other cases do not retry
* </ul>
*
* <p>Instances of this class are not thread-safe and not reusable: each HTTP call requires its own
* instance.
*/
@NotThreadSafe
public class HttpRetryPolicy implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(HttpRetryPolicy.class);
private static final int NO_RESPONSE_RECEIVED = -1;
private static final int TOO_MANY_REQUESTS_HTTP_CODE = 429;
private static final String X_RATELIMIT_RESET_HTTP_HEADER = "x-ratelimit-reset";
private static final int RATE_LIMIT_RESET_TIME_UNDEFINED = -1;
private static final int MAX_ALLOWED_WAIT_TIME_SECONDS = 10;
private static final int RATE_LIMIT_DELAY_RANDOM_COMPONENT_MAX_MILLIS = 401;
private int retriesLeft;
private long delay;
private boolean interrupted;
private final double delayFactor;
private final boolean suppressInterrupts;
private HttpRetryPolicy(
int retriesLeft, long delay, double delayFactor, boolean suppressInterrupts) {
this.retriesLeft = retriesLeft;
this.delay = delay;
this.delayFactor = delayFactor;
this.suppressInterrupts = suppressInterrupts;
}
public boolean shouldRetry(Exception e) {
if (e instanceof ConnectException) {
return shouldRetry((okhttp3.Response) null);
}
if (e instanceof InterruptedIOException) {
if (suppressInterrupts) {
return shouldRetry((okhttp3.Response) null);
}
}
if (e instanceof InterruptedException) {
if (suppressInterrupts) {
// remember interrupted status to restore the thread's interrupted flag later
interrupted = true;
return shouldRetry((okhttp3.Response) null);
}
}
return false;
}
public boolean shouldRetry(@Nullable okhttp3.Response response) {
if (retriesLeft == 0) {
return false;
}
int responseCode = response != null ? response.code() : NO_RESPONSE_RECEIVED;
if (responseCode == TOO_MANY_REQUESTS_HTTP_CODE) {
long waitTimeSeconds = getRateLimitResetTime(response);
if (waitTimeSeconds == RATE_LIMIT_RESET_TIME_UNDEFINED) {
retriesLeft--; // doing a regular retry if proper reset time was not provided
return true;
}
if (waitTimeSeconds > MAX_ALLOWED_WAIT_TIME_SECONDS) {
return false; // too long to wait, will not retry
}
retriesLeft = 0;
delay =
TimeUnit.SECONDS.toMillis(waitTimeSeconds)
+ ThreadLocalRandom.current().nextInt(RATE_LIMIT_DELAY_RANDOM_COMPONENT_MAX_MILLIS);
return true;
} else if (responseCode >= 500 || responseCode == NO_RESPONSE_RECEIVED) {
retriesLeft--;
return true;
} else {
return false;
}
}
private long getRateLimitResetTime(okhttp3.Response response) {
String rateLimitHeader = response.header(X_RATELIMIT_RESET_HTTP_HEADER);
if (rateLimitHeader == null) {
return RATE_LIMIT_RESET_TIME_UNDEFINED;
}
try {
return Long.parseLong(rateLimitHeader);
} catch (NumberFormatException e) {
log.error(
"Could not parse {} header contents: {}",
X_RATELIMIT_RESET_HTTP_HEADER,
rateLimitHeader,
e);
return RATE_LIMIT_RESET_TIME_UNDEFINED;
}
}
long getBackoffDelay() {
long currentDelay = delay;
delay = (long) (delay * delayFactor);
return currentDelay;
}
public void backoff() throws IOException {
try {
Thread.sleep(getBackoffDelay());
} catch (InterruptedException e) {
if (suppressInterrupts) {
// remember interrupted status to restore the thread's interrupted flag later
interrupted = true;
} else {
Thread.currentThread().interrupt();
throw new InterruptedIOException("thread interrupted");
}
}
}
@Override
public void close() {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
public static class Factory {
public static final Factory NEVER_RETRY = new Factory(0, 0, 0);
private final int maxRetries;
private final long initialDelay;
private final double delayFactor;
private final boolean retryInterrupts;
public Factory(int maxRetries, int initialDelay, double delayFactor) {
this(maxRetries, initialDelay, delayFactor, false);
}
public Factory(int maxRetries, int initialDelay, double delayFactor, boolean retryInterrupts) {
this.maxRetries = maxRetries;
this.initialDelay = initialDelay;
this.delayFactor = delayFactor;
this.retryInterrupts = retryInterrupts;
}
public HttpRetryPolicy create() {
return new HttpRetryPolicy(maxRetries, initialDelay, delayFactor, retryInterrupts);
}
}
}