UNPKG

@azure/service-bus

Version:
315 lines • 15.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { getAlreadyReceivingErrorMsg, getReceiverClosedErrorMsg, InvalidMaxMessageCountError, throwErrorIfConnectionClosed, throwTypeErrorIfParameterMissing, throwTypeErrorIfParameterNotLong, throwErrorIfInvalidOperationOnMessage, throwTypeErrorIfParameterTypeMismatch, } from "../util/errors"; import { StreamingReceiver } from "../core/streamingReceiver"; import { BatchingReceiver } from "../core/batchingReceiver"; import { abandonMessage, assertValidMessageHandlers, completeMessage, deadLetterMessage, deferMessage, getMessageIterator, } from "./receiverCommon"; import { Constants, RetryOperationType, retry } from "@azure/core-amqp"; import { LockRenewer } from "../core/autoLockRenewer"; import { receiverLogger as logger } from "../log"; import { translateServiceBusError } from "../serviceBusError"; import { ensureValidIdentifier } from "../util/utils"; import { toSpanOptions, tracingClient } from "../diagnostics/tracing"; import { extractSpanContextFromServiceBusMessage } from "../diagnostics/instrumentServiceBusMessage"; /** * The default time to wait for messages _after_ the first message * has been received. * * This timeout only applies to receiveMessages() * * @internal */ export const defaultMaxTimeAfterFirstMessageForBatchingMs = 1000; /** * @internal */ export class ServiceBusReceiverImpl { get logPrefix() { return `[${this._context.connectionId}|receiver:${this.entityPath}]`; } /** * @throws Error if the underlying connection is closed. */ constructor(_context, entityPath, receiveMode, maxAutoRenewLockDurationInMs, skipParsingBodyAsJson, skipConvertingDate = false, retryOptions = {}, identifier) { this._context = _context; this.entityPath = entityPath; this.receiveMode = receiveMode; this.skipParsingBodyAsJson = skipParsingBodyAsJson; this.skipConvertingDate = skipConvertingDate; /** * Denotes if close() was called on this receiver */ this._isClosed = false; throwErrorIfConnectionClosed(_context); this._retryOptions = retryOptions; this._lockRenewer = LockRenewer.create(this._context, maxAutoRenewLockDurationInMs, receiveMode); this.identifier = ensureValidIdentifier(this.entityPath, identifier); } _throwIfAlreadyReceiving() { if (this._isReceivingMessages()) { const errorMessage = getAlreadyReceivingErrorMsg(this.entityPath); const error = new Error(errorMessage); logger.logError(error, `${this.logPrefix} is already receiving`); throw error; } } _throwIfReceiverOrConnectionClosed() { throwErrorIfConnectionClosed(this._context); if (this.isClosed) { const errorMessage = getReceiverClosedErrorMsg(this.entityPath); const error = new Error(errorMessage); logger.logError(error, `${this.logPrefix} is closed`); throw error; } } get isClosed() { return this._isClosed || this._context.wasConnectionCloseCalled; } 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 receiveMessages = async () => { if (!this._batchingReceiver || !this._context.messageReceivers[this._batchingReceiver.name]) { const receiveOptions = { maxConcurrentCalls: 0, receiveMode: this.receiveMode, lockRenewer: this._lockRenewer, skipParsingBodyAsJson: this.skipParsingBodyAsJson, skipConvertingDate: this.skipConvertingDate, }; this._batchingReceiver = this._createBatchingReceiver(this._context, this.entityPath, receiveOptions); } const receivedMessages = await this._batchingReceiver.receive(maxMessageCount, options?.maxWaitTimeInMs ?? Constants.defaultOperationTimeoutInMs, defaultMaxTimeAfterFirstMessageForBatchingMs, options ?? {}); return receivedMessages; }; const config = { connectionHost: this._context.config.host, connectionId: this._context.connectionId, operation: receiveMessages, operationType: RetryOperationType.receiveMessage, abortSignal: options?.abortSignal, retryOptions: this._retryOptions, }; return retry(config).catch((err) => { throw translateServiceBusError(err); }); } getMessageIterator(options) { return getMessageIterator(this, options); } 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, undefined, { ...options, associatedLinkName: this._getAssociatedReceiverName(), 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); } // ManagementClient methods # Begin async peekMessages(maxMessageCount, options = {}) { this._throwIfReceiverOrConnectionClosed(); const managementRequestOptions = { ...options, associatedLinkName: this._getAssociatedReceiverName(), 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, undefined, options.omitMessageBody, managementRequestOptions); } else { return this._context .getManagementClient(this.entityPath) .peek(maxMessageCount, options.omitMessageBody, managementRequestOptions); } }; const config = { operation: peekOperationPromise, connectionId: this._context.connectionId, operationType: RetryOperationType.management, retryOptions: this._retryOptions, abortSignal: options?.abortSignal, }; return retry(config); } subscribe(handlers, options) { assertValidMessageHandlers(handlers); throwErrorIfConnectionClosed(this._context); this._throwIfReceiverOrConnectionClosed(); this._throwIfAlreadyReceiving(); options = { ...(options ?? {}), autoCompleteMessages: options?.autoCompleteMessages ?? true, }; // When the user "stops" a streaming receiver (via the returned instance from 'subscribe' we just suspend // it, leaving the link open). This allows users to stop the flow of messages but still be able to settle messages // since the link itself hasn't been shut down. // // Users can, if they want, restart their subscription (since we've got a link already established). // So you'll have an instance here if the user has done: // 1. const subscription = receiver.subscribe() // 2. subscription.stop() // 3. receiver.subscribe() this._streamingReceiver = this._streamingReceiver ?? new StreamingReceiver(this.identifier, this._context, this.entityPath, { ...options, receiveMode: this.receiveMode, retryOptions: this._retryOptions, lockRenewer: this._lockRenewer, skipParsingBodyAsJson: this.skipParsingBodyAsJson, skipConvertingDate: this.skipConvertingDate, }); // this ensures that if the outer service bus client is closed that this receiver is cleaned up. // this mostly affects us if we're in the middle of init() - the connection (and receiver) are not yet // open but we do need to close the receiver to exit the init() loop. this._context.messageReceivers[this._streamingReceiver.name] = this._streamingReceiver; this._streamingReceiver.subscribe(handlers, options).catch((_) => { // (the error will already have been reported to the user) if (this._streamingReceiver) { delete this._context.messageReceivers[this._streamingReceiver.name]; } }); return { close: async () => { return this._streamingReceiver?.stopReceivingMessages(); }, }; } 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(message) { this._throwIfReceiverOrConnectionClosed(); throwErrorIfInvalidOperationOnMessage(message, this.receiveMode, this._context.connectionId); const tracingContext = extractSpanContextFromServiceBusMessage(message); const spanLinks = tracingContext ? [{ tracingContext }] : []; return tracingClient.withSpan("ServiceBusReceiver.renewMessageLock", {}, () => { const msgImpl = message; let associatedLinkName; if (msgImpl.delivery.link) { const associatedReceiver = this._context.getReceiverFromCache(msgImpl.delivery.link.name); associatedLinkName = associatedReceiver?.name; } return this._context .getManagementClient(this.entityPath) .renewLock(message.lockToken, { associatedLinkName }) .then((lockedUntil) => { message.lockedUntilUtc = lockedUntil; return lockedUntil; }); }, { spanLinks, ...toSpanOptions({ entityPath: this.entityPath, host: this._context.config.host }, "receive", "client"), }); } async close() { try { this._isClosed = true; if (this._context.connection && this._context.connection.isOpen()) { // Close the streaming receiver. if (this._streamingReceiver) { await this._streamingReceiver.close(); } // Close the batching receiver. if (this._batchingReceiver) { await this._batchingReceiver.close(); } } } catch (err) { logger.logError(err, `${this.logPrefix} An error occurred while closing the Receiver`); throw err; } } /** * Indicates whether the receiver is currently receiving messages or not. * When this returns true, new `registerMessageHandler()` or `receiveMessages()` calls cannot be made. */ _isReceivingMessages() { if (this._streamingReceiver && this._streamingReceiver.isOpen() && this._streamingReceiver.isSubscribeActive) { return true; } if (this._batchingReceiver && this._batchingReceiver.isOpen() && this._batchingReceiver.isReceivingMessages) { return true; } return false; } _createBatchingReceiver(context, entityPath, options) { const receiver = BatchingReceiver.create(this.identifier, context, entityPath, options); logger.verbose(`[${this.logPrefix}] receiver '${receiver.name}' created, with maxConcurrentCalls set to ${options.maxConcurrentCalls}.`); return receiver; } /** * Helper function to retrieve any active receiver name, regardless of streaming or * batching if it exists. This is used for optimization on the service side */ _getAssociatedReceiverName() { if (this._streamingReceiver && this._streamingReceiver.isOpen()) { return this._streamingReceiver.name; } if (this._batchingReceiver && this._batchingReceiver.isOpen() && this._batchingReceiver.isReceivingMessages) { return this._batchingReceiver.name; } return; } } //# sourceMappingURL=receiver.js.map