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-http] Throttling retry policy fix in core-http #15832

Merged
31 commits merged into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0616843
throttling policy fix in core-http
HarshaNalluru Jun 17, 2021
10a3816
remove comment
HarshaNalluru Jun 17, 2021
8bf61fe
typo
HarshaNalluru Jun 17, 2021
d876b54
import core-amqp delay from core-util
HarshaNalluru Jun 17, 2021
db74514
import core-util
HarshaNalluru Jun 17, 2021
01bd7c4
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-js in…
HarshaNalluru Jun 17, 2021
a0307af
import { delay } from "@azure/core-util";
HarshaNalluru Jun 17, 2021
78814aa
changelog
HarshaNalluru Jun 17, 2021
ff9cb2a
throw abort error check
HarshaNalluru Jun 17, 2021
d003ea8
revert core-rest-pipeline
HarshaNalluru Jun 17, 2021
15f9428
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-js into…
HarshaNalluru Jun 18, 2021
c774368
import { delay } from "@azure/core-util";
HarshaNalluru Jun 18, 2021
daedfbb
fix linter errors
HarshaNalluru Jun 18, 2021
0dd2974
fix build failures
HarshaNalluru Jun 18, 2021
6c4ed84
export { delay } from "@azure/core-util";
HarshaNalluru Jun 18, 2021
0ad0a26
API Report
HarshaNalluru Jun 18, 2021
f7e294a
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-js into…
HarshaNalluru Jun 23, 2021
d80e0d9
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-js into…
HarshaNalluru Jun 23, 2021
97c6f0a
have 2 copies of delay
HarshaNalluru Jun 23, 2021
99f4d41
keep duplicates
HarshaNalluru Jun 23, 2021
8d77016
API Report
HarshaNalluru Jun 23, 2021
ed5e621
retain core-util
HarshaNalluru Jun 23, 2021
8977a42
get rid of circular dependencies
HarshaNalluru Jun 24, 2021
3029418
value should be the second argument
HarshaNalluru Jun 24, 2021
75ad58e
update delay to have options bag and additional feedback
HarshaNalluru Jun 24, 2021
6ede956
make async functions and typeguard feedback
HarshaNalluru Jun 24, 2021
37018a1
update app-config and API Report
HarshaNalluru Jun 24, 2021
1a6555b
HttpMockFacade
HarshaNalluru Jun 24, 2021
58b5855
should honor the abort signal passed
HarshaNalluru Jun 24, 2021
a3a0a04
remove nock
HarshaNalluru Jun 24, 2021
9b7f233
satisfy linter
HarshaNalluru Jun 24, 2021
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { AbortError, AbortSignalLike } from "@azure/abort-controller";
import { AbortError } from "@azure/abort-controller";
import {
BaseRequestPolicy,
RequestPolicy,
Expand All @@ -12,7 +12,7 @@ import {
Constants,
RestError
} from "@azure/core-http";
Copy link
Member

Choose a reason for hiding this comment

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

Now we just need to move to corev2 so we can fix #6484

import { isDefined } from "../internal/typeguards";
import { delay } from "@azure/core-http";

/**
* @internal
Expand All @@ -27,55 +27,6 @@ export function throttlingRetryPolicy(): RequestPolicyFactory {

const StandardAbortMessage = "The operation was aborted.";

/**
* A wrapper for setTimeout that resolves a promise after t milliseconds.
* @param delayInMs - The number of milliseconds to be delayed.
* @param abortSignal - The abortSignal associated with containing operation.
* @param abortErrorMsg - The abort error message associated with containing operation.
* @returns - Resolved promise
*/
export function delay(
delayInMs: number,
abortSignal?: AbortSignalLike,
abortErrorMsg?: string
): Promise<void> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
let onAborted: (() => void) | undefined = undefined;

const rejectOnAbort = (): void => {
return reject(new AbortError(abortErrorMsg ? abortErrorMsg : StandardAbortMessage));
};

const removeListeners = (): void => {
if (abortSignal && onAborted) {
abortSignal.removeEventListener("abort", onAborted);
}
};

onAborted = (): void => {
if (isDefined(timer)) {
clearTimeout(timer);
}
removeListeners();
return rejectOnAbort();
};

if (abortSignal && abortSignal.aborted) {
return rejectOnAbort();
}

timer = setTimeout(() => {
removeListeners();
resolve();
}, delayInMs);

if (abortSignal) {
abortSignal.addEventListener("abort", onAborted);
}
});
}

/**
* This policy is a close copy of the ThrottlingRetryPolicy class from
* core-http with modifications to work with how AppConfig is currently
Expand Down
1 change: 1 addition & 0 deletions sdk/core/core-http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Fixed

- 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)
- Throttling retry policy respects abort signal [#15796](https://github.com/Azure/azure-sdk-for-js/issues/15796)

## 1.2.6 (2021-06-14)

Expand Down
1 change: 1 addition & 0 deletions sdk/core/core-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
"karma-sourcemap-loader": "^0.3.8",
"mocha": "^7.1.1",
"mocha-junit-reporter": "^1.18.0",
"nock": "^12.0.3",
"npm-run-all": "^4.1.5",
"nyc": "^14.0.0",
"prettier": "^1.16.4",
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-http/review/core-http.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export class DefaultHttpClient extends FetchHttpClient {
}

// @public
export function delay<T>(t: number, value?: T): Promise<T | void>;
export function delay<T>(delayInMs: number, abortSignal?: AbortSignalLike, abortErrorMsg?: string, value?: T): Promise<T | void>;
Copy link
Contributor

Choose a reason for hiding this comment

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

value?: T should be the 2nd argument, otherwise this would be a breaking change.

Copy link
Member Author

@HarshaNalluru HarshaNalluru Jun 24, 2021

Choose a reason for hiding this comment

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

Good point.

I'll switch to that.
What about the core-amqp counterpart? How to get the copy of this method to stay consistent? Does that mean we can only bring consistency if we upgrade to a major version?

Copy link
Member

Choose a reason for hiding this comment

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

@HarshaNalluru I think there is no need to care about their consistency, we can just ignore them once core-util goes GA. Also, core-http is going to be phased out fairly soon anyway.


// @public
export interface DeserializationContentTypes {
Expand Down
3 changes: 1 addition & 2 deletions sdk/core/core-http/src/coreHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export {
export {
stripRequest,
stripResponse,
delay,
executePromisesSequentially,
generateUuid,
encodeUri,
Expand All @@ -113,7 +112,7 @@ export {
} from "./util/utils";
export { URLBuilder, URLQuery } from "./url";
export { AbortSignalLike } from "@azure/abort-controller";

export { delay } from "./util/delay";
// legacy exports. Use core-tracing instead (and remove on next major version update of core-http).
export { createSpanFunction, SpanConfig } from "./createSpanLegacy";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { Constants } from "../util/constants";
import { HttpOperationResponse } from "../httpOperationResponse";
import { WebResourceLike } from "../webResource";
import { delay } from "../util/utils";
import { delay } from "../util/delay";

// #region Access Token Cycler

Expand Down Expand Up @@ -71,7 +71,7 @@ async function beginRefresh(
): Promise<AccessToken> {
// This wrapper handles exceptions gracefully as long as we haven't exceeded
// the timeout.
async function tryGetAccessToken() {
async function tryGetAccessToken(): Promise<AccessToken | null> {
if (Date.now() < timeoutInMs) {
try {
return await getAccessToken();
Expand Down
4 changes: 2 additions & 2 deletions sdk/core/core-http/src/policies/exponentialRetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license.

import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
Expand All @@ -22,6 +21,7 @@ import {
} from "../util/exponentialBackoffStrategy";
import { RestError } from "../restError";
import { logger } from "../log";
import { delay } from "../util/delay";

export function exponentialRetryPolicy(
retryCount?: number,
Expand Down Expand Up @@ -164,7 +164,7 @@ async function retry(
if (!isAborted && shouldRetry(policy.retryCount, shouldPolicyRetry, retryData, response)) {
logger.info(`Retrying request in ${retryData.retryInterval}`);
try {
await utils.delay(retryData.retryInterval);
await delay(retryData.retryInterval);
const res = await policy._nextPolicy.sendRequest(request.clone());
return retry(policy, request, res, retryData);
} catch (err) {
Expand Down
8 changes: 4 additions & 4 deletions sdk/core/core-http/src/policies/rpRegistrationPolicy.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 { delay } from "../coreHttp";
import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
Expand Down Expand Up @@ -185,14 +186,13 @@ function getRegistrationStatus(
reqOptions.url = url;
reqOptions.method = "GET";

return policy._nextPolicy.sendRequest(reqOptions).then((res) => {
return policy._nextPolicy.sendRequest(reqOptions).then(async (res) => {
HarshaNalluru marked this conversation as resolved.
Show resolved Hide resolved
const obj = res.parsedBody as any;
if (res.parsedBody && obj.registrationState && obj.registrationState === "Registered") {
return true;
} else {
return utils
.delay(policy._retryTimeout * 1000)
.then(() => getRegistrationStatus(policy, url, originalRequest));
await delay(policy._retryTimeout * 1000);
return getRegistrationStatus(policy, url, originalRequest);
}
});
}
4 changes: 2 additions & 2 deletions sdk/core/core-http/src/policies/systemErrorRetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license.

import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
Expand All @@ -21,6 +20,7 @@ import {
DEFAULT_CLIENT_MIN_RETRY_INTERVAL,
isNumber
} from "../util/exponentialBackoffStrategy";
import { delay } from "../coreHttp";

export function systemErrorRetryPolicy(
retryCount?: number,
Expand Down Expand Up @@ -107,7 +107,7 @@ async function retry(
if (shouldRetry(policy.retryCount, shouldPolicyRetry, retryData, operationResponse, err)) {
// If previous operation ended with an error and the policy allows a retry, do that
try {
await utils.delay(retryData.retryInterval);
await delay(retryData.retryInterval);
return policy._nextPolicy.sendRequest(request.clone());
} catch (nestedErr) {
return retry(policy, request, operationResponse, nestedErr, retryData);
Expand Down
11 changes: 9 additions & 2 deletions sdk/core/core-http/src/policies/throttlingRetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
import { WebResourceLike } from "../webResource";
import { HttpOperationResponse } from "../httpOperationResponse";
import { Constants } from "../util/constants";
import { delay } from "../util/utils";
import { delay } from "../coreHttp";
import { AbortError } from "@azure/abort-controller";

type ResponseHandler = (
httpRequest: WebResourceLike,
Expand All @@ -26,6 +27,8 @@ export function throttlingRetryPolicy(): RequestPolicyFactory {
};
}

const StandardAbortMessage = "The operation was aborted.";

/**
* To learn more, please refer to
* https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-request-limits,
Expand Down Expand Up @@ -67,7 +70,11 @@ export class ThrottlingRetryPolicy extends BaseRequestPolicy {
retryAfterHeader
);
if (delayInMs) {
return delay(delayInMs).then((_: any) => this._nextPolicy.sendRequest(httpRequest));
await delay(delayInMs, httpRequest.abortSignal, StandardAbortMessage);
if (httpRequest.abortSignal?.aborted) {
throw new AbortError(StandardAbortMessage);
}
return this._nextPolicy.sendRequest(httpRequest);
}
}

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

import { isDefined } from "./typeguards";
import { AbortError, AbortSignalLike } from "@azure/abort-controller";
const StandardAbortMessage = "The operation was aborted.";

/**
* A wrapper for setTimeout that resolves a promise after delayInMs milliseconds.
* @param delayInMs - The number of milliseconds to be delayed.
* @param abortSignal - The abortSignal associated with containing operation.
* @param abortErrorMsg - The abort error message associated with containing operation.
* @param value - The value to be resolved with after a timeout of t milliseconds.
* @returns - Resolved promise
*/
export function delay<T>(
delayInMs: number,
abortSignal?: AbortSignalLike,
abortErrorMsg?: string,
value?: T
): Promise<T | void> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
let onAborted: (() => void) | undefined = undefined;

const rejectOnAbort = (): void => {
return reject(new AbortError(abortErrorMsg ? abortErrorMsg : StandardAbortMessage));
};

const removeListeners = (): void => {
if (abortSignal && onAborted) {
abortSignal.removeEventListener("abort", onAborted);
}
};

onAborted = (): void => {
Copy link
Member

Choose a reason for hiding this comment

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

why is this assigned? I don't see it getting assigned back to undefined can we just declare the method in scope?

Copy link
Member Author

Choose a reason for hiding this comment

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

I just copied this from core-amqp.

Can be done, will do.

Copy link
Member Author

Choose a reason for hiding this comment

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

image
Linter complains about this. Reverting.

if (isDefined(timer)) {
HarshaNalluru marked this conversation as resolved.
Show resolved Hide resolved
clearTimeout(timer);
}
removeListeners();
return rejectOnAbort();
};

if (abortSignal && abortSignal.aborted) {
return rejectOnAbort();
}

timer = setTimeout(() => {
removeListeners();
resolve(value);
}, delayInMs);

if (abortSignal) {
abortSignal.addEventListener("abort", onAborted);
}
});
}
11 changes: 11 additions & 0 deletions sdk/core/core-http/src/util/typeguards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Helper TypeGuard that checks if something is defined or not.
HarshaNalluru marked this conversation as resolved.
Show resolved Hide resolved
* @param thing - Anything
* @internal
*/
export function isDefined<T>(thing: T | undefined | null): thing is T {
return typeof thing !== "undefined" && thing !== null;
}
10 changes: 0 additions & 10 deletions sdk/core/core-http/src/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,6 @@ export function executePromisesSequentially(
return result;
}

/**
* A wrapper for setTimeout that resolves a promise after t milliseconds.
* @param t - The number of milliseconds to be delayed.
* @param value - The value to be resolved with after a timeout of t milliseconds.
* @returns Resolved promise
*/
export function delay<T>(t: number, value?: T): Promise<T | void> {
return new Promise((resolve) => setTimeout(() => resolve(value), t));
}

/**
* Service callback that is returned for REST requests initiated by the service client.
*/
Expand Down
52 changes: 52 additions & 0 deletions sdk/core/core-http/test/policies/throttlingRetryPolicy.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import nock from "nock";
jeremymeng marked this conversation as resolved.
Show resolved Hide resolved
import { Constants, ServiceClient } from "../../src/coreHttp";
import { AbortController } from "@azure/abort-controller";
import { assert } from "chai";

describe("Throttling retry policy", () => {
let client: ServiceClient;

beforeEach(function() {
if (!nock.isActive()) {
nock.activate();
}
nock("https://fakeservice.io:443")
.persist()
.put(/.*/g)
.reply(
Constants.HttpConstants.StatusCodes.TooManyRequests,
{
type: "https://fakeservice.io/errors/too-many-requests",
title: "Resource utilization has surpassed the assigned quota",
policy: "Total Requests",
status: Constants.HttpConstants.StatusCodes.TooManyRequests
},
["Retry-After", "10000"]
);
client = new ServiceClient();
});

afterEach(async function() {
nock.restore();
nock.cleanAll();
nock.enableNetConnect();
});

it("Should not retry forever (honors the abort signal passed)", async () => {
let errorWasThrown = false;
try {
await client.sendRequest({
url: "https://fakeservice.io/ABCD",
abortSignal: AbortController.timeout(100),
method: "PUT"
});
} catch (error) {
errorWasThrown = true;
assert.equal((error as any).name, "AbortError", "Unexpected error thrown");
}
assert.equal(errorWasThrown, true, "Error was not thrown");
});
});