UNPKG

@azure/service-bus

Version:
379 lines • 19 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { getAlreadyReceivingErrorMsg, getReceiverClosedErrorMsg, InvalidMaxMessageCountError, throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing, throwTypeErrorIfParameterNotLong, throwErrorIfInvalidOperationOnMessage, throwTypeErrorIfParameterTypeMismatch, } from "../util/errors"; import { abandonMessage, assertValidMessageHandlers, completeMessage, deadLetterMessage, deferMessage, getMessageIterator, wrapProcessErrorHandler, } from "./receiverCommon"; import { defaultMaxTimeAfterFirstMessageForBatchingMs } from "./receiver"; import { Constants, RetryOperationType, retry, ErrorNameConditionMapper, } from "@azure/core-amqp"; import { toProcessingSpanOptions } from "../diagnostics/instrumentServiceBusMessage"; import { tracingClient } from "../diagnostics/tracing"; import { receiverLogger as logger } from "../log"; import { translateServiceBusError } from "../serviceBusError"; /** * @internal */ export class ServiceBusSessionReceiverImpl { get logPrefix() { return `[${this._context.connectionId}|session:${this.entityPath}]`; } /** * @internal * @throws Error if the underlying connection is closed. * @throws Error if an open receiver is already existing for given sessionId. */ constructor(_messageSession, _context, entityPath, receiveMode, _skipParsingBodyAsJson, _skipConvertingDate, _retryOptions = {}) { this._messageSession = _messageSession; this._context = _context; this.entityPath = entityPath; this.receiveMode = receiveMode; this._skipParsingBodyAsJson = _skipParsingBodyAsJson; this._skipConvertingDate = _skipConvertingDate; this._retryOptions = _retryOptions; /** * Denotes if close() was called on this receiver */ this._isClosed = false; throwErrorIfConnectionClosed(_context); this.sessionId = _messageSession.sessionId; this.identifier = _messageSession.identifier; } _throwIfReceiverOrConnectionClosed() { throwErrorIfConnectionClosed(this._context); if (this.isClosed) { if (this._isClosed) { const errorMessage = getReceiverClosedErrorMsg(this.entityPath, this.sessionId); const error = new Error(errorMessage); logger.logError(error, `${this.logPrefix} already closed`); throw error; } const amqpError = { condition: ErrorNameConditionMapper.SessionLockLostError, description: `The session lock has expired on the session with id ${this.sessionId}`, }; throw translateServiceBusError(amqpError); } } _throwIfAlreadyReceiving() { if (this._isReceivingMessages()) { const errorMessage = getAlreadyReceivingErrorMsg(this.entityPath, this.sessionId); const error = new Error(errorMessage); logger.logError(error, `${this.logPrefix} is already receiving.`); throw error; } } get isClosed() { return (this._isClosed || !this._context.messageSessions[this._messageSession.name] || !this._messageSession.isOpen()); } /** * The time in UTC until which the session is locked. * Every time `renewSessionLock()` is called, this time gets updated to current time plus the lock * duration as specified during the Queue/Subscription creation. * * When the lock on the session expires * - The current receiver can no longer be used to receive more messages. * Create a new receiver using `ServiceBusClient.acceptSession()` or `ServiceBusClient.acceptNextSession()`. * - Messages that were received in `peekLock` mode with this receiver but not yet settled * will land back in the Queue/Subscription with their delivery count incremented. * * @readonly */ get sessionLockedUntilUtc() { return this._messageSession.sessionLockedUntilUtc; } /** * Renews the lock on the session for the duration as specified during the Queue/Subscription * creation. You can check the `sessionLockedUntilUtc` property for the time when the lock expires. * * When the lock on the session expires * - The current receiver can no longer be used to receive mode messages. * Create a new receiver using `ServiceBusClient.acceptSession()` or `ServiceBusClient.acceptNextSession()`. * - Messages that were received in `peekLock` mode with this receiver but not yet settled * will land back in the Queue/Subscription with their delivery count incremented. * * @param options - Options bag to pass an abort signal or tracing options. * @returns New lock token expiry date and time in UTC format. * @throws Error if the underlying connection or receiver is closed. * @throws `ServiceBusError` if the service returns an error while renewing session lock. */ async renewSessionLock(options) { this._throwIfReceiverOrConnectionClosed(); return tracingClient.withSpan("ServiceBusSessionReceiver.renewSessionLock", options ?? {}, (updatedOptions) => { const renewSessionLockOperationPromise = async () => { this._messageSession.sessionLockedUntilUtc = await this._context .getManagementClient(this.entityPath) .renewSessionLock(this.sessionId, { ...updatedOptions, associatedLinkName: this._messageSession.name, requestName: "renewSessionLock", timeoutInMs: this._retryOptions.timeoutInMs, }); return this._messageSession.sessionLockedUntilUtc; }; const config = { operation: renewSessionLockOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.management, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config); }); } /** * Sets the state on the Session. For more on session states, see * {@link https://docs.microsoft.com/azure/service-bus-messaging/message-sessions#message-session-state | Session State} * @param state - The state that needs to be set. * @param options - Options bag to pass an abort signal or tracing options. * @throws Error if the underlying connection or receiver is closed. * @throws `ServiceBusError` if the service returns an error while setting the session state. */ async setSessionState(state, options = {}) { this._throwIfReceiverOrConnectionClosed(); return tracingClient.withSpan("ServiceBusSessionReceiver.setSessionState", options ?? {}, (updatedOptions) => { const setSessionStateOperationPromise = async () => { await this._context .getManagementClient(this.entityPath) .setSessionState(this.sessionId, state, { ...updatedOptions, associatedLinkName: this._messageSession.name, requestName: "setState", timeoutInMs: this._retryOptions.timeoutInMs, }); return; }; const config = { operation: setSessionStateOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.management, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config); }); } /** * Gets the state of the Session. For more on session states, see * {@link https://docs.microsoft.com/azure/service-bus-messaging/message-sessions#message-session-state | Session State} * @param options - Options bag to pass an abort signal or tracing options. * @returns The state of that session * @throws Error if the underlying connection or receiver is closed. * @throws `ServiceBusError` if the service returns an error while retrieving session state. */ async getSessionState(options = {}) { this._throwIfReceiverOrConnectionClosed(); return tracingClient.withSpan("ServiceBusSessionReceiver.getSessionState", options ?? {}, (updatedOptions) => { const getSessionStateOperationPromise = async () => { return this._context .getManagementClient(this.entityPath) .getSessionState(this.sessionId, { ...updatedOptions, associatedLinkName: this._messageSession.name, requestName: "getState", timeoutInMs: this._retryOptions.timeoutInMs, }); }; const config = { operation: getSessionStateOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.management, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config); }); } async peekMessages(maxMessageCount, options = {}) { this._throwIfReceiverOrConnectionClosed(); const managementRequestOptions = { ...options, associatedLinkName: this._messageSession.name, requestName: "peekMessages", timeoutInMs: this._retryOptions?.timeoutInMs, skipParsingBodyAsJson: this._skipParsingBodyAsJson, skipConvertingDate: this._skipConvertingDate, }; const peekOperationPromise = async () => { if (options.fromSequenceNumber !== undefined) { return this._context .getManagementClient(this.entityPath) .peekBySequenceNumber(options.fromSequenceNumber, maxMessageCount, this.sessionId, options.omitMessageBody, managementRequestOptions); } else { return this._context .getManagementClient(this.entityPath) .peekMessagesBySession(this.sessionId, maxMessageCount, options.omitMessageBody, managementRequestOptions); } }; const config = { operation: peekOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.management, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config); } async receiveDeferredMessages(sequenceNumbers, options = {}) { this._throwIfReceiverOrConnectionClosed(); throwTypeErrorIfParameterMissing(this._context.connectionId, "sequenceNumbers", sequenceNumbers); throwTypeErrorIfParameterNotLong(this._context.connectionId, "sequenceNumbers", sequenceNumbers); const deferredSequenceNumbers = Array.isArray(sequenceNumbers) ? sequenceNumbers : [sequenceNumbers]; const receiveDeferredMessagesOperationPromise = async () => { const deferredMessages = await this._context .getManagementClient(this.entityPath) .receiveDeferredMessages(deferredSequenceNumbers, this.receiveMode, this.sessionId, { ...options, associatedLinkName: this._messageSession.name, requestName: "receiveDeferredMessages", timeoutInMs: this._retryOptions.timeoutInMs, skipParsingBodyAsJson: this._skipParsingBodyAsJson, skipConvertingDate: this._skipConvertingDate, }); return deferredMessages; }; const config = { operation: receiveDeferredMessagesOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.management, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config); } async receiveMessages(maxMessageCount, options) { this._throwIfReceiverOrConnectionClosed(); this._throwIfAlreadyReceiving(); throwTypeErrorIfParameterMissing(this._context.connectionId, "maxMessageCount", maxMessageCount); throwTypeErrorIfParameterTypeMismatch(this._context.connectionId, "maxMessageCount", maxMessageCount, "number"); if (isNaN(maxMessageCount) || maxMessageCount < 1) { throw new TypeError(InvalidMaxMessageCountError); } const receiveBatchOperationPromise = async () => { const receivedMessages = await this._messageSession.receiveMessages(maxMessageCount, options?.maxWaitTimeInMs ?? Constants.defaultOperationTimeoutInMs, defaultMaxTimeAfterFirstMessageForBatchingMs, options ?? {}); return receivedMessages; }; const config = { operation: receiveBatchOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.receiveMessage, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config).catch((err) => { throw translateServiceBusError(err); }); } subscribe(handlers, options) { // TODO - receiverOptions for subscribe?? assertValidMessageHandlers(handlers); options = options ?? {}; const processError = wrapProcessErrorHandler(handlers); this._registerMessageHandler(async (message) => { return tracingClient.withSpan("SessionReceiver.process", options ?? {}, () => handlers.processMessage(message), toProcessingSpanOptions(message, this, this._context.config, "process")); }, processError, options); return { close: async () => { return this._messageSession?.receiverHelper.suspend(); }, }; } /** * Registers handlers to deal with the incoming stream of messages over an AMQP receiver link * from a Queue/Subscription. * To stop receiving messages, call `close()` on the SessionReceiver. * * Throws an error if there is another receive operation in progress on the same receiver. If you * are not sure whether there is another receive operation running, check the `isReceivingMessages` * property on the receiver. * * @param onMessage - Handler for processing each incoming message. * @param onError - Handler for any error that occurs while receiving or processing messages. * @param options - Options to control whether messages should be automatically completed * or if the lock on the session should be automatically renewed. You can control the * maximum number of messages that should be concurrently processed. You can * also provide a timeout in milliseconds to denote the amount of time to wait for a new message * before closing the receiver. * * @throws Error if the underlying connection or receiver is closed. * @throws Error if the receiver is already in state of receiving messages. * @throws `ServiceBusError` if the service returns an error while receiving messages. These are bubbled up to be handled by user provided `onError` handler. */ _registerMessageHandler(onMessage, onError, options) { this._throwIfReceiverOrConnectionClosed(); this._throwIfAlreadyReceiving(); const connId = this._context.connectionId; throwTypeErrorIfParameterMissing(connId, "onMessage", onMessage); throwTypeErrorIfParameterMissing(connId, "onError", onError); if (typeof onMessage !== "function") { throw new TypeError("The parameter 'onMessage' must be of type 'function'."); } if (typeof onError !== "function") { throw new TypeError("The parameter 'onError' must be of type 'function'."); } try { this._messageSession.subscribe(onMessage, onError, options); } catch (err) { onError({ error: err, errorSource: "receive", entityPath: this.entityPath, fullyQualifiedNamespace: this._context.config.host, identifier: this.identifier, }); } } getMessageIterator(options) { return getMessageIterator(this, options); } async completeMessage(message) { this._throwIfReceiverOrConnectionClosed(); throwErrorIfInvalidOperationOnMessage(message, this.receiveMode, this._context.connectionId); const msgImpl = message; return completeMessage(msgImpl, this._context, this.entityPath, this._retryOptions); } async abandonMessage(message, propertiesToModify) { this._throwIfReceiverOrConnectionClosed(); throwErrorIfInvalidOperationOnMessage(message, this.receiveMode, this._context.connectionId); const msgImpl = message; return abandonMessage(msgImpl, this._context, this.entityPath, propertiesToModify, this._retryOptions); } async deferMessage(message, propertiesToModify) { this._throwIfReceiverOrConnectionClosed(); throwErrorIfInvalidOperationOnMessage(message, this.receiveMode, this._context.connectionId); const msgImpl = message; return deferMessage(msgImpl, this._context, this.entityPath, propertiesToModify, this._retryOptions); } async deadLetterMessage(message, options) { this._throwIfReceiverOrConnectionClosed(); throwErrorIfInvalidOperationOnMessage(message, this.receiveMode, this._context.connectionId); const msgImpl = message; return deadLetterMessage(msgImpl, this._context, this.entityPath, options, this._retryOptions); } async renewMessageLock() { throw new Error("Renewing message lock is an invalid operation when working with sessions."); } async close() { try { await this._messageSession.close(); } catch (err) { logger.logError(err, "%s An error occurred while closing the SessionReceiver for session %s", this.logPrefix, this.sessionId); throw err; } finally { this._isClosed = true; } } /** * Indicates whether the receiver is currently receiving messages or not. * When this returns true, new `registerMessageHandler()` or `receiveMessages()` calls cannot be made. */ _isReceivingMessages() { return this._messageSession ? this._messageSession.isReceivingMessages : false; } } //# sourceMappingURL=sessionReceiver.js.map