Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Retry on 503 using the Retry-After header #15906

Merged
17 commits merged into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
4 changes: 3 additions & 1 deletion sdk/core/core-http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

### Features Added

- Changed TS compilation target to ES2017 in order to produce smaller bundles and use more native platform features
- Changed TS compilation target to ES2017 in order to produce smaller bundles and use more native platform features.
- Added support for the `Retry-After` header on responses with status code 503, Service Unavailable.
- Added support for multiple retries on the `ThrottlingRetryPolicy` (up to 3 by default).

### Breaking Changes

Expand Down
1 change: 1 addition & 0 deletions sdk/core/core-http/review/core-http.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const Constants: {
};
StatusCodes: {
TooManyRequests: number;
ServiceUnavailable: number;
};
};
HeaderConstants: {
Expand Down
10 changes: 10 additions & 0 deletions sdk/core/core-http/src/policies/exponentialRetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "../util/exponentialBackoffStrategy";
import { RestError } from "../restError";
import { logger } from "../log";
import { Constants } from "../util/constants";
import { delay } from "../util/delay";

export function exponentialRetryPolicy(
Expand Down Expand Up @@ -139,6 +140,15 @@ async function retry(
): Promise<HttpOperationResponse> {
function shouldPolicyRetry(responseParam?: HttpOperationResponse): boolean {
const statusCode = responseParam?.status;
if (
statusCode &&
statusCode === 503 &&
response &&
response.headers.get(Constants.HeaderConstants.RETRY_AFTER)
) {
return false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to put this in a separate conditional to make it easier to think through.


if (
statusCode === undefined ||
(statusCode < 500 && statusCode !== 408) ||
Expand Down
31 changes: 22 additions & 9 deletions sdk/core/core-http/src/policies/throttlingRetryPolicy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { AbortError } from "@azure/abort-controller";
import {
BaseRequestPolicy,
RequestPolicy,
Expand All @@ -10,8 +11,8 @@ import {
import { WebResourceLike } from "../webResource";
import { HttpOperationResponse } from "../httpOperationResponse";
import { Constants } from "../util/constants";
import { DEFAULT_CLIENT_MAX_RETRY_COUNT } from "../util/throttlingRetryStrategy";
import { delay } from "../util/delay";
import { AbortError } from "@azure/abort-controller";

type ResponseHandler = (
httpRequest: WebResourceLike,
Expand All @@ -37,6 +38,7 @@ const StandardAbortMessage = "The operation was aborted.";
*/
export class ThrottlingRetryPolicy extends BaseRequestPolicy {
private _handleResponse: ResponseHandler;
private numberOfRetries = 0;

constructor(
nextPolicy: RequestPolicy,
Expand All @@ -48,13 +50,15 @@ export class ThrottlingRetryPolicy extends BaseRequestPolicy {
}

public async sendRequest(httpRequest: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy.sendRequest(httpRequest.clone()).then((response) => {
if (response.status !== StatusCodes.TooManyRequests) {
return response;
} else {
return this._handleResponse(httpRequest, response);
}
});
const response = await this._nextPolicy.sendRequest(httpRequest.clone());
if (
response.status !== StatusCodes.TooManyRequests &&
response.status !== StatusCodes.ServiceUnavailable
) {
return response;
} else {
return this._handleResponse(httpRequest, response);
}
}

private async _defaultResponseHandler(
Expand All @@ -70,14 +74,23 @@ export class ThrottlingRetryPolicy extends BaseRequestPolicy {
retryAfterHeader
);
if (delayInMs) {
this.numberOfRetries += 1;

await delay(delayInMs, undefined, {
abortSignal: httpRequest.abortSignal,
abortErrorMsg: StandardAbortMessage
});

if (httpRequest.abortSignal?.aborted) {
throw new AbortError(StandardAbortMessage);
}
return this._nextPolicy.sendRequest(httpRequest);

// The original request and up to DEFAULT_CLIENT_MAX_RETRY_COUNT retries
if (this.numberOfRetries <= DEFAULT_CLIENT_MAX_RETRY_COUNT) {
return this.sendRequest(httpRequest);
} else {
return this._nextPolicy.sendRequest(httpRequest);
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion sdk/core/core-http/src/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export const Constants = {
},

StatusCodes: {
TooManyRequests: 429
TooManyRequests: 429,
ServiceUnavailable: 503
}
},

Expand Down
7 changes: 7 additions & 0 deletions sdk/core/core-http/src/util/throttlingRetryStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Maximum number of retries for the throttling retry policy
*/
export const DEFAULT_CLIENT_MAX_RETRY_COUNT = 3;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t believe we should preemptively expose this to the public API. If anything, now we retry up to 3 times on the throttlingRetryPolicy, while we were only retrying one time before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three is good, and is the same default value for exponential and system error retry policy.

24 changes: 23 additions & 1 deletion sdk/core/core-http/test/policies/throttlingRetryPolicyTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe("ThrottlingRetryPolicy", () => {
assert.deepEqual(response.request, request);
});

it("should do nothing when status code is not 429", async () => {
it("should do nothing when status code is not 429 nor 503", async () => {
const request = new WebResource();
const mockResponse = {
status: 400,
Expand Down Expand Up @@ -114,6 +114,28 @@ describe("ThrottlingRetryPolicy", () => {
assert.deepEqual(response, mockResponse);
});

it("should pass the response to the handler if the status code equals 503", async () => {
const request = new WebResource();
const mockResponse = {
status: 503,
headers: new HttpHeaders({
"Retry-After": "100"
}),
request: request
};
const policy = createDefaultThrottlingRetryPolicy(mockResponse, (_, response) => {
delete (response.request as any).requestId;
delete (mockResponse.request as any).requestId;
assert.deepEqual(response, mockResponse);
return Promise.resolve(response);
});

const response = await policy.sendRequest(request);
delete (request as any).requestId;
delete (response.request as any).requestId;
assert.deepEqual(response, mockResponse);
});

it("should honor the abort signal passed", async () => {
const request = new WebResource(
"https://fakeservice.io",
Expand Down
6 changes: 6 additions & 0 deletions sdk/core/core-rest-pipeline/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

- Fixed an issue where `proxySettings` does not work when there is username but no password [Issue 15720](https://github.com/Azure/azure-sdk-for-js/issues/15720)

### Features Added

- Added support for the `Retry-After` header on responses with status code 503, Service Unavailable.
- The `ExponentialRetryPolicy` will now ignore `503` responses if they have the `Retry-After` header.
- Added support for multiple retries on the `ThrottlingRetryPolicy` (up to 3 by default).

### Breaking Changes

- Updated @azure/core-tracing to version `1.0.0-preview.12`. See [@azure/core-tracing CHANGELOG](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-tracing/CHANGELOG.md) for details about breaking changes with tracing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ export function exponentialRetryPolicy(
* @param retryData - The retry data.
* @returns True if the operation qualifies for a retry; false otherwise.
*/
function shouldRetry(statusCode: number | undefined, retryData: RetryData): boolean {
function shouldRetry(response: PipelineResponse | undefined, retryData: RetryData): boolean {
const statusCode = response?.status;
if (statusCode && statusCode === 503 && response && response.headers.get("Retry-After")) {
return false;
}

if (
statusCode === undefined ||
(statusCode < 500 && statusCode !== 408) ||
Expand Down Expand Up @@ -126,7 +131,7 @@ export function exponentialRetryPolicy(
): Promise<PipelineResponse> {
retryData = updateRetryData(retryData, requestError);
const isAborted = request.abortSignal?.aborted;
if (!isAborted && shouldRetry(response?.status, retryData)) {
if (!isAborted && shouldRetry(response, retryData)) {
logger.info(`Retrying request in ${retryData.retryInterval}`);
try {
await delay(retryData.retryInterval);
Expand Down
29 changes: 18 additions & 11 deletions sdk/core/core-rest-pipeline/src/policies/throttlingRetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { delay } from "../util/helpers";
*/
export const throttlingRetryPolicyName = "throttlingRetryPolicy";

/**
* Maximum number of retries for the throttling retry policy
*/
export const DEFAULT_CLIENT_MAX_RETRY_COUNT = 3;

/**
* A policy that retries when the server sends a 429 response with a Retry-After header.
*
Expand All @@ -22,21 +27,23 @@ export function throttlingRetryPolicy(): PipelinePolicy {
return {
name: throttlingRetryPolicyName,
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
const response = await next(request);
if (response.status !== 429) {
return response;
}
let response = await next(request);

const retryAfterHeader = response.headers.get("Retry-After");

if (retryAfterHeader) {
for (let count = 0; count <= DEFAULT_CLIENT_MAX_RETRY_COUNT; count++) {
if (response.status !== 429 && response.status !== 503) {
return response;
}
const retryAfterHeader = response.headers.get("Retry-After");
if (!retryAfterHeader) {
break;
}
const delayInMs = parseRetryAfterHeader(retryAfterHeader);
if (delayInMs) {
await delay(delayInMs);
return next(request);
if (!delayInMs) {
break;
}
await delay(delayInMs);
response = await next(request);
}

return response;
}
};
Expand Down
Loading