UNPKG

@azure/service-bus

Version:
355 lines • 21.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { connectionLogger as logger } from "./log"; import { packageJsonInfo } from "./util/constants"; import { ConnectionContextBase, } from "@azure/core-amqp"; import { ConnectionEvents, } from "rhea-promise"; import { ManagementClient } from "./core/managementClient"; import { formatUserAgentPrefix } from "./util/utils"; import { getRuntimeInfo } from "./util/runtimeInfo"; import { ServiceBusError } from "./serviceBusError"; /** * @internal * Helper method to call onDetached on the non-sessions batching and streaming receivers from the connection context upon seeing an error. */ async function callOnDetachedOnReceivers(connectionContext, contextOrConnectionError, receiverType) { const detachCalls = []; // Iterating over non-sessions batching and streaming receivers for (const receiverName of Object.keys(connectionContext.messageReceivers)) { const receiver = connectionContext.messageReceivers[receiverName]; if (receiver && receiver.receiverType === receiverType) { logger.verbose("[%s] calling detached on %s receiver '%s'.", connectionContext.connection.id, receiver.receiverType, receiver.name); detachCalls.push(receiver.onDetached(contextOrConnectionError).catch((err) => { logger.logError(err, "[%s] An error occurred while calling onDetached() on the %s receiver '%s'", connectionContext.connection.id, receiver.receiverType, receiver.name); })); } } return Promise.all(detachCalls); } /** * @internal * Helper method to call onDetached on the session receivers from the connection context upon seeing an error. */ async function callOnDetachedOnSessionReceivers(connectionContext, contextOrConnectionError) { const getSessionError = (sessionId, entityPath) => { const sessionInfo = `The receiver for session "${sessionId}" in "${entityPath}" has been closed and can no longer be used. ` + `Please create a new receiver using the "acceptSession" or "acceptNextSession" method on the ServiceBusClient.`; const errorMessage = contextOrConnectionError == null ? `Unknown error occurred on the AMQP connection while receiving messages. ` + sessionInfo : `Error occurred on the AMQP connection while receiving messages. ` + sessionInfo + `\nMore info - \n${contextOrConnectionError}`; const error = new ServiceBusError(errorMessage, "SessionLockLost"); error.retryable = false; return error; }; const detachCalls = []; for (const receiverName of Object.keys(connectionContext.messageSessions)) { const receiver = connectionContext.messageSessions[receiverName]; logger.verbose("[%s] calling detached on %s receiver(sessions).", connectionContext.connection.id, receiver.name); detachCalls.push(receiver.onDetached(getSessionError(receiver.sessionId, receiver.entityPath)).catch((err) => { logger.logError(err, "[%s] An error occurred while calling onDetached() on the session receiver(sessions) '%s'", connectionContext.connection.id, receiver.name); })); } return Promise.all(detachCalls); } /** * @internal * Helper method to get the number of receivers of specified type from the connectionContext. */ function getNumberOfReceivers(connectionContext, receiverType) { if (receiverType === "session") { const receivers = connectionContext.messageSessions; return Object.keys(receivers).length; } const receivers = connectionContext.messageReceivers; const receiverNames = Object.keys(receivers); const count = receiverNames.reduce((acc, name) => (receivers[name].receiverType === receiverType ? ++acc : acc), 0); return count; } /** * @internal */ // eslint-disable-next-line @typescript-eslint/no-namespace export var ConnectionContext; (function (ConnectionContext) { function create(config, tokenCredential, options) { if (!options) options = {}; const userAgent = `${formatUserAgentPrefix(options.userAgentOptions?.userAgentPrefix)} ${getRuntimeInfo()}`; const parameters = { config: config, // re-enabling this will be a post-GA discussion similar to event-hubs. // dataTransformer: options.dataTransformer, isEntityPathRequired: false, connectionProperties: { product: "MSJSClient", userAgent, version: packageJsonInfo.version, }, }; // Let us create the base context and then add ServiceBus specific ConnectionContext properties. const connectionContext = ConnectionContextBase.create(parameters); connectionContext.tokenCredential = tokenCredential; connectionContext.senders = {}; connectionContext.messageReceivers = {}; connectionContext.messageSessions = {}; connectionContext.managementClients = {}; let waitForConnectionRefreshResolve; let waitForConnectionRefreshPromise; Object.assign(connectionContext, { isConnectionClosing() { // When the connection is not open, but the remote end is open, // then the rhea connection is in the process of terminating. return Boolean(!this.connection.isOpen() && this.connection.isRemoteOpen()); }, async readyToOpenLink() { logger.verbose(`[${this.connectionId}] Waiting until the connection is ready to open link.`); // Check that the connection isn't in the process of closing. // This can happen when the idle timeout has been reached but // the underlying socket is waiting to be destroyed. if (this.isConnectionClosing()) { logger.verbose(`[${this.connectionId}] Connection is closing, waiting for disconnected event`); // Wait for the disconnected event that indicates the underlying socket has closed. await this.waitForDisconnectedEvent(); } // Wait for the connection to be reset. await this.waitForConnectionReset(); logger.verbose(`[${this.connectionId}] Connection is ready to open link.`); }, waitForDisconnectedEvent() { return new Promise((resolve) => { logger.verbose(`[${this.connectionId}] Attempting to reinitialize connection` + ` but the connection is in the process of closing.` + ` Waiting for the disconnect event before continuing.`); this.connection.once(ConnectionEvents.disconnected, resolve); }); }, waitForConnectionReset() { // Check if the connection is currently in the process of disconnecting. if (waitForConnectionRefreshPromise) { logger.verbose(`[${this.connectionId}] Waiting for connection reset`); return waitForConnectionRefreshPromise; } logger.verbose(`[${this.connectionId}] Connection not waiting to be reset. Resolving immediately.`); return Promise.resolve(); }, getReceiverFromCache(receiverName, sessionId) { if (sessionId != null && this.messageSessions[receiverName]) { return this.messageSessions[receiverName]; } if (this.messageReceivers[receiverName]) { return this.messageReceivers[receiverName]; } let existingReceivers = ""; if (sessionId != null) { for (const messageSessionName of Object.keys(this.messageSessions)) { if (this.messageSessions[messageSessionName].sessionId === sessionId) { existingReceivers = this.messageSessions[messageSessionName].name; break; } } } else { existingReceivers += (existingReceivers ? ", " : "") + Object.keys(this.messageReceivers).join(","); } logger.verbose("[%s] Failed to find receiver '%s' among existing receivers: %s", this.connectionId, receiverName, existingReceivers); return; }, getManagementClient(entityPath) { if (!this.managementClients[entityPath]) { this.managementClients[entityPath] = new ManagementClient(this, entityPath, { address: `${entityPath}/$management`, }); } return this.managementClients[entityPath]; }, }); // Define listeners to be added to the connection object for // "connection_open" and "connection_error" events. const onConnectionOpen = () => { connectionContext.wasConnectionCloseCalled = false; logger.verbose("[%s] setting 'wasConnectionCloseCalled' property of connection context to %s.", connectionContext.connection.id, connectionContext.wasConnectionCloseCalled); }; const disconnected = async (context) => { if (waitForConnectionRefreshPromise) { return; } waitForConnectionRefreshPromise = new Promise((resolve) => { waitForConnectionRefreshResolve = resolve; }); const connectionError = context.connection && context.connection.error ? context.connection.error : undefined; if (connectionError) { logger.logError(connectionError, "[%s] Error (context.connection.error) occurred on the amqp connection", connectionContext.connection.id); } const contextError = context.error; if (contextError) { logger.logError(contextError, "[%s] Error (context.error) occurred on the amqp connection", connectionContext.connection.id); } const state = { wasConnectionCloseCalled: connectionContext.wasConnectionCloseCalled, numSenders: Object.keys(connectionContext.senders).length, numReceivers: Object.keys(connectionContext.messageReceivers).length + Object.keys(connectionContext.messageSessions).length, }; // Clear internal map maintained by rhea to avoid reconnecting of old links once the // connection is back up. connectionContext.connection.removeAllSessions(); // Close the cbs session to ensure all the event handlers are released. await connectionContext.cbsSession.close(); // Close the management sessions to ensure all the event handlers are released. for (const entityPath of Object.keys(connectionContext.managementClients)) { await connectionContext.managementClients[entityPath].close(); } if (state.wasConnectionCloseCalled) { // Do Nothing } else { // Calling onDetached on sender if (state.numSenders) { // We don't do recovery for the sender: // Because we don't want to keep the sender active all the time // and the "next" send call would bear the burden of creating the link. // Call onDetached() on sender so that it can gracefully shutdown // by cleaning up the timers and closing the links. // We don't call onDetached for sender after `refreshConnection()` // because any new send calls that potentially initialize links would also get affected if called later. logger.verbose(`[${connectionContext.connection.id}] connection.close() was not called from the sdk and there were ${state.numSenders} ` + `senders. We should not reconnect.`); const detachCalls = []; for (const senderName of Object.keys(connectionContext.senders)) { const sender = connectionContext.senders[senderName]; if (sender) { logger.verbose("[%s] calling detached on sender '%s'.", connectionContext.connection.id, sender.name); detachCalls.push(sender.onDetached().catch((err) => { logger.logError(err, "[%s] An error occurred while calling onDetached() the sender '%s'", connectionContext.connection.id, sender.name); })); } } await Promise.all(detachCalls); } // Calling onDetached on batching receivers for the same reasons as sender const numBatchingReceivers = getNumberOfReceivers(connectionContext, "batching"); if (numBatchingReceivers) { logger.verbose(`[${connectionContext.connection.id}] connection.close() was not called from the sdk and there were ${numBatchingReceivers} ` + `batching receivers. We should not reconnect.`); // Call onDetached() on receivers so that batching receivers it can gracefully close any ongoing batch operation await callOnDetachedOnReceivers(connectionContext, connectionError || contextError, "batching"); } // Calling onDetached on session receivers const numSessionReceivers = getNumberOfReceivers(connectionContext, "session"); if (numSessionReceivers) { logger.verbose(`[${connectionContext.connection.id}] connection.close() was not called from the sdk and there were ${numSessionReceivers} ` + `session receivers. We should close them.`); await callOnDetachedOnSessionReceivers(connectionContext, connectionError || contextError); } } await refreshConnection(); waitForConnectionRefreshResolve(); waitForConnectionRefreshPromise = undefined; // The connection should always be brought back up if the sdk did not call connection.close() // and there was at least one receiver link on the connection before it went down. logger.verbose("[%s] state: %O", connectionContext.connectionId, state); // Calling onDetached on streaming receivers const numStreamingReceivers = getNumberOfReceivers(connectionContext, "streaming"); if (!state.wasConnectionCloseCalled && numStreamingReceivers) { logger.verbose(`[${connectionContext.connection.id}] connection.close() was not called from the sdk and there were ${numStreamingReceivers} ` + `streaming receivers. We should reconnect.`); // Calling `onDetached()` on streaming receivers after the refreshConnection() since `onDetached()` would // recover the streaming receivers and that would only be possible after the connection is refreshed. // // This is different from the batching receiver since `onDetached()` for the batching receiver would // return the outstanding messages and close the receive link. await callOnDetachedOnReceivers(connectionContext, connectionError || contextError, "streaming"); } }; const protocolError = async (context) => { if (context.connection && context.connection.error) { logger.logError(context.connection.error, "[%s] Error (context.connection.error) occurred on the amqp connection", connectionContext.connection.id); } if (context.error) { logger.logError(context.error, "[%s] Error (context.error) occurred on the amqp connection", connectionContext.connection.id); } }; const error = async (context) => { if (context.connection && context.connection.error) { logger.logError(context.connection.error, "[%s] Error (context.connection.error) occurred on the amqp connection", connectionContext.connection.id); } if (context.error) { logger.logError(context.error, "[%s] Error (context.error) occurred on the amqp connection", connectionContext.connection.id); } }; async function refreshConnection() { const originalConnectionId = connectionContext.connectionId; try { await cleanConnectionContext(); } catch (err) { logger.logError(err, `[${connectionContext.connectionId}] There was an error closing the connection before reconnecting`); } // Create a new connection, id, locks, and cbs client. connectionContext.refreshConnection(); addConnectionListeners(connectionContext.connection); logger.verbose(`The connection "${originalConnectionId}" has been updated to "${connectionContext.connectionId}".`); } function addConnectionListeners(connection) { // Add listeners on the connection object. connection.on(ConnectionEvents.connectionOpen, onConnectionOpen); connection.on(ConnectionEvents.disconnected, disconnected); connection.on(ConnectionEvents.protocolError, protocolError); connection.on(ConnectionEvents.error, error); } async function cleanConnectionContext() { // Remove listeners from the connection object. connectionContext.connection.removeListener(ConnectionEvents.connectionOpen, onConnectionOpen); connectionContext.connection.removeListener(ConnectionEvents.disconnected, disconnected); connectionContext.connection.removeListener(ConnectionEvents.protocolError, protocolError); connectionContext.connection.removeListener(ConnectionEvents.error, error); // Close the connection await connectionContext.connection.close(); } addConnectionListeners(connectionContext.connection); logger.verbose("[%s] Created connection context successfully.", connectionContext.connectionId); return connectionContext; } ConnectionContext.create = create; /** * Closes the AMQP connection created by this ServiceBusClient along with AMQP links for * sender/receivers created by the queue/topic/subscription clients created by this * ServiceBusClient. * Once closed, * - the clients created by this ServiceBusClient cannot be used to send/receive messages anymore. * - this ServiceBusClient cannot be used to create any new queues/topics/subscriptions clients. */ async function close(context) { const logPrefix = `[${context.connectionId}]`; try { logger.verbose(`${logPrefix} Permanently closing the amqp connection on the client.`); const senderNames = Object.keys(context.senders); const messageReceiverNames = Object.keys(context.messageReceivers); const messageSessionNames = Object.keys(context.messageSessions); const managementClientsEntityPaths = Object.keys(context.managementClients); logger.verbose(`${logPrefix} Permanently closing all the senders(${senderNames.length}), MessageReceivers(${messageReceiverNames.length}), MessageSessions(${messageSessionNames.length}), and ManagementClients(${managementClientsEntityPaths.length}).`); await Promise.all([ ...senderNames.map((n) => context.senders[n].close()), ...messageReceiverNames.map((n) => context.messageReceivers[n].close()), ...messageSessionNames.map((n) => context.messageSessions[n].close()), ...managementClientsEntityPaths.map((p) => context.managementClients[p].close()), ]); logger.verbose(`${logPrefix} Permanently closing cbsSession`); await context.cbsSession.close(); logger.verbose(`${logPrefix} Permanently closing internal connection`); await context.connection.close(); context.wasConnectionCloseCalled = true; logger.verbose(`[${logPrefix} Permanently closed the amqp connection on the client.`); } catch (err) { const errObj = err instanceof Error ? err : new Error(JSON.stringify(err)); logger.logError(err, `${logPrefix} An error occurred while closing the connection`); throw errObj; } } ConnectionContext.close = close; })(ConnectionContext || (ConnectionContext = {})); //# sourceMappingURL=connectionContext.js.map