-
Notifications
You must be signed in to change notification settings - Fork 19
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
Add cancellation support to all async operations #66
Changes from 10 commits
d53dab8
6978967
a01480c
2c9e833
111e48e
91920e7
1198fcd
2487cae
51c2055
4e077f2
a18c3b7
3d6248d
503399b
c12aeed
7e6c3db
da1f64b
48740af
5f0266e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ import { Sender, SenderOptions } from "./sender"; | |
import { Receiver, ReceiverOptions } from "./receiver"; | ||
import { Container } from "./container"; | ||
import { defaultOperationTimeoutInSeconds } from "./util/constants"; | ||
import { Func, EmitParameters, emitEvent } from "./util/utils"; | ||
import { Func, EmitParameters, emitEvent, AbortSignalLike, createAbortError } from "./util/utils"; | ||
import { | ||
ConnectionEvents, SessionEvents, SenderEvents, ReceiverEvents, create_connection, websocket_connect, | ||
ConnectionOptions as RheaConnectionOptions, Connection as RheaConnection, AmqpError, Dictionary, | ||
|
@@ -48,6 +48,36 @@ export interface ReceiverOptionsWithSession extends ReceiverOptions { | |
session?: Session; | ||
} | ||
|
||
/** | ||
* Set of options to use when running Connection.open() | ||
*/ | ||
export interface ConnectionOpenOptions { | ||
/** | ||
* A signal used to cancel the Connection.open() operation. | ||
*/ | ||
abortSignal?: AbortSignalLike; | ||
ramya-rao-a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Set of options to use when running Connection.close() | ||
*/ | ||
export interface ConnectionCloseOptions { | ||
/** | ||
* A signal used to cancel the Connection.close() operation. | ||
*/ | ||
abortSignal?: AbortSignalLike; | ||
} | ||
|
||
/** | ||
* Set of options to use when running Connection.createSession() | ||
*/ | ||
export interface SessionCreateOptions { | ||
/** | ||
* A signal used to cancel the Connection.createSession() operation. | ||
*/ | ||
abortSignal?: AbortSignalLike; | ||
} | ||
|
||
/** | ||
* Describes the options that can be provided while creating an AMQP connection. | ||
* @interface ConnectionOptions | ||
|
@@ -264,17 +294,20 @@ export class Connection extends Entity { | |
|
||
/** | ||
* Creates a new amqp connection. | ||
* @param options A set of options including a signal used to cancel the operation. | ||
* @return {Promise<Connection>} Promise<Connection> | ||
* - **Resolves** the promise with the Connection object when rhea emits the "connection_open" event. | ||
* - **Rejects** the promise with an AmqpError when rhea emits the "connection_close" event | ||
* while trying to establish an amqp connection. | ||
* while trying to establish an amqp connection or with an AbortError if the operation was cancelled. | ||
*/ | ||
open(): Promise<Connection> { | ||
open(options?: ConnectionOpenOptions): Promise<Connection> { | ||
return new Promise((resolve, reject) => { | ||
if (!this.isOpen()) { | ||
|
||
let onOpen: Func<RheaEventContext, void>; | ||
let onClose: Func<RheaEventContext, void>; | ||
let onAbort: Func<void, void>; | ||
const abortSignal = options?.abortSignal; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose this must work or else CI wouldn't run but the package.json says we're using TS 3.5. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's because 3.5.1 is the minimum version 😄 But that does remind me that before a new version is published, we should evaluate if down-leveled types are needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right! :) It's a dev dependency anyways - so should we bump it up? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed optional chaining in c12aeed to avoid rocking the boat regarding TS versions |
||
let waitTimer: any; | ||
|
||
const removeListeners: Function = () => { | ||
|
@@ -283,6 +316,7 @@ export class Connection extends Entity { | |
this._connection.removeListener(ConnectionEvents.connectionOpen, onOpen); | ||
this._connection.removeListener(ConnectionEvents.connectionClose, onClose); | ||
this._connection.removeListener(ConnectionEvents.disconnected, onClose); | ||
abortSignal?.removeEventListener("abort", onAbort); | ||
}; | ||
|
||
onOpen = (context: RheaEventContext) => { | ||
|
@@ -299,6 +333,14 @@ export class Connection extends Entity { | |
return reject(err); | ||
}; | ||
|
||
onAbort = () => { | ||
removeListeners(); | ||
this._connection.close(); | ||
chradek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const err = createAbortError("Connection open request has been cancelled."); | ||
log.error("[%s] [%s]", this.id, err.message); | ||
return reject(err); | ||
}; | ||
|
||
const actionAfterTimeout = () => { | ||
removeListeners(); | ||
const msg: string = `Unable to open the amqp connection "${this.id}" due to operation timeout.`; | ||
|
@@ -314,6 +356,14 @@ export class Connection extends Entity { | |
log.connection("[%s] Trying to create a new amqp connection.", this.id); | ||
this._connection.connect(); | ||
this.actionInitiated++; | ||
|
||
if (abortSignal) { | ||
ramya-rao-a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (abortSignal.aborted) { | ||
onAbort(); | ||
} else { | ||
abortSignal.addEventListener("abort", onAbort); | ||
} | ||
} | ||
} else { | ||
return resolve(this); | ||
} | ||
|
@@ -323,25 +373,33 @@ export class Connection extends Entity { | |
|
||
/** | ||
* Closes the amqp connection. | ||
* @param options A set of options including a signal used to cancel the operation. | ||
* When the abort signal in the options is fired, the local endpoint is closed. | ||
* This does not guarantee that the remote has closed as well. It only stops listening for | ||
* an acknowledgement that the remote endpoint is closed as well. | ||
* @return {Promise<void>} Promise<void> | ||
* - **Resolves** the promise when rhea emits the "connection_close" event. | ||
* - **Rejects** the promise with an AmqpError when rhea emits the "connection_error" event while | ||
* trying to close an amqp connection. | ||
* trying to close an amqp connection or with an AbortError if the operation was cancelled. | ||
*/ | ||
close(): Promise<void> { | ||
close(options?: ConnectionCloseOptions): Promise<void> { | ||
return new Promise<void>((resolve, reject) => { | ||
log.error("[%s] The connection is open ? -> %s", this.id, this.isOpen()); | ||
if (this.isOpen()) { | ||
let onClose: Func<RheaEventContext, void>; | ||
let onError: Func<RheaEventContext, void>; | ||
let onDisconnected: Func<RheaEventContext, void>; | ||
let onAbort: Func<void, void>; | ||
const abortSignal = options?.abortSignal; | ||
let waitTimer: any; | ||
|
||
const removeListeners = () => { | ||
clearTimeout(waitTimer); | ||
this.actionInitiated--; | ||
this._connection.removeListener(ConnectionEvents.connectionError, onError); | ||
this._connection.removeListener(ConnectionEvents.connectionClose, onClose); | ||
this._connection.removeListener(ConnectionEvents.disconnected, onDisconnected); | ||
abortSignal?.removeEventListener("abort", onAbort); | ||
}; | ||
|
||
onClose = (context: RheaEventContext) => { | ||
|
@@ -366,6 +424,13 @@ export class Connection extends Entity { | |
log.error("[%s] Connection got disconnected while closing itself: %O.", this.id, error); | ||
}; | ||
|
||
onAbort = () => { | ||
removeListeners(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious here as well what happens if the server does close the connection with an error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the way rhea is set up is to fire events and not throw errors. If there are listeners, they respond, else the fired event gets ignored. There is no unhandled exceptions |
||
const err = createAbortError("Connection close request has been cancelled."); | ||
log.error("[%s] [%s]", this.id, err.message); | ||
return reject(err); | ||
}; | ||
|
||
const actionAfterTimeout = () => { | ||
removeListeners(); | ||
const msg: string = `Unable to close the amqp connection "${this.id}" due to operation timeout.`; | ||
|
@@ -380,6 +445,14 @@ export class Connection extends Entity { | |
waitTimer = setTimeout(actionAfterTimeout, this.options!.operationTimeoutInSeconds! * 1000); | ||
this._connection.close(); | ||
this.actionInitiated++; | ||
|
||
if (abortSignal) { | ||
if (abortSignal.aborted) { | ||
onAbort(); | ||
} else { | ||
abortSignal.addEventListener("abort", onAbort); | ||
} | ||
} | ||
} else { | ||
return resolve(); | ||
} | ||
|
@@ -452,19 +525,22 @@ export class Connection extends Entity { | |
|
||
/** | ||
* Creates an amqp session on the provided amqp connection. | ||
* @param options A set of options including a signal used to cancel the operation. | ||
* @return {Promise<Session>} Promise<Session> | ||
* - **Resolves** the promise with the Session object when rhea emits the "session_open" event. | ||
* - **Rejects** the promise with an AmqpError when rhea emits the "session_close" event while | ||
* trying to create an amqp session. | ||
* trying to create an amqp session or with an AbortError if the operation was cancelled. | ||
*/ | ||
createSession(): Promise<Session> { | ||
createSession(options?: SessionCreateOptions): Promise<Session> { | ||
return new Promise((resolve, reject) => { | ||
const rheaSession = this._connection.create_session(); | ||
const session = new Session(this, rheaSession); | ||
session.actionInitiated++; | ||
let onOpen: Func<RheaEventContext, void>; | ||
let onClose: Func<RheaEventContext, void>; | ||
let onDisconnected: Func<RheaEventContext, void>; | ||
let onAbort: Func<void, void>; | ||
const abortSignal = options?.abortSignal; | ||
let waitTimer: any; | ||
|
||
const removeListeners = () => { | ||
|
@@ -473,6 +549,7 @@ export class Connection extends Entity { | |
rheaSession.removeListener(SessionEvents.sessionOpen, onOpen); | ||
rheaSession.removeListener(SessionEvents.sessionClose, onClose); | ||
rheaSession.connection.removeListener(ConnectionEvents.disconnected, onDisconnected); | ||
abortSignal?.removeEventListener("abort", onAbort); | ||
}; | ||
|
||
onOpen = (context: RheaEventContext) => { | ||
|
@@ -498,6 +575,14 @@ export class Connection extends Entity { | |
return reject(error); | ||
}; | ||
|
||
onAbort = () => { | ||
removeListeners(); | ||
rheaSession.close(); | ||
richardpark-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const err = createAbortError("Create session request has been cancelled."); | ||
log.error("[%s] [%s]", this.id, err.message); | ||
return reject(err); | ||
}; | ||
|
||
const actionAfterTimeout = () => { | ||
removeListeners(); | ||
const msg: string = `Unable to create the amqp session due to operation timeout.`; | ||
|
@@ -512,19 +597,30 @@ export class Connection extends Entity { | |
log.session("[%s] Calling amqp session.begin().", this.id); | ||
waitTimer = setTimeout(actionAfterTimeout, this.options!.operationTimeoutInSeconds! * 1000); | ||
rheaSession.begin(); | ||
|
||
if (abortSignal) { | ||
if (abortSignal.aborted) { | ||
onAbort(); | ||
} else { | ||
abortSignal.addEventListener("abort", onAbort); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Creates an amqp sender link. It either uses the provided session or creates a new one. | ||
* - **Resolves** the promise with the Sender object when rhea emits the "sender_open" event. | ||
* - **Rejects** the promise with an AmqpError when rhea emits the "sender_close" event while | ||
* trying to create an amqp session or with an AbortError if the operation was cancelled. | ||
* @param {SenderOptionsWithSession} options Optional parameters to create a sender link. | ||
* @return {Promise<Sender>} Promise<Sender>. | ||
*/ | ||
async createSender(options?: SenderOptionsWithSession): Promise<Sender> { | ||
async createSender(options?: SenderOptionsWithSession & { abortSignal?: AbortSignalLike; }): Promise<Sender> { | ||
if (options && options.session && options.session.createSender) { | ||
return options.session.createSender(options); | ||
} | ||
const session = await this.createSession(); | ||
const session = await this.createSession({ abortSignal: options?.abortSignal }); | ||
return session.createSender(options); | ||
} | ||
|
||
|
@@ -540,24 +636,27 @@ export class Connection extends Entity { | |
* | ||
* @return Promise<AwaitableSender>. | ||
*/ | ||
async createAwaitableSender(options?: AwaitableSenderOptionsWithSession): Promise<AwaitableSender> { | ||
async createAwaitableSender(options?: AwaitableSenderOptionsWithSession & { abortSignal?: AbortSignalLike; }): Promise<AwaitableSender> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So, the right place to add the abortSignal would have been But then, |
||
if (options && options.session && options.session.createAwaitableSender) { | ||
return options.session.createAwaitableSender(options); | ||
} | ||
const session = await this.createSession(); | ||
const session = await this.createSession({ abortSignal: options?.abortSignal }); | ||
return session.createAwaitableSender(options); | ||
} | ||
|
||
/** | ||
* Creates an amqp receiver link. It either uses the provided session or creates a new one. | ||
* - **Resolves** the promise with the Sender object when rhea emits the "receiver_open" event. | ||
* - **Rejects** the promise with an AmqpError when rhea emits the "receiver_close" event while | ||
* trying to create an amqp session or with an AbortError if the operation was cancelled. | ||
* @param {ReceiverOptionsWithSession} options Optional parameters to create a receiver link. | ||
* @return {Promise<Receiver>} Promise<Receiver>. | ||
*/ | ||
async createReceiver(options?: ReceiverOptionsWithSession): Promise<Receiver> { | ||
async createReceiver(options?: ReceiverOptionsWithSession & { abortSignal?: AbortSignalLike; }): Promise<Receiver> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (options && options.session && options.session.createReceiver) { | ||
return options.session.createReceiver(options); | ||
} | ||
const session = await this.createSession(); | ||
const session = await this.createSession({ abortSignal: options?.abortSignal }); | ||
return session.createReceiver(options); | ||
} | ||
|
||
|
@@ -572,17 +671,17 @@ export class Connection extends Entity { | |
* @return {Promise<ReqResLink>} Promise<ReqResLink> | ||
*/ | ||
async createRequestResponseLink(senderOptions: SenderOptions, receiverOptions: ReceiverOptions, | ||
providedSession?: Session): Promise<ReqResLink> { | ||
providedSession?: Session, abortSignal?: AbortSignal): Promise<ReqResLink> { | ||
if (!senderOptions) { | ||
throw new Error(`Please provide sender options.`); | ||
} | ||
if (!receiverOptions) { | ||
throw new Error(`Please provide receiver options.`); | ||
} | ||
const session = providedSession || await this.createSession(); | ||
const session = providedSession || await this.createSession({ abortSignal }); | ||
const [sender, receiver] = await Promise.all([ | ||
session.createSender(senderOptions), | ||
session.createReceiver(receiverOptions) | ||
session.createSender({ ...senderOptions, abortSignal }), | ||
session.createReceiver({ ...receiverOptions, abortSignal }) | ||
]); | ||
log.connection("[%s] Successfully created the sender '%s' and receiver '%s' on the same " + | ||
"amqp session '%s'.", this.id, sender.name, receiver.name, session.id); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put this into a function rather than inlining it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you thinking of a function that accepts
delivery
as an argument, and then still in-lining, something like: