|
1 | 1 | package com.stripe.net;
|
2 | 2 |
|
3 | 3 | import com.stripe.Stripe;
|
| 4 | +import com.stripe.exception.ApiConnectionException; |
4 | 5 | import com.stripe.exception.StripeException;
|
5 | 6 | import com.stripe.util.Stopwatch;
|
| 7 | +import java.net.ConnectException; |
| 8 | +import java.time.Duration; |
6 | 9 | import java.util.HashMap;
|
7 | 10 | import java.util.Map;
|
| 11 | +import java.util.concurrent.ThreadLocalRandom; |
8 | 12 |
|
9 | 13 | /** Base abstract class for HTTP clients used to send requests to Stripe's API. */
|
10 | 14 | public abstract class HttpClient {
|
11 |
| - private static final RequestTelemetry requestTelemetry = new RequestTelemetry(); |
| 15 | + /** Maximum sleep time between tries to send HTTP requests after network failure. */ |
| 16 | + public static final Duration maxNetworkRetriesDelay = Duration.ofSeconds(5); |
| 17 | + |
| 18 | + /** Minimum sleep time between tries to send HTTP requests after network failure. */ |
| 19 | + public static final Duration minNetworkRetriesDelay = Duration.ofMillis(500); |
| 20 | + |
| 21 | + private final int maxNetworkRetries; |
| 22 | + |
| 23 | + private final RequestTelemetry requestTelemetry = new RequestTelemetry(); |
| 24 | + |
| 25 | + /** A value indicating whether the client should sleep between automatic request retries. */ |
| 26 | + boolean networkRetriesSleep = true; |
| 27 | + |
| 28 | + /** Initializes a new instance of the {@link HttpClient} class with default parameters. */ |
| 29 | + public HttpClient() { |
| 30 | + this(0); |
| 31 | + } |
| 32 | + |
| 33 | + /** |
| 34 | + * Initializes a new instance of the {@link HttpClient} class. |
| 35 | + * |
| 36 | + * @param maxNetworkRetries the maximum number of times the client will retry requests that fail |
| 37 | + * due to an intermittent problem. |
| 38 | + */ |
| 39 | + public HttpClient(int maxNetworkRetries) { |
| 40 | + this.maxNetworkRetries = maxNetworkRetries; |
| 41 | + } |
12 | 42 |
|
13 | 43 | /**
|
14 | 44 | * Sends the given request to Stripe's API.
|
@@ -40,6 +70,50 @@ public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeE
|
40 | 70 | return response;
|
41 | 71 | }
|
42 | 72 |
|
| 73 | + /** |
| 74 | + * Sends the given request to Stripe's API, retrying the request in cases of intermittent |
| 75 | + * problems. |
| 76 | + * |
| 77 | + * @param request the request |
| 78 | + * @return the response |
| 79 | + * @throws StripeException If the request fails for any reason |
| 80 | + */ |
| 81 | + public StripeResponse requestWithRetries(StripeRequest request) throws StripeException { |
| 82 | + ApiConnectionException requestException = null; |
| 83 | + StripeResponse response = null; |
| 84 | + int retry = 0; |
| 85 | + |
| 86 | + while (true) { |
| 87 | + requestException = null; |
| 88 | + |
| 89 | + try { |
| 90 | + response = this.requestWithTelemetry(request); |
| 91 | + } catch (ApiConnectionException e) { |
| 92 | + requestException = e; |
| 93 | + } |
| 94 | + |
| 95 | + if (!this.shouldRetry(retry, requestException, response)) { |
| 96 | + break; |
| 97 | + } |
| 98 | + |
| 99 | + retry += 1; |
| 100 | + |
| 101 | + try { |
| 102 | + Thread.sleep(this.sleepTime(retry).toMillis()); |
| 103 | + } catch (InterruptedException e) { |
| 104 | + Thread.currentThread().interrupt(); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + if (requestException != null) { |
| 109 | + throw requestException; |
| 110 | + } |
| 111 | + |
| 112 | + response.setNumRetries(retry); |
| 113 | + |
| 114 | + return response; |
| 115 | + } |
| 116 | + |
43 | 117 | /**
|
44 | 118 | * Builds the value of the {@code User-Agent} header.
|
45 | 119 | *
|
@@ -98,4 +172,76 @@ private static String formatAppInfo(Map<String, String> info) {
|
98 | 172 |
|
99 | 173 | return str;
|
100 | 174 | }
|
| 175 | + |
| 176 | + private boolean shouldRetry(int numRetries, StripeException exception, StripeResponse response) { |
| 177 | + // Do not retry if we are out of retries. |
| 178 | + if (numRetries >= this.maxNetworkRetries) { |
| 179 | + return false; |
| 180 | + } |
| 181 | + |
| 182 | + // Retry on connection error. |
| 183 | + if ((exception != null) |
| 184 | + && (exception.getCause() != null) |
| 185 | + && (exception.getCause() instanceof ConnectException)) { |
| 186 | + return true; |
| 187 | + } |
| 188 | + |
| 189 | + // The API may ask us not to retry (eg; if doing so would be a no-op) |
| 190 | + // or advise us to retry (eg; in cases of lock timeouts); we defer to that. |
| 191 | + if ((response != null) && (response.headers() != null)) { |
| 192 | + String value = response.headers().get("Stripe-Should-Retry"); |
| 193 | + |
| 194 | + if ("true".equals(value)) { |
| 195 | + return true; |
| 196 | + } |
| 197 | + |
| 198 | + if ("false".equals(value)) { |
| 199 | + return false; |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + // Retry on conflict errors. |
| 204 | + if ((response != null) && (response.code() == 409)) { |
| 205 | + return true; |
| 206 | + } |
| 207 | + |
| 208 | + // Retry on 500, 503, and other internal errors. |
| 209 | + // |
| 210 | + // Note that we expect the Stripe-Should-Retry header to be false |
| 211 | + // in most cases when a 500 is returned, since our idempotency framework |
| 212 | + // would typically replay it anyway. |
| 213 | + if ((response != null) && (response.code() >= 500)) { |
| 214 | + return true; |
| 215 | + } |
| 216 | + |
| 217 | + return false; |
| 218 | + } |
| 219 | + |
| 220 | + private Duration sleepTime(int numRetries) { |
| 221 | + // We disable sleeping in some cases for tests. |
| 222 | + if (!this.networkRetriesSleep) { |
| 223 | + return Duration.ZERO; |
| 224 | + } |
| 225 | + |
| 226 | + // Apply exponential backoff with MinNetworkRetriesDelay on the number of numRetries |
| 227 | + // so far as inputs. |
| 228 | + Duration delay = |
| 229 | + Duration.ofNanos((long) (minNetworkRetriesDelay.toNanos() * Math.pow(2, numRetries - 1))); |
| 230 | + |
| 231 | + // Do not allow the number to exceed MaxNetworkRetriesDelay |
| 232 | + if (delay.compareTo(maxNetworkRetriesDelay) > 0) { |
| 233 | + delay = maxNetworkRetriesDelay; |
| 234 | + } |
| 235 | + |
| 236 | + // Apply some jitter by randomizing the value in the range of 75%-100%. |
| 237 | + double jitter = ThreadLocalRandom.current().nextDouble(0.75, 1.0); |
| 238 | + delay = Duration.ofNanos((long) (delay.toNanos() * jitter)); |
| 239 | + |
| 240 | + // But never sleep less than the base sleep seconds. |
| 241 | + if (delay.compareTo(minNetworkRetriesDelay) < 0) { |
| 242 | + delay = minNetworkRetriesDelay; |
| 243 | + } |
| 244 | + |
| 245 | + return delay; |
| 246 | + } |
101 | 247 | }
|
0 commit comments