|
2 | 2 |
|
3 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; |
4 | 4 | import io.github.sornerol.chess.pubapi.client.enums.ResponseCode; |
| 5 | +import io.github.sornerol.chess.pubapi.client.enums.RetryStrategy; |
5 | 6 | import io.github.sornerol.chess.pubapi.exception.ChessComPubApiException; |
6 | 7 | import lombok.Getter; |
7 | 8 | import lombok.Setter; |
8 | 9 | import org.apache.commons.io.IOUtils; |
9 | | -import org.apache.http.Header; |
10 | 10 | import org.apache.http.client.methods.CloseableHttpResponse; |
11 | 11 | import org.apache.http.client.methods.HttpGet; |
12 | 12 | import org.apache.http.impl.client.CloseableHttpClient; |
|
22 | 22 | */ |
23 | 23 | abstract class PubApiClientBase { |
24 | 24 |
|
| 25 | + /** |
| 26 | + * Default value for retryInterval. |
| 27 | + */ |
| 28 | + public static final Integer DEFAULT_RETRY_INTERVAL = 3; |
| 29 | + |
| 30 | + /** |
| 31 | + * Default value for maxRetries. |
| 32 | + */ |
| 33 | + public static final Integer DEFAULT_MAX_RETRIES = 2; |
| 34 | + |
| 35 | + /** |
| 36 | + * Default value for retryStrategy. |
| 37 | + */ |
| 38 | + public static final RetryStrategy DEFAULT_RETRY_STRATEGY = RetryStrategy.RETRY_N_TIMES; |
| 39 | + |
25 | 40 | /** |
26 | 41 | * If supplied, the userAgent value will be passed in the User-Agent header when making requests to Chess.com. |
27 | 42 | * By specifying this value with your contact information, Chess.com can contact you in the event they need to |
28 | 43 | * block access for your application. |
| 44 | + * |
29 | 45 | * @see <a href="https://www.chess.com/news/view/published-data-api#pubapi-general-rate-limits">Rate Limiting</a> |
30 | 46 | */ |
31 | 47 | @Getter |
32 | 48 | @Setter |
33 | 49 | private String userAgent; |
34 | 50 |
|
| 51 | + /** |
| 52 | + * Number of seconds to wait before retrying if a request fails due to a 429 response code. If retryStrategy is set |
| 53 | + * to NEVER, this field does nothing. |
| 54 | + * |
| 55 | + * @see <a href="https://www.chess.com/news/view/published-data-api#pubapi-general-rate-limits">Rate Limiting</a> |
| 56 | + */ |
| 57 | + @Getter |
| 58 | + @Setter |
| 59 | + private Integer retryInterval; |
| 60 | + |
| 61 | + /** |
| 62 | + * Maximum number of retries when retryStrategy is RETRY_N_TIMES. This does not include the initial attempt |
| 63 | + * (e.g. if maxRetries is 2, the client will make three total attempts before giving up). |
| 64 | + * |
| 65 | + * @see <a href="https://www.chess.com/news/view/published-data-api#pubapi-general-rate-limits">Rate Limiting</a> |
| 66 | + */ |
| 67 | + @Getter |
| 68 | + @Setter |
| 69 | + private Integer maxRetries; |
| 70 | + |
| 71 | + /** |
| 72 | + * Defines the behavior to use when receiving a 429 response from the Chess.com API. |
| 73 | + * |
| 74 | + * @see <a href="https://www.chess.com/news/view/published-data-api#pubapi-general-rate-limits">Rate Limiting</a> |
| 75 | + */ |
| 76 | + @Getter |
| 77 | + @Setter |
| 78 | + private RetryStrategy retryStrategy; |
| 79 | + |
35 | 80 | private final CloseableHttpClient httpClient; |
36 | 81 |
|
37 | 82 | protected PubApiClientBase() { |
38 | 83 | httpClient = HttpClients.createDefault(); |
| 84 | + retryInterval = DEFAULT_RETRY_INTERVAL; |
| 85 | + maxRetries = DEFAULT_MAX_RETRIES; |
| 86 | + retryStrategy = DEFAULT_RETRY_STRATEGY; |
39 | 87 | } |
40 | 88 |
|
41 | 89 | /** |
@@ -70,15 +118,34 @@ protected String getRequest(String endpoint) throws IOException, ChessComPubApiE |
70 | 118 | } |
71 | 119 |
|
72 | 120 | String responseBody; |
73 | | - try (CloseableHttpResponse response = httpClient.execute(request)) { |
74 | | - int statusCode = response.getStatusLine().getStatusCode(); |
75 | | - //TODO: Handle rate throttling more gracefully |
76 | | - if (statusCode != ResponseCode.OK.getValue()) { |
77 | | - throw new ChessComPubApiException("Error executing GET request: API returned status code " + statusCode); |
| 121 | + Integer attempts = 1; |
| 122 | + |
| 123 | + boolean keepTrying = false; |
| 124 | + do { |
| 125 | + try (CloseableHttpResponse response = httpClient.execute(request)) { |
| 126 | + int statusCode = response.getStatusLine().getStatusCode(); |
| 127 | + if (statusCode == ResponseCode.RATE_LIMIT_EXCEEDED.getValue() && shouldTryRequestAgain(attempts)) { |
| 128 | + keepTrying = true; |
| 129 | + attempts++; |
| 130 | + Thread.sleep(retryInterval * 1000); |
| 131 | + } else if (statusCode != ResponseCode.OK.getValue()) { |
| 132 | + throw new ChessComPubApiException("Error executing GET request: API returned status code " + statusCode); |
| 133 | + } |
| 134 | + InputStream inputStream = response.getEntity().getContent(); |
| 135 | + responseBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8); |
| 136 | + } catch (InterruptedException e) { |
| 137 | + throw new ChessComPubApiException("Error while executing request: " + e.getMessage()); |
78 | 138 | } |
79 | | - InputStream inputStream = response.getEntity().getContent(); |
80 | | - responseBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8); |
81 | | - } |
| 139 | + } while (keepTrying); |
| 140 | + |
82 | 141 | return responseBody; |
83 | 142 | } |
| 143 | + |
| 144 | + private boolean shouldTryRequestAgain(Integer attemptsSoFar) { |
| 145 | + if (retryStrategy == RetryStrategy.RETRY_FOREVER) { |
| 146 | + return true; |
| 147 | + } |
| 148 | + |
| 149 | + return retryStrategy == RetryStrategy.RETRY_N_TIMES && attemptsSoFar <= maxRetries; |
| 150 | + } |
84 | 151 | } |
0 commit comments