Skip to content

Commit ec04ce9

Browse files
authored
COMPUTE-1236: Add support for 429 response code from platform (#132)
* COMPUTE-1236: Add support for 429 response code from platform * Fix test
1 parent 4b9c7c3 commit ec04ce9

File tree

11 files changed

+253
-34
lines changed

11 files changed

+253
-34
lines changed

DEVELOPING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
```
1616
* Note that dxScala will compile with JDK8 or JDK11 and that JDK8 is used as the build target so the resulting JAR file can be executed with JRE8 or later.
1717
* Install [sbt](https://www.scala-sbt.org/), which also installs Scala. Sbt is a make-like utility that works with the ```scala``` language.
18-
* On MacOS: `brew install sbt`
18+
* On MacOS: `brew install sbt` or `brew install --ignore-dependencies sbt` (if you don't want to install the newest JDK)
1919
* On Linux:
2020
```
2121
$ wget www.scala-lang.org/files/archive/scala-2.13.7.deb
@@ -48,7 +48,7 @@ If you want to make a change to dxScala, do the following:
4848
3. If the current snapshot version matches the release version, increment the snapshot version.
4949
- For example, if the current release is `1.0.0` and the current snapshot version is `1.0.0-SNAPSHOT`, increment the snapshot version to `1.0.1-SNAPSHOT`.
5050
4. Make your changes. Test locally using `sbt test`.
51-
5. Update the release notes under the top-most header (which should be "in develop").
51+
5. Update the release notes under the top-most header (which should be "unreleased").
5252
6. If the current snapshot version only differs from the release version by a patch, and you added any new functionality (vs just fixing a bug), increment the minor version instead.
5353
- For example, when you first created the branch you set the version to `1.0.1-SNAPSHOT`, but then you realized you needed to add a new function to the public API, change the version to `1.1.0-SNAPSHOT`.
5454
7. When you are done, create a pull request against the `develop` branch.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright 2024 DNAnexus
189+
Copyright 2025 DNAnexus
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

api/RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## unreleased
44

5+
* Support 429 Too Many Requests response code from platform to retry throttled requests.
6+
* Add identity tokens requests.
7+
* Add version number in user-agent string.
8+
59
## 0.13.10 (2024-03-25)
610

711
* Change to make the user-agent string for dxScala more distinctive.

api/src/main/java/com/dnanexus/DXAPI.java

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21141,6 +21141,192 @@ public static JsonNode jobUpdate(String objectId, JsonNode inputParams, DXEnviro
2114121141
RetryStrategy.SAFE_TO_RETRY);
2114221142
}
2114321143

21144+
/**
21145+
* Invokes the jobGetIdentityToken method with an empty input, deserializing to an object of the specified class.
21146+
*
21147+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21148+
*
21149+
* @param objectId ID of the object to operate on
21150+
* @param outputClass class to deserialize the server reponse to
21151+
*
21152+
* @return Response object
21153+
*
21154+
* @throws DXAPIException
21155+
* If the server returns a complete response with an HTTP status
21156+
* code other than 200 (OK).
21157+
* @throws DXHTTPException
21158+
* If an error occurs while making the HTTP request or obtaining
21159+
* the response (includes HTTP protocol errors).
21160+
*/
21161+
public static <T> T jobGetIdentityToken(String objectId, Class<T> outputClass) {
21162+
return jobGetIdentityToken(objectId, mapper.createObjectNode(), outputClass);
21163+
}
21164+
/**
21165+
* Invokes the jobGetIdentityToken method with the given input, deserializing to an object of the specified class.
21166+
*
21167+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21168+
*
21169+
* @param objectId ID of the object to operate on
21170+
* @param inputObject input object (to be JSON serialized to an input hash)
21171+
* @param outputClass class to deserialize the server reponse to
21172+
*
21173+
* @return Response object
21174+
*
21175+
* @throws DXAPIException
21176+
* If the server returns a complete response with an HTTP status
21177+
* code other than 200 (OK).
21178+
* @throws DXHTTPException
21179+
* If an error occurs while making the HTTP request or obtaining
21180+
* the response (includes HTTP protocol errors).
21181+
*/
21182+
public static <T> T jobGetIdentityToken(String objectId, Object inputObject, Class<T> outputClass) {
21183+
JsonNode input = mapper.valueToTree(inputObject);
21184+
return DXJSON.safeTreeToValue(
21185+
new DXHTTPRequest().request("/" + objectId + "/" + "getIdentityToken",
21186+
input, RetryStrategy.SAFE_TO_RETRY), outputClass);
21187+
}
21188+
/**
21189+
* Invokes the jobGetIdentityToken method with an empty input using the given environment, deserializing to an object of the specified class.
21190+
*
21191+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21192+
*
21193+
* @param objectId ID of the object to operate on
21194+
* @param outputClass class to deserialize the server reponse to
21195+
* @param env environment object specifying the auth token and remote server and protocol
21196+
*
21197+
* @return Response object
21198+
*
21199+
* @throws DXAPIException
21200+
* If the server returns a complete response with an HTTP status
21201+
* code other than 200 (OK).
21202+
* @throws DXHTTPException
21203+
* If an error occurs while making the HTTP request or obtaining
21204+
* the response (includes HTTP protocol errors).
21205+
*/
21206+
public static <T> T jobGetIdentityToken(String objectId, Class<T> outputClass, DXEnvironment env) {
21207+
return jobGetIdentityToken(objectId, mapper.createObjectNode(), outputClass, env);
21208+
}
21209+
/**
21210+
* Invokes the jobGetIdentityToken method with the given input using the given environment, deserializing to an object of the specified class.
21211+
*
21212+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21213+
*
21214+
* @param objectId ID of the object to operate on
21215+
* @param inputObject input object (to be JSON serialized to an input hash)
21216+
* @param outputClass class to deserialize the server reponse to
21217+
* @param env environment object specifying the auth token and remote server and protocol
21218+
*
21219+
* @return Response object
21220+
*
21221+
* @throws DXAPIException
21222+
* If the server returns a complete response with an HTTP status
21223+
* code other than 200 (OK).
21224+
* @throws DXHTTPException
21225+
* If an error occurs while making the HTTP request or obtaining
21226+
* the response (includes HTTP protocol errors).
21227+
*/
21228+
public static <T> T jobGetIdentityToken(String objectId, Object inputObject, Class<T> outputClass, DXEnvironment env) {
21229+
JsonNode input = mapper.valueToTree(inputObject);
21230+
return DXJSON.safeTreeToValue(
21231+
new DXHTTPRequest(env).request("/" + objectId + "/" + "getIdentityToken",
21232+
input, RetryStrategy.SAFE_TO_RETRY), outputClass);
21233+
}
21234+
21235+
/**
21236+
* Invokes the jobGetIdentityToken method.
21237+
*
21238+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21239+
*
21240+
* @param objectId ID of the object to operate on
21241+
*
21242+
* @return Server response parsed from JSON
21243+
*
21244+
* @throws DXAPIException
21245+
* If the server returns a complete response with an HTTP status
21246+
* code other than 200 (OK).
21247+
* @throws DXHTTPException
21248+
* If an error occurs while making the HTTP request or obtaining
21249+
* the response (includes HTTP protocol errors).
21250+
*
21251+
* @deprecated Use {@link #jobGetIdentityToken(String, Class)} instead and supply your own class to deserialize to.
21252+
*/
21253+
@Deprecated
21254+
public static JsonNode jobGetIdentityToken(String objectId) {
21255+
return jobGetIdentityToken(objectId, mapper.createObjectNode());
21256+
}
21257+
/**
21258+
* Invokes the jobGetIdentityToken method with the specified parameters.
21259+
*
21260+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21261+
*
21262+
* @param objectId ID of the object to operate on
21263+
* @param inputParams input parameters to the API call
21264+
*
21265+
* @return Server response parsed from JSON
21266+
*
21267+
* @throws DXAPIException
21268+
* If the server returns a complete response with an HTTP status
21269+
* code other than 200 (OK).
21270+
* @throws DXHTTPException
21271+
* If an error occurs while making the HTTP request or obtaining
21272+
* the response (includes HTTP protocol errors).
21273+
*
21274+
* @deprecated Use {@link #jobGetIdentityToken(String, Object, Class)} instead and supply your own class to deserialize to.
21275+
*/
21276+
@Deprecated
21277+
public static JsonNode jobGetIdentityToken(String objectId, JsonNode inputParams) {
21278+
return new DXHTTPRequest().request("/" + objectId + "/" + "getIdentityToken", inputParams,
21279+
RetryStrategy.SAFE_TO_RETRY);
21280+
}
21281+
/**
21282+
* Invokes the jobGetIdentityToken method with the specified environment.
21283+
*
21284+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21285+
*
21286+
* @param objectId ID of the object to operate on
21287+
* @param env environment object specifying the auth token and remote server and protocol
21288+
*
21289+
* @return Server response parsed from JSON
21290+
*
21291+
* @throws DXAPIException
21292+
* If the server returns a complete response with an HTTP status
21293+
* code other than 200 (OK).
21294+
* @throws DXHTTPException
21295+
* If an error occurs while making the HTTP request or obtaining
21296+
* the response (includes HTTP protocol errors).
21297+
*
21298+
* @deprecated Use {@link #jobGetIdentityToken(String, Class, DXEnvironment)} instead and supply your own class to deserialize to.
21299+
*/
21300+
@Deprecated
21301+
public static JsonNode jobGetIdentityToken(String objectId, DXEnvironment env) {
21302+
return jobGetIdentityToken(objectId, mapper.createObjectNode(), env);
21303+
}
21304+
/**
21305+
* Invokes the jobGetIdentityToken method with the specified environment and parameters.
21306+
*
21307+
* <p>For more information about this method, see the <a href="https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken">API specification</a>.
21308+
*
21309+
* @param objectId ID of the object to operate on
21310+
* @param inputParams input parameters to the API call
21311+
* @param env environment object specifying the auth token and remote server and protocol
21312+
*
21313+
* @return Server response parsed from JSON
21314+
*
21315+
* @throws DXAPIException
21316+
* If the server returns a complete response with an HTTP status
21317+
* code other than 200 (OK).
21318+
* @throws DXHTTPException
21319+
* If an error occurs while making the HTTP request or obtaining
21320+
* the response (includes HTTP protocol errors).
21321+
*
21322+
* @deprecated Use {@link #jobGetIdentityToken(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to.
21323+
*/
21324+
@Deprecated
21325+
public static JsonNode jobGetIdentityToken(String objectId, JsonNode inputParams, DXEnvironment env) {
21326+
return new DXHTTPRequest(env).request("/" + objectId + "/" + "getIdentityToken", inputParams,
21327+
RetryStrategy.SAFE_TO_RETRY);
21328+
}
21329+
2114421330
/**
2114521331
* Invokes the jobNew method with an empty input, deserializing to an object of the specified class.
2114621332
*

api/src/main/java/com/dnanexus/DXHTTPRequest.java

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ private ParsedResponse requestImpl(String resource, String data, boolean parseRe
287287
// Retry with exponential backoff
288288
int timeoutSeconds = 1;
289289
int attempts = 0;
290+
int attemptsWithThrottled = 0;
290291

291292
while (true) {
292293
Integer statusCode = null;
@@ -356,6 +357,21 @@ private ParsedResponse requestImpl(String resource, String data, boolean parseRe
356357
} else {
357358
return new ParsedResponse(new String(value, Charset.forName("UTF-8")), null);
358359
}
360+
} else if (statusCode == 503 || statusCode == 429) {
361+
retryRequest = true;
362+
Header retryAfterHeader = response.getFirstHeader("retry-after");
363+
// Consume the response to avoid leaking resources
364+
EntityUtils.consume(entity);
365+
if (retryAfterHeader != null) {
366+
try {
367+
retryAfterSeconds = Integer.parseInt(retryAfterHeader.getValue());
368+
} catch (NumberFormatException e) {
369+
// Just fall back to the default
370+
}
371+
}
372+
373+
// will be processed further in this file
374+
throw new ServiceUnavailableException("503 Service Unavailable", statusCode, retryAfterSeconds);
359375
} else if (statusCode < 500) {
360376
// 4xx errors should be considered not recoverable.
361377
String responseStr = EntityUtils.toString(entity);
@@ -383,44 +399,45 @@ private ParsedResponse requestImpl(String resource, String data, boolean parseRe
383399
throw DXAPIException.getInstance(errorType, errorMessage, statusCode);
384400
} else {
385401
// Propagate 500 error to caller
386-
if (this.disableRetry && statusCode != 503) {
402+
if (this.disableRetry) {
387403
logError("POST " + resource + ": " + statusCode + " Internal Server Error, try "
388404
+ String.valueOf(attempts + 1) + "/" + NUM_RETRIES
389405
+ " Request ID: " + requestId);
390406
throw new InternalErrorException("Internal Server Error", statusCode);
391407
}
392408
// If retries enabled, 500 InternalError should get retried unconditionally
393409
retryRequest = true;
394-
if (statusCode == 503) {
395-
Header retryAfterHeader = response.getFirstHeader("retry-after");
396-
// Consume the response to avoid leaking resources
397-
EntityUtils.consume(entity);
398-
if (retryAfterHeader != null) {
399-
try {
400-
retryAfterSeconds = Integer.parseInt(retryAfterHeader.getValue());
401-
} catch (NumberFormatException e) {
402-
// Just fall back to the default
403-
}
404-
}
405-
throw new ServiceUnavailableException("503 Service Unavailable", statusCode, retryAfterSeconds);
410+
String entityStr = EntityUtils.toString(entity);
411+
String msg;
412+
if (entityStr == null || "".equals(entityStr)) {
413+
// This can happen if the server was unable to send response back to us,
414+
// as happened with TIP-1743. Make this more clear in the exception.
415+
msg = "No response entity received";
416+
} else {
417+
msg = String.format("Response entity: %s", entityStr);
406418
}
407-
throw new IOException(EntityUtils.toString(entity));
419+
throw new IOException(msg);
408420
}
409421
} catch (ServiceUnavailableException e) {
410-
int secondsToWait = retryAfterSeconds;
422+
// increased backoff for throttled requests over attempts up to x5 times from the original
423+
double increasedBackoff = retryAfterSeconds < 60
424+
? retryAfterSeconds + 0.25 * Math.min(20, attemptsWithThrottled) * retryAfterSeconds
425+
: retryAfterSeconds;
426+
timeoutSeconds = (int) increasedBackoff;
427+
428+
String logMessage = String.format("POST %s: %s, attempt %d, %s %d seconds. Request ID: %s",
429+
resource,
430+
statusCode == 429 ? "429 Too Many Requests" : "503 Service Unavailable",
431+
attemptsWithThrottled + 1,
432+
this.disableRetry ? "suggested wait " : "waiting for",
433+
timeoutSeconds,
434+
requestId);
435+
436+
logError(logMessage);
411437

412438
if (this.disableRetry) {
413-
logError("POST " + resource + ": 503 Service Unavailable, suggested wait "
414-
+ secondsToWait + " seconds" + ". Request ID: " + requestId);
415439
throw e;
416440
}
417-
418-
// Retries due to 503 Service Unavailable and Retry-After do NOT count against the
419-
// allowed number of retries.
420-
logError("POST " + resource + ": 503 Service Unavailable, waiting for "
421-
+ Integer.toString(secondsToWait) + " seconds" + " Request ID: " + requestId);
422-
sleep(secondsToWait);
423-
continue;
424441
} catch (IOException e) {
425442
// Note, this catches both exceptions directly thrown from httpclient.execute (e.g.
426443
// no connectivity to server) and exceptions thrown by our code above after parsing
@@ -434,12 +451,19 @@ private ParsedResponse requestImpl(String resource, String data, boolean parseRe
434451
throw new InternalErrorException("Maximum number of retries reached, or unsafe to retry",
435452
statusCode);
436453
}
454+
455+
timeoutSeconds *= 2;
437456
}
438457

439458
assert attempts < NUM_RETRIES;
440459
assert retryRequest;
441460

442-
attempts++;
461+
attemptsWithThrottled++;
462+
// Retries due to 429 or 503 Service Unavailable and Retry-After do NOT count against the
463+
// allowed number of retries.
464+
if (statusCode != 503 && statusCode != 429) {
465+
attempts++;
466+
}
443467

444468
// The number of failed attempts is now no more than NUM_RETRIES, and the total number
445469
// of attempts allowed is NUM_RETRIES + 1 (the first attempt, plus up to NUM_RETRIES
@@ -452,7 +476,6 @@ private ParsedResponse requestImpl(String resource, String data, boolean parseRe
452476

453477

454478
sleep(timeoutSeconds);
455-
timeoutSeconds *= 2;
456479
}
457480
}
458481
}

0 commit comments

Comments
 (0)