Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
19c7a19
Adding retry config
HantingZhang2 Aug 3, 2023
bfcdd61
Implement retry logic
HantingZhang2 Aug 4, 2023
f5a8069
correct millis
HantingZhang2 Aug 4, 2023
d65da68
add default to retry config
HantingZhang2 Aug 4, 2023
fb23a96
change retry interval calculation
HantingZhang2 Aug 4, 2023
b331730
first test iteration
HantingZhang2 Aug 7, 2023
f73f609
pre-commit fixes
Aug 7, 2023
c336631
Using apiclient With Retry
HantingZhang2 Aug 7, 2023
c5f2680
Merge branch 'hantingz/add-retry' of github.com:DataDog/datadog-api-c…
HantingZhang2 Aug 7, 2023
ef4a9b5
pre-commit fixes
Aug 7, 2023
4caf8ba
Add retry test cassette
HantingZhang2 Aug 8, 2023
e692c77
pre-commit fixes
Aug 8, 2023
864c584
add generator code
HantingZhang2 Aug 8, 2023
dd5b81c
pre-commit fixes
Aug 8, 2023
5f3aaef
Add retryconfig back
HantingZhang2 Aug 8, 2023
b470314
pre-commit fixes
Aug 8, 2023
092528f
Update ReadMe
HantingZhang2 Aug 8, 2023
7daa698
pre-commit fixes
Aug 8, 2023
0fe83c5
Add RetryConfig in generator file item
HantingZhang2 Aug 8, 2023
b3c4c2b
Merge branch 'hantingz/add-retry' of github.com:DataDog/datadog-api-c…
HantingZhang2 Aug 8, 2023
0d450ef
Merge branch 'master' into hantingz/add-retry
HantingZhang2 Aug 8, 2023
57dec22
add retryConfig back
HantingZhang2 Aug 8, 2023
e35b275
pre-commit fixes
Aug 8, 2023
474db21
fix function name
HantingZhang2 Aug 9, 2023
0aa6aa3
Add cassette for status code 500 test
HantingZhang2 Aug 9, 2023
30dbb8d
Add enable retry method on the client
HantingZhang2 Aug 9, 2023
d952594
pre-commit fixes
Aug 9, 2023
0c13e0a
update retryConfig object and test casettes
HantingZhang2 Aug 9, 2023
1478c67
pre-commit fixes
Aug 9, 2023
d2cf66c
Refactor tests and retry config
HantingZhang2 Aug 9, 2023
9e1b705
pre-commit fixes
Aug 9, 2023
b664dd8
change test assert
HantingZhang2 Aug 9, 2023
e27c5e3
add back sleep method using retry object
HantingZhang2 Aug 9, 2023
caa432b
pre-commit fixes
Aug 9, 2023
ae5600c
refresh jdoc
HantingZhang2 Aug 9, 2023
533fb45
pre-commit fixes
Aug 9, 2023
4352fc2
Remove param for jdoc
HantingZhang2 Aug 9, 2023
a913feb
pre-commit fixes
Aug 9, 2023
9aef306
Add mock retry config and simplify the sleep
HantingZhang2 Aug 10, 2023
999073d
pre-commit fixes
Aug 10, 2023
5f4938f
fix for comments
HantingZhang2 Aug 11, 2023
dcdd922
pre-commit fixes
Aug 11, 2023
a511cc8
refactor retry test
HantingZhang2 Aug 11, 2023
1e0adb7
pre-commit fixes
Aug 11, 2023
dd420ec
Rearrange mockconfig
HantingZhang2 Aug 11, 2023
e18d64a
pre-commit fixes
Aug 11, 2023
858d2ce
Merge branch 'master' into hantingz/add-retry
therve Aug 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .generator/src/generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def cli(specs, output):
"PaginationIterator.java": env.get_template("PaginationIterator.j2"),
"UnparsedObject.java": env.get_template("UnparsedObject.j2"),
"ZstdEncoder.java": env.get_template("ZstdEncoder.j2"),
"RetryConfig.java": env.get_template("RetryConfig.j2"),
}

auth_files = {
Expand Down
117 changes: 87 additions & 30 deletions .generator/src/generator/templates/ApiClient.j2
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {{ common_package_name }}.ApiException;
import {{ common_package_name }}.ApiResponse;
import {{ common_package_name }}.JSON;
import {{ common_package_name }}.Pair;
import {{ common_package_name }}.RetryConfig;
import {{ common_package_name }}.RFC3339DateFormat;
import {{ common_package_name }}.ServerConfiguration;
import {{ common_package_name }}.ServerVariable;
Expand Down Expand Up @@ -162,6 +163,7 @@ public class ApiClient {
protected Map<String, Integer> operationServerIndex = new HashMap<String, Integer>();
protected Map<String, Map<String, String>> operationServerVariables = new HashMap<String, Map<String, String>>();
protected boolean debugging = false;
protected RetryConfig retry = new RetryConfig(false, 2, 2, 3);
protected boolean compress = true;
protected ClientConfig clientConfig;
protected int connectionTimeout = 0;
Expand Down Expand Up @@ -288,6 +290,30 @@ public class ApiClient {
return offsetDateTimeFormatter;
}

/**
* Add custom retry object in the client
* @param retry retry object
* */
public void setRetry(RetryConfig retry) {
this.retry = retry;
}

/**
* Return the retryConfig object
* @return retryConfig
*/
public RetryConfig getRetry() {
return retry;
}

/**
* Enable retry directly on the client instead of creating a new retry object
* @param enableRetry bool, enable retry or not
*/
public void enableRetry(boolean enableRetry){
this.retry.setEnableRetry(enableRetry);
}

/**
* Set the date format used to parse/format {@code OffsetDateTime} parameters.
*
Expand Down Expand Up @@ -1229,45 +1255,76 @@ public class ApiClient {
throws ApiException {

String contentEncoding = headerParams.get(HttpHeaders.CONTENT_ENCODING);
Entity<?> entity = serialize(body, formParams, selectHeaderContentType(contentTypes), contentEncoding, isBodyNullable);
Entity<?> entity =
serialize(
body,
formParams,
selectHeaderContentType(contentTypes),
contentEncoding,
isBodyNullable);

Response response = null;

try {
response = sendRequest(method, invocationBuilder, entity);

int statusCode = response.getStatusInfo().getStatusCode();
Map<String, List<String>> responseHeaders = buildResponseHeaders(response);

if (response.getStatusInfo() == Status.NO_CONTENT) {
return new ApiResponse<T>(statusCode, responseHeaders);
} else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) {
if (returnType == null) {
int currentRetry = 0;
while (true){
response = sendRequest(method, invocationBuilder, entity);
int statusCode = response.getStatusInfo().getStatusCode();
Map<String, List<String>> responseHeaders = buildResponseHeaders(response);
if (response.getStatusInfo() == Status.NO_CONTENT) {
return new ApiResponse<T>(statusCode, responseHeaders);
} else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) {
if (returnType == null) {
return new ApiResponse<T>(statusCode, responseHeaders);
} else {
return new ApiResponse<T>(statusCode, responseHeaders, deserialize(response, returnType));
}
} else if (shouldRetry(currentRetry, statusCode, retry)){
retry.sleepInterval(calculateRetryInterval(responseHeaders, retry, currentRetry));
currentRetry++;
} else {
return new ApiResponse<T>(statusCode, responseHeaders, deserialize(response, returnType));
}
} else {
String message = "error";
String respBody = null;
if (response.hasEntity()) {
try {
respBody = String.valueOf(response.readEntity(String.class));
message = respBody;
} catch (RuntimeException e) {
// e.printStackTrace();
String message = "error";
String respBody = null;
if (response.hasEntity()) {
try {
respBody = String.valueOf(response.readEntity(String.class));
message = respBody;
} catch (RuntimeException e) {
// e.printStackTrace();
}
}
}
throw new ApiException(
response.getStatus(), message, buildResponseHeaders(response), respBody);
}
throw new ApiException(
response.getStatus(), message, buildResponseHeaders(response), respBody);
}
}
} finally {
try {
response.close();
} catch (Exception e) {
// it's not critical, since the response object is local in method invokeAPI; that's fine,
// just continue
try {
response.close();
} catch (Exception e) {
// it's not critical, since the response object is local in method invokeAPI; that's fine,
// just continue
}
}
}

private boolean shouldRetry(int retryCount, int statusCode, RetryConfig retryConfig){
boolean statusToRetry = false;
if (statusCode == 429 || statusCode >= 500){
statusToRetry = true;
}
return (retryConfig.maxRetries>retryCount && statusToRetry && retryConfig.isEnableRetry());
}

private int calculateRetryInterval(Map<String, List<String>> responseHeaders, RetryConfig retryConfig, int retryCount){
if ( responseHeaders.get("x-ratelimit-reset")!=null){
List<String> rateLimitHeader = responseHeaders.get("x-ratelimit-reset");
return Integer.parseInt(rateLimitHeader.get(0));
} else {
int retryInterval= (int) Math.pow (retry.backOffMultiplier, retryCount)* retryConfig.backOffBase;
if (getConnectTimeout()>0){
retryInterval = Math.min(retryInterval, getConnectTimeout());
}
return retryInterval;
}
}

Expand Down
64 changes: 64 additions & 0 deletions .generator/src/generator/templates/RetryConfig.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{% include "ApiInfo.j2" %}

package {{ common_package_name }};

public class RetryConfig {
public boolean enableRetry;
public int backOffMultiplier;
public int backOffBase;
public int maxRetries;

/**
* @param enableRetry Enable retry when rate limited
* @param backOffMultiplier Multiplier for retry backoff
* @param backOffBase Base for retry backoff
* @param maxRetries Maximum number of retries
*/
public RetryConfig(boolean enableRetry, int backOffMultiplier, int backOffBase, int maxRetries) {
this.enableRetry = enableRetry;
this.backOffMultiplier = backOffMultiplier;
this.backOffBase = backOffBase;
this.maxRetries = maxRetries;
}

public boolean isEnableRetry() {
return enableRetry;
}

public int getBackOffMultiplier() {
return backOffMultiplier;
}

public int getBackOffBase() {
return backOffBase;
}

public int getMaxRetries() {
return maxRetries;
}

public void setEnableRetry(boolean enableRetry) {
this.enableRetry = enableRetry;
}

public void setBackOffMultiplier(int backOffMultiplier) {
this.backOffMultiplier = backOffMultiplier;
}

public void setBackOffBase(int backOffBase) {
this.backOffBase = backOffBase;
}

public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}

public void sleepInterval(int sleepInterval) {
try {
Thread.sleep(sleepInterval * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ If you want to enable requests logging, set the `debugging` flag on your client:
defaultClient.setDebugging(true)
```

### Enable Retry

To enable the client to retry when rate limited (status 429) or status 500 and above:

```java
defaultClient.enableRetry(true)
```

The interval between 2 retry attempts will be the value of the `x-ratelimit-reset` response header when available. If not, it will be :
`Math.pow (multiplier_for_retry_backoff, current_retry_count)*base_for_retry_backoff`.

### Configure proxy

You can provide custom `connectorProvider` implemtation to `clientConfig` to use proxy. See example below using `ApacheConnectorProvider`:
Expand Down
100 changes: 78 additions & 22 deletions src/main/java/com/datadog/api/client/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ public class ApiClient {
protected Map<String, Map<String, String>> operationServerVariables =
new HashMap<String, Map<String, String>>();
protected boolean debugging = false;
protected RetryConfig retry = new RetryConfig(false, 2, 2, 3);
protected boolean compress = true;
protected ClientConfig clientConfig;
protected int connectionTimeout = 0;
Expand Down Expand Up @@ -517,6 +518,33 @@ public DateTimeFormatter getOffsetDateTimeFormatter() {
return offsetDateTimeFormatter;
}

/**
* Add custom retry object in the client
*
* @param retry retry object
*/
public void setRetry(RetryConfig retry) {
this.retry = retry;
}

/**
* Return the retryConfig object
*
* @return retryConfig
*/
public RetryConfig getRetry() {
return retry;
}

/**
* Enable retry directly on the client instead of creating a new retry object
*
* @param enableRetry bool, enable retry or not
*/
public void enableRetry(boolean enableRetry) {
this.retry.setEnableRetry(enableRetry);
}

/**
* Set the date format used to parse/format {@code OffsetDateTime} parameters.
*
Expand Down Expand Up @@ -1525,32 +1553,37 @@ public <T> ApiResponse<T> invokeAPI(
Response response = null;

try {
response = sendRequest(method, invocationBuilder, entity);

int statusCode = response.getStatusInfo().getStatusCode();
Map<String, List<String>> responseHeaders = buildResponseHeaders(response);

if (response.getStatusInfo() == Status.NO_CONTENT) {
return new ApiResponse<T>(statusCode, responseHeaders);
} else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) {
if (returnType == null) {
int currentRetry = 0;
while (true) {
response = sendRequest(method, invocationBuilder, entity);
int statusCode = response.getStatusInfo().getStatusCode();
Map<String, List<String>> responseHeaders = buildResponseHeaders(response);
if (response.getStatusInfo() == Status.NO_CONTENT) {
return new ApiResponse<T>(statusCode, responseHeaders);
} else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) {
if (returnType == null) {
return new ApiResponse<T>(statusCode, responseHeaders);
} else {
return new ApiResponse<T>(
statusCode, responseHeaders, deserialize(response, returnType));
}
} else if (shouldRetry(currentRetry, statusCode, retry)) {
retry.sleepInterval(calculateRetryInterval(responseHeaders, retry, currentRetry));
currentRetry++;
} else {
return new ApiResponse<T>(statusCode, responseHeaders, deserialize(response, returnType));
}
} else {
String message = "error";
String respBody = null;
if (response.hasEntity()) {
try {
respBody = String.valueOf(response.readEntity(String.class));
message = respBody;
} catch (RuntimeException e) {
// e.printStackTrace();
String message = "error";
String respBody = null;
if (response.hasEntity()) {
try {
respBody = String.valueOf(response.readEntity(String.class));
message = respBody;
} catch (RuntimeException e) {
// e.printStackTrace();
}
}
throw new ApiException(
response.getStatus(), message, buildResponseHeaders(response), respBody);
}
throw new ApiException(
response.getStatus(), message, buildResponseHeaders(response), respBody);
}
} finally {
try {
Expand All @@ -1562,6 +1595,29 @@ public <T> ApiResponse<T> invokeAPI(
}
}

private boolean shouldRetry(int retryCount, int statusCode, RetryConfig retryConfig) {
boolean statusToRetry = false;
if (statusCode == 429 || statusCode >= 500) {
statusToRetry = true;
}
return (retryConfig.maxRetries > retryCount && statusToRetry && retryConfig.isEnableRetry());
}

private int calculateRetryInterval(
Map<String, List<String>> responseHeaders, RetryConfig retryConfig, int retryCount) {
if (responseHeaders.get("x-ratelimit-reset") != null) {
List<String> rateLimitHeader = responseHeaders.get("x-ratelimit-reset");
return Integer.parseInt(rateLimitHeader.get(0));
} else {
int retryInterval =
(int) Math.pow(retry.backOffMultiplier, retryCount) * retryConfig.backOffBase;
if (getConnectTimeout() > 0) {
retryInterval = Math.min(retryInterval, getConnectTimeout());
}
return retryInterval;
}
}

private Response sendRequest(
String method, Invocation.Builder invocationBuilder, Entity<?> entity) {
Response response;
Expand Down
Loading