Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
260b0e6
[Event Hubs] Introduce timeoutInMs on RetryOptions (#4239)
ramya0820 Jul 11, 2019
c74f5e4
Merge branch 'master' of https://github.com/ramya0820/azure-sdk-for-js
Jul 13, 2019
00016cb
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-js
Jul 15, 2019
5876468
Bring over RequestResponse link creation and management from core-amqp
Jul 15, 2019
2f10541
Update _makeManagementRequest()
Jul 16, 2019
934130d
Simplify promise handling
Jul 16, 2019
a974e22
Address comments
Jul 16, 2019
41c5f08
Fix operationType
Jul 16, 2019
5b6d1e1
Update sendRequest() contract
Jul 16, 2019
80624c4
Revert whitespaces
Jul 16, 2019
ef7d712
Address comments
Jul 18, 2019
2ebcc2f
Rearrange code for clarity
Jul 18, 2019
5dec336
Update tests
Jul 18, 2019
ebd6e13
Dummy commit
Jul 19, 2019
4e31c97
Update
Jul 19, 2019
776680c
Update tests
Jul 19, 2019
4a088bc
Update tests
Jul 19, 2019
1617865
Revert "Rearrange code for clarity"
Jul 19, 2019
c64168c
Address comments
Jul 19, 2019
57f4f01
Address comments
Jul 19, 2019
71570d9
Merge branch 'issue-2835-p3' of https://github.com/ramya0820/azure-sd…
Jul 19, 2019
492fe0f
Revert timer clearance
Jul 19, 2019
c8a1887
Improve variable names
Jul 19, 2019
3fb5286
Use getRetryTimeoutInMs util
Jul 19, 2019
e16757e
Test tests [Tests not run yet]
Jul 19, 2019
e8e5cc6
Remove retry test from req-res tests
Jul 19, 2019
cc15d72
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-js in…
Jul 22, 2019
868fafd
Allow abort during init
Jul 22, 2019
df19eae
Remove unused imports
Jul 22, 2019
d997887
Add test for error surfacing
Jul 23, 2019
f304522
Merge branch 'master' into issue-2835-p3
ramya0820 Jul 23, 2019
fb13acd
Remove abort event listener
Jul 23, 2019
c149776
Address comments
Jul 23, 2019
7e1036a
Address comments
Jul 23, 2019
2910d8b
Rearrange test
Jul 23, 2019
885ac8f
Update sdk/eventhub/event-hubs/src/managementClient.ts
ramya0820 Jul 23, 2019
a636a13
Address comments
Jul 23, 2019
590ece6
Improve test
Jul 23, 2019
9008fb2
Fix error message
Jul 23, 2019
e9232ee
Address comments
Jul 23, 2019
b7a78a1
Fix link receiver references
Jul 24, 2019
0ba8305
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-js in…
Jul 24, 2019
c13de88
Remove abort on init
Jul 24, 2019
76aea7b
Address comments
Jul 25, 2019
cd11d1c
Fix typo
Jul 25, 2019
a5988d0
Remove unnecessary check
Jul 25, 2019
6ab619e
Remove unnecessary check
Jul 25, 2019
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: 4 additions & 0 deletions sdk/core/core-amqp/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.0.0-preview.2.0 - 16th July, 2019

- `sendRequest()` function in the `RequestResponseLink` now excludes default retries and leaves it up to the users to implement it as necessary.
Comment thread
ramya-rao-a marked this conversation as resolved.

## 1.0.0-preview.1.0 - 28th June, 2019

This library is based off of the [@azure/amqp-common](https://www.npmjs.com/package/@azure/amqp-common)
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-amqp/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@azure/core-amqp",
"sdk-type": "client",
"version": "1.0.0-preview.1",
"version": "1.0.0-preview.2",
"description": "Common library for amqp based azure sdks like @azure/event-hubs.",
"author": "Microsoft Corporation",
"license": "MIT",
Expand Down
138 changes: 56 additions & 82 deletions sdk/core/core-amqp/src/requestResponseLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { AbortSignalLike, AbortError } from "@azure/abort-controller";
import * as Constants from "./util/constants";
import { retry, RetryConfig, RetryOperationType } from "./retry";
import {
Session,
Connection,
Expand Down Expand Up @@ -32,19 +31,9 @@ export interface SendRequestOptions {
abortSignal?: AbortSignalLike;
/**
* @property {number} [timeoutInSeconds] Max time to wait for the operation to complete.
* Default: `10 seconds`.
* Default: `60 seconds`.
*/
timeoutInSeconds?: number;
/**
* @property {number} [maxRetries] Number of times the operation needs to be retried in case
* of error. Default: 3.
*/
maxRetries?: number;
/**
* @property {number} [delayInSeconds] Amount of time to wait in seconds before making the
* next attempt. Default: 15.
*/
delayInSeconds?: number;
/**
* @property {string} [requestName] Name of the request being performed.
*/
Expand Down Expand Up @@ -86,9 +75,7 @@ export class RequestResponseLink implements ReqResLink {

/**
* Sends the given request message and returns the received response. If the operation is not
* completed in the provided timeout in seconds `default: 10`, then the request will be retried
* linearly for the provided number of times `default: 3` with the provided delay in seconds
* `default: 15` between each attempt.
* completed in the provided timeout in seconds `default: 60`, then `OperationTimeoutError` is thrown.
*
* @param {Message} request The AMQP (request) message.
* @param {SendRequestOptions} [options] Options that can be provided while sending a request.
Expand All @@ -98,65 +85,38 @@ export class RequestResponseLink implements ReqResLink {
if (!options) options = {};

if (!options.timeoutInSeconds) {
options.timeoutInSeconds = 10;
options.timeoutInSeconds = Constants.defaultOperationTimeoutInSeconds;
}

let count: number = 0;
const aborter: AbortSignalLike | undefined = options && options.abortSignal;

const sendRequestPromise = () =>
new Promise<AmqpMessage>((resolve: any, reject: any) => {
let waitTimer: any;
let timeOver: boolean = false;
type NormalizedInfo = {
statusCode: number;
statusDescription: string;
errorCondition: string;
};

count++;
if (count !== 1) {
// Generate a new message_id every time after the first attempt
request.message_id = generate_uuid();
} else if (!request.message_id) {
// Set the message_id in the first attempt only if it is not set
request.message_id = generate_uuid();
}

const rejectOnAbort = () => {
const address = this.receiver.address || "address";
const requestName = options!.requestName;
const desc: string =
`[${this.connection.id}] The request "${requestName}" ` +
`to "${address}" has been cancelled by the user.`;
log.error(desc);
const error = new AbortError(
`The ${requestName ? requestName + " " : ""}operation has been cancelled by the user.`
);

reject(error);
};

const onAbort = () => {
// remove the event listener as this will be registered next time someone makes a request.
const actionAfterTimeout = () => {
timeOver = true;
this.receiver.removeListener(ReceiverEvents.message, messageCallback);
// safe to clear the timeout if it hasn't already occurred.
if (!timeOver) {
clearTimeout(waitTimer);
if (aborter) {
aborter.removeEventListener("abort", onAbort);
}
aborter!.removeEventListener("abort", onAbort);

rejectOnAbort();
Comment thread
ramya-rao-a marked this conversation as resolved.
const address = this.receiver.address || "address";
const desc: string =
`The request with message_id "${request.message_id}" to "${address}" ` +
`endpoint timed out. Please try again later.`;
const e: Error = {
name: "OperationTimeoutError",
message: desc
};
return reject(translate(e));
};

if (aborter) {
// the aborter may have been triggered between request attempts
// so check if it was triggered and reject if needed.
if (aborter.aborted) {
return rejectOnAbort();
}
aborter.addEventListener("abort", onAbort);
}
const waitTimer = setTimeout(actionAfterTimeout, options!.timeoutInSeconds! * 1000);

// Handle different variations of property names in responses emitted by EventHubs and ServiceBus.
const getCodeDescriptionAndError = (props: any): NormalizedInfo => {
Expand Down Expand Up @@ -222,25 +182,48 @@ export class RequestResponseLink implements ReqResLink {
}
};

const actionAfterTimeout = () => {
timeOver = true;
this.receiver.removeListener(ReceiverEvents.message, messageCallback);
if (aborter) {
aborter.removeEventListener("abort", onAbort);
}
const rejectOnAbort = () => {
const address = this.receiver.address || "address";
const requestName = options!.requestName;
const desc: string =
`The request with message_id "${request.message_id}" to "${address}" ` +
`endpoint timed out. Please try again later.`;
const e: AmqpError = {
condition: ConditionStatusMapper[408],
description: desc
};
return reject(translate(e));
`[${this.connection.id}] The request "${requestName}" ` +
`to "${address}" has been cancelled by the user.`;
log.error(desc);
const error = new AbortError(
`The ${requestName ? requestName + " " : ""}operation has been cancelled by the user.`
);

reject(error);
};

const onAbort = () => {
// remove the event listener as this will be registered next time someone makes a request.
this.receiver.removeListener(ReceiverEvents.message, messageCallback);
// safe to clear the timeout if it hasn't already occurred.
if (!timeOver) {
clearTimeout(waitTimer);
}
aborter!.removeEventListener("abort", onAbort);

rejectOnAbort();
};

if (aborter) {
// the aborter may have been triggered between request attempts
// so check if it was triggered and reject if needed.
if (aborter.aborted) {
return rejectOnAbort();
}
aborter.addEventListener("abort", onAbort);
}

if (!request.message_id) {
// Set the message_id only if it is not set
request.message_id = generate_uuid();
}

this.receiver.on(ReceiverEvents.message, messageCallback);
waitTimer = setTimeout(actionAfterTimeout, options!.timeoutInSeconds! * 1000);

log.reqres(
"[%s] %s request sent: %O",
this.connection.id,
Expand All @@ -249,17 +232,8 @@ export class RequestResponseLink implements ReqResLink {
);
this.sender.send(request);
});
const config: RetryConfig<AmqpMessage> = {
operation: sendRequestPromise,
connectionId: this.connection.id,
operationType:
request.to && request.to === Constants.cbsEndpoint
? RetryOperationType.cbsAuth
: RetryOperationType.management,
delayInSeconds: options.delayInSeconds,
maxRetries: options.maxRetries
};
return retry<AmqpMessage>(config);

return sendRequestPromise();
Comment thread
ramya-rao-a marked this conversation as resolved.
Outdated
}

/**
Expand Down
2 changes: 0 additions & 2 deletions sdk/core/core-amqp/test/requestResponse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ describe("RequestResponseLink", function() {
});
}, 4000);
const response = await link.sendRequest(request, {
delayInSeconds: 1,
timeoutInSeconds: 5
});
assert.equal(response.correlation_id, messageId);
Expand Down Expand Up @@ -240,7 +239,6 @@ describe("RequestResponseLink", function() {
const signal = controller.signal;
setTimeout(controller.abort.bind(controller), 100);
Comment thread
ramya-rao-a marked this conversation as resolved.
await link.sendRequest(request, {
delayInSeconds: 1,
timeoutInSeconds: 5,
abortSignal: signal // cancel between request attempts
});
Expand Down
2 changes: 1 addition & 1 deletion sdk/eventhub/event-hubs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
},
"dependencies": {
"@azure/abort-controller": "1.0.0-preview.1",
"@azure/core-amqp": "^1.0.0-preview.1",
"@azure/core-amqp": "^1.0.0-preview.2",
"@azure/core-asynciterator-polyfill": "^1.0.0-preview.1",
"async-lock": "^1.1.3",
"debug": "^3.1.0",
Expand Down
101 changes: 80 additions & 21 deletions sdk/eventhub/event-hubs/src/managementClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
defaultLock,
translate,
Constants,
SendRequestOptions
SendRequestOptions,
retry,
RetryConfig,
RetryOperationType,
randomNumberFromInterval
} from "@azure/core-amqp";
import {
Message,
Expand Down Expand Up @@ -306,31 +310,86 @@ export class ManagementClient extends LinkEntity {
options?: { retryOptions?: RetryOptions; abortSignal?: AbortSignalLike; requestName?: string }
): Promise<any> {
try {
log.mgmt(
"[%s] Acquiring lock to get the management req res link.",
this._context.connectionId
);
await defaultLock.acquire(this.managementLock, () => {
return this._init();
});

if (!options) {
options = {};
}

const sendRequestOptions: SendRequestOptions = {
maxRetries: options.retryOptions && options.retryOptions.maxRetries,
abortSignal: options.abortSignal,
requestName: options.requestName,
timeoutInSeconds: getRetryAttemptTimeoutInMs(options.retryOptions) / 1000,
delayInSeconds:
options.retryOptions &&
options.retryOptions.retryInterval &&
options.retryOptions.retryInterval >= 0
? options.retryOptions.retryInterval / 1000
: undefined
const sendOperationPromise = () =>
new Promise<Message>(async (resolve, reject) => {
try {
const actionAfterTimeout = () => {
const address = this._mgmtReqResLink!.receiver.address || "address";
const desc: string =
`The request with message_id "${request.message_id}" to "${address}" ` +
`endpoint timed out. Please try again later.`;
const e: Error = {
name: "OperationTimeoutError",
message: desc
};
return reject(translate(e));
};

const waitTimer = setTimeout(
actionAfterTimeout,
getRetryAttemptTimeoutInMs(options!.retryOptions)
);

const initOperationStartTime = Date.now();
log.mgmt(
"[%s] Acquiring lock to get the management req res link.",
this._context.connectionId
);
await defaultLock.acquire(this.managementLock, () => {
Comment thread
ramya-rao-a marked this conversation as resolved.
Outdated
return this._init().catch((err: Error) => {
clearTimeout(waitTimer);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should also check if the operation was cancelled after calling _init. If it has, we know we can close the connection and reject.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This second check is unnecessary since we already have one at the start of sendRequest

Discussed offline about how abortSignal will need to be passed in to the init and other internal operations to gracefully handle cancellation during init as well.
Clearing connection, links, listeners and timers will need to be done as part of this.
Since this needs to be done for receive() as well and would involve more work, changes for it and tests can be added as part of new PR and be linked to separate issue #4422

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This second check is unnecessary since we already have one at the start of sendRequest

I agree

reject(translate(err));
});
});
const initOperationEndTime = Date.now();

Comment thread
ramya-rao-a marked this conversation as resolved.
if (!options) {
options = {};
}

const operationTimeoutInMs = initOperationEndTime - initOperationStartTime;
Comment thread
ramya-rao-a marked this conversation as resolved.
Outdated
const sendRequestOptions: SendRequestOptions = {
abortSignal: options.abortSignal,
requestName: options.requestName,
timeoutInSeconds: operationTimeoutInMs / 1000
};

const result = await this._mgmtReqResLink!.sendRequest(request, sendRequestOptions);
resolve(result);
} catch (err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

init() call has its own try/catch
This catch corresponds to the outer try block i.e the block that covers both init() and this._mgmtReqResLink!.sendRequest()
Any reason to have an outer try/catch block this way?

I would imagine that init() and this._mgmtReqResLink!.sendRequest() would have their own try/catch blocks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It allows us to catch errors from other code paths in case any gets thrown and indicate in logs with a message about source of it.

@ramya-rao-a Ramya Rao (ramya-rao-a) Jul 29, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The 2 points from where an error can be expected to be thrown (by code that this file doesnt control) are the init() call and the this._mgmtReqResLink!.sendRequest() call. Having try/catch around the 2 calls separately is enough.

err = translate(err);
const address = this._mgmtReqResLink!.sender.address || "address";
Comment thread
ramya0820 marked this conversation as resolved.
Outdated
log.error(
"[%s] An error occurred during send on management request-response link '%s' with address " +
"'%s': %O",
this._context.connectionId,
this._mgmtReqResLink!.name,
address,
err
);
reject(err);
}
});

const maxRetries = options.retryOptions && options.retryOptions.maxRetries;
const delayInSeconds =
options.retryOptions &&
options.retryOptions.retryInterval &&
options.retryOptions.retryInterval >= 0
? options.retryOptions.retryInterval / 1000
: Constants.defaultDelayBetweenOperationRetriesInSeconds;
const config: RetryConfig<void> = {
operation: sendOperationPromise,
connectionId: this._context.connectionId,
operationType: RetryOperationType.management,
maxRetries: maxRetries,
Comment thread
ramya-rao-a marked this conversation as resolved.
delayInSeconds: delayInSeconds
};
return (await this._mgmtReqResLink!.sendRequest(request, sendRequestOptions)).body;
return (await retry<Message>(config)).body;
} catch (err) {
err = translate(err);
log.error("An error occurred while making the request to $management endpoint: %O", err);
Expand Down