UNPKG

@azure/event-hubs

Version:
613 lines (612 loc) • 23.8 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var eventHubSender_exports = {}; __export(eventHubSender_exports, { EventHubSender: () => EventHubSender, generateIdempotentLinkProperties: () => generateIdempotentLinkProperties, transformEventsForSend: () => transformEventsForSend }); module.exports = __toCommonJS(eventHubSender_exports); var import_rhea_promise = require("rhea-promise"); var import_core_amqp = require("@azure/core-amqp"); var import_eventData = require("./eventData.js"); var import_eventDataBatch = require("./eventDataBatch.js"); var import_logger = require("./logger.js"); var import_retries = require("./util/retries.js"); var import_constants = require("./util/constants.js"); var import_core_util = require("@azure/core-util"); var import_error = require("./util/error.js"); var import_withAuth = require("./withAuth.js"); var import_utils = require("./util/utils.js"); class EventHubSender { /** * The unique lock name per connection that is used to acquire the * lock for establishing a sender link by an entity on that connection. */ senderLock = (0, import_utils.getRandomName)("sender"); /** * The handler function to handle errors that happen on the * underlying sender. */ _onAmqpError; /** * The handler function to handle "sender_close" event * that happens on the underlying sender. */ _onAmqpClose; /** * The message handler that will be set as the handler on * the underlying rhea sender's session for the "session_error" event. */ _onSessionError; /** * The message handler that will be set as the handler on * the underlying rhea sender's session for the "session_close" event. */ _onSessionClose; /** * The AMQP sender link. */ _sender; /** * The partition ID. */ partitionId; /** * Indicates whether the sender is configured for idempotent publishing. */ _isIdempotentProducer; /** * Indicates whether the sender has an in-flight send while idempotent * publishing is enabled. */ _hasPendingSend; /** * A local copy of the PartitionPublishingProperties that can be mutated to * keep track of the lastSequenceNumber used. */ _localPublishingProperties; /** * The user-provided set of options that can be specified to influence * publishing behavior specific to a partition. */ _userProvidedPublishingOptions; /** * Indicates whether the link is in the process of connecting * (establishing) itself. Default value: `false`. */ isConnecting = false; /** * The unique name for the entity (mostly a guid). */ name; /** * The address in the following form: * - `"<hubName>"` * - `"<hubName>/Partitions/<partitionId>"`. */ address; /** * The token audience in the following form: * - `"sb://<yournamespace>.servicebus.windows.net/<hubName>"` * - `"sb://<yournamespace>.servicebus.windows.net/<hubName>/Partitions/<partitionId>"`. */ audience; /** * Provides relevant information about the amqp connection, * cbs and $management sessions, token provider, sender and receivers. */ _context; /** * The auth loop. */ authLoop; /** * The logger. */ logger; /** The client identifier */ _id; /** * Creates a new EventHubSender instance. * @param context - The connection context. * @param options - Options used to configure the EventHubSender. */ constructor(context, senderId, { partitionId, enableIdempotentProducer, partitionPublishingOptions }) { this.address = context.config.getSenderAddress(partitionId); this.name = this.address; this._id = senderId; this.audience = context.config.getSenderAudience(partitionId); this._context = context; this.partitionId = partitionId; this._isIdempotentProducer = enableIdempotentProducer; this._userProvidedPublishingOptions = partitionPublishingOptions; const logPrefix = (0, import_logger.createSenderLogPrefix)(this.name, this._context.connectionId); this.logger = (0, import_logger.createSimpleLogger)(import_logger.logger, logPrefix); this._onAmqpError = (eventContext) => { const senderError = eventContext.sender && eventContext.sender.error; this.logger.verbose( "'sender_error' event occurred. The associated error is: %O", senderError ); }; this._onSessionError = (eventContext) => { const sessionError = eventContext.session && eventContext.session.error; this.logger.verbose( "'session_error' event occurred. The associated error is: %O", sessionError ); }; this._onAmqpClose = (eventContext) => { const sender = this._sender || eventContext.sender; this.logger.verbose( "'sender_close' event occurred. Value for isItselfClosed on the receiver is: '%s' Value for isConnecting on the session is: '%s'.", sender?.isItselfClosed().toString(), this.isConnecting ); if (sender && !this.isConnecting) { sender.close().catch((err) => { this.logger.verbose("error when closing after 'sender_close' event: %O", err); }); } }; this._onSessionClose = (eventContext) => { const sender = this._sender || eventContext.sender; this.logger.verbose( "'session_close' event occurred. Value for isSessionItselfClosed on the session is: '%s' Value for isConnecting on the session is: '%s'.", sender?.isSessionItselfClosed().toString(), this.isConnecting ); if (sender && !this.isConnecting) { sender.close().catch((err) => { this.logger.verbose("error when closing after 'session_close' event: %O", err); }); } }; } /** * Deletes the sender from the context. Clears the token renewal timer. Closes the sender link. */ async close() { try { if (this._sender) { this.logger.info("closing"); const senderLink = this._sender; this._deleteFromCache(); await senderLink.close(); this.authLoop?.stop(); this.logger.verbose("closed."); } } catch (err) { const msg = `an error occurred while closing: ${err?.name}: ${err?.message}`; this.logger.warning(msg); (0, import_logger.logErrorStackTrace)(err); throw err; } } /** * Determines whether the AMQP sender link is open. If open then returns true else returns false. * @returns boolean */ isOpen() { const result = Boolean(this._sender && this._sender.isOpen()); this.logger.verbose("is open? -> %s", result); return result; } /** * Returns maximum message size on the AMQP sender link. * @param abortSignal - An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. * For example, use the &commat;azure/abort-controller to create an `AbortSignal`. * @returns Promise<number> * @throws AbortError if the operation is cancelled via the abortSignal. */ async getMaxMessageSize(options = {}) { const sender = await this._getLink(options); return sender.maxMessageSize; } /** * Get the information about the state of publishing for a partition as observed by the `EventHubSender`. * This data can always be read, but will only be populated with information relevant to the active features * for the producer client. */ async getPartitionPublishingProperties(options = {}) { if (this._localPublishingProperties) { return { ...this._localPublishingProperties }; } const properties = { isIdempotentPublishingEnabled: this._isIdempotentProducer, partitionId: this.partitionId ?? "" }; if (this._isIdempotentProducer) { this._sender = await this._getLink(options); if (!this._sender) { throw new Error( `Failed to retrieve partition publishing properties for partition "${this.partitionId}".` ); } const { [import_constants.idempotentProducerAmqpPropertyNames.epoch]: ownerLevel, [import_constants.idempotentProducerAmqpPropertyNames.producerId]: producerGroupId, [import_constants.idempotentProducerAmqpPropertyNames.producerSequenceNumber]: lastPublishedSequenceNumber } = this._sender.properties ?? {}; properties.ownerLevel = parseInt(ownerLevel, 10); properties.producerGroupId = parseInt(producerGroupId, 10); properties.lastPublishedSequenceNumber = parseInt(lastPublishedSequenceNumber, 10); } this._localPublishingProperties = properties; return { ...properties }; } /** * Send a batch of EventData to the EventHub. The "message_annotations", * "application_properties" and "properties" of the first message will be set as that * of the envelope (batch message). * @param events - An array of EventData objects to be sent in a Batch message. * @param options - Options to control the way the events are batched along with request options */ async send(events, options) { try { this.logger.info("trying to send EventData[]."); if (this._isIdempotentProducer && this._hasPendingSend) { throw new Error( `There can only be 1 "sendBatch" call in-flight per partition while "enableIdempotentRetries" is set to true.` ); } const eventCount = (0, import_eventDataBatch.isEventDataBatch)(events) ? events.count : events.length; if (eventCount === 0) { this.logger.info(`no events were passed to sendBatch.`); return; } if (this._isIdempotentProducer) { this._hasPendingSend = true; } this.logger.info("sending encoded batch message."); await this._trySendBatch(events, options); if (this._isIdempotentProducer) { commitIdempotentSequenceNumbers(events); if (this._localPublishingProperties) { const { lastPublishedSequenceNumber = 0 } = this._localPublishingProperties; this._localPublishingProperties.lastPublishedSequenceNumber = lastPublishedSequenceNumber + eventCount; } } return; } catch (err) { rollbackIdempotentSequenceNumbers(events); this.logger.warning( `an error occurred while sending the batch message ${err?.name}: ${err?.message}` ); (0, import_logger.logErrorStackTrace)(err); throw err; } finally { if (this._isIdempotentProducer) { this._hasPendingSend = false; } } } /** * @param sender - The rhea sender that contains the idempotent producer properties. */ _populateLocalPublishingProperties(sender) { const { [import_constants.idempotentProducerAmqpPropertyNames.epoch]: ownerLevel, [import_constants.idempotentProducerAmqpPropertyNames.producerId]: producerGroupId, [import_constants.idempotentProducerAmqpPropertyNames.producerSequenceNumber]: lastPublishedSequenceNumber } = sender.properties ?? {}; this._localPublishingProperties = { isIdempotentPublishingEnabled: this._isIdempotentProducer, partitionId: this.partitionId ?? "", lastPublishedSequenceNumber, ownerLevel, producerGroupId }; } _deleteFromCache() { this._sender = void 0; delete this._context.senders[this.name]; this.logger.verbose("deleted from the client cache."); } _createSenderOptions() { const srOptions = { name: this.name, source: this._id, target: { address: this.address }, onError: this._onAmqpError, onClose: this._onAmqpClose, onSessionError: this._onSessionError, onSessionClose: this._onSessionClose }; srOptions.desired_capabilities = [import_constants.geoReplication]; if (this._isIdempotentProducer) { srOptions.desired_capabilities.push(import_constants.idempotentProducerAmqpPropertyNames.capability); const idempotentProperties = generateIdempotentLinkProperties( this._userProvidedPublishingOptions, this._localPublishingProperties ); srOptions.properties = idempotentProperties; } this.logger.verbose("being created with options: %O", srOptions); return srOptions; } /** * Tries to send the message to EventHub if there is enough credit to send them * and the circular buffer has available space to settle the message after sending them. * * We have implemented a synchronous send over here in the sense that we shall be waiting * for the message to be accepted or rejected and accordingly resolve or reject the promise. * @param rheaMessage - The message to be sent to EventHub. * @returns Promise<void> */ async _trySendBatch(events, options = {}) { const abortSignal = options.abortSignal; const retryOptions = options.retryOptions || {}; const timeoutInMs = (0, import_retries.getRetryAttemptTimeoutInMs)(retryOptions); retryOptions.timeoutInMs = timeoutInMs; const sendEventPromise = async () => { const initStartTime = Date.now(); const sender = await this._getLink(options); const publishingProps = await this.getPartitionPublishingProperties(options); const timeTakenByInit = Date.now() - initStartTime; this.logger.verbose( "credit: %d available: %d", sender.credit, sender.session.outgoing.available() ); let waitTimeForSendable = 1e3; if (!sender.sendable() && timeoutInMs - timeTakenByInit > waitTimeForSendable) { this.logger.verbose("waiting for 1 second for sender to become sendable"); await (0, import_core_amqp.delay)(waitTimeForSendable); this.logger.verbose( "after waiting for a second, credit: %d available: %d", sender.credit, sender.session?.outgoing?.available() ); } else { waitTimeForSendable = 0; } if (!sender.sendable()) { const msg = `cannot send the message right now. Please try later.`; this.logger.warning(msg); const amqpError = { condition: import_core_amqp.ErrorNameConditionMapper.SenderBusyError, description: msg }; throw (0, import_core_amqp.translate)(amqpError); } if (timeoutInMs <= timeTakenByInit + waitTimeForSendable) { const desc = `was not able to send the message right now, due to operation timeout.`; this.logger.warning(desc); const e = { condition: import_core_amqp.ErrorNameConditionMapper.ServiceUnavailableError, description: desc }; throw (0, import_core_amqp.translate)(e); } try { const encodedMessage = transformEventsForSend(events, publishingProps, options); const delivery = await sender.send(encodedMessage, { format: 2147563264, timeoutInSeconds: (timeoutInMs - timeTakenByInit - waitTimeForSendable) / 1e3, abortSignal }); this.logger.info("sent message with delivery id: %d", delivery.id); } catch (err) { const error = err.innerError || err; const translatedError = (0, import_error.translateError)(error); throw translatedError; } }; const config = { operation: sendEventPromise, connectionId: this._context.connectionId, operationType: import_core_amqp.RetryOperationType.sendMessage, abortSignal, retryOptions }; try { await (0, import_core_amqp.retry)(config); } catch (err) { const translatedError = (0, import_core_amqp.translate)(err); this.logger.warning( "an error occurred while sending the message %s", `${translatedError?.name}: ${translatedError?.message}` ); (0, import_logger.logErrorStackTrace)(translatedError); throw translatedError; } } async _getLink(options = {}) { if (this.isOpen() && this._sender) { return this._sender; } const retryOptions = options.retryOptions || {}; const timeoutInMs = (0, import_retries.getRetryAttemptTimeoutInMs)(retryOptions); retryOptions.timeoutInMs = timeoutInMs; const senderOptions = this._createSenderOptions(); const startTime = Date.now(); const createLinkPromise = async () => { return import_core_amqp.defaultCancellableLock.acquire( this.senderLock, () => { const taskStartTime = Date.now(); const taskTimeoutInMs = timeoutInMs - (taskStartTime - startTime); return this._init({ ...senderOptions, abortSignal: options.abortSignal, timeoutInMs: taskTimeoutInMs }); }, { abortSignal: options.abortSignal, timeoutInMs } ); }; const config = { operation: createLinkPromise, connectionId: this._context.connectionId, operationType: import_core_amqp.RetryOperationType.senderLink, abortSignal: options.abortSignal, retryOptions }; try { return await (0, import_core_amqp.retry)(config); } catch (err) { const translatedError = (0, import_core_amqp.translate)(err); this.logger.warning( "an error occurred while creating: %s", `${translatedError?.name}: ${translatedError?.message}` ); (0, import_logger.logErrorStackTrace)(translatedError); throw translatedError; } } /** * Initializes the sender session on the connection. * Should only be called from _createLinkIfNotOpen */ async _init(options) { const createSender = async () => { this.logger.verbose("trying to be created..."); const sender = await this._context.connection.createAwaitableSender(options); this._sender = sender; this._populateLocalPublishingProperties(this._sender); this.isConnecting = false; this.logger.verbose("created with options: %O", options); if (!this._context.senders[this.name]) this._context.senders[this.name] = this; }; try { if (!this.isOpen() || !this._sender) { await this._context.readyToOpenLink(); this.authLoop = await (0, import_withAuth.withAuth)( createSender, this._context, this.audience, options.timeoutInMs, this.logger, { abortSignal: options.abortSignal } ); return this._sender; } else { this.logger.verbose("is open -> %s. Hence not reconnecting.", this.isOpen()); return this._sender; } } catch (err) { const translatedError = (0, import_core_amqp.translate)(err); this.logger.warning( "an error occurred while being created: %s", `${translatedError?.name}: ${translatedError?.message}` ); (0, import_logger.logErrorStackTrace)(translatedError); throw translatedError; } } /** * Creates a new sender to the given event hub, and optionally to a given partition if it is * not present in the context or returns the one present in the context. * @hidden * @param options - Options used to configure the EventHubSender. */ static create(context, senderId, options) { const ehSender = new EventHubSender(context, senderId, options); if (!context.senders[ehSender.name]) { context.senders[ehSender.name] = ehSender; } return context.senders[ehSender.name]; } } function generateIdempotentLinkProperties(userProvidedPublishingOptions, localPublishingOptions) { let ownerLevel; let producerGroupId; let sequenceNumber; if (localPublishingOptions) { ownerLevel = localPublishingOptions.ownerLevel; producerGroupId = localPublishingOptions.producerGroupId; sequenceNumber = localPublishingOptions.lastPublishedSequenceNumber; } else if (userProvidedPublishingOptions) { ownerLevel = userProvidedPublishingOptions.ownerLevel; producerGroupId = userProvidedPublishingOptions.producerGroupId; sequenceNumber = userProvidedPublishingOptions.startingSequenceNumber; } else { return {}; } const idempotentLinkProperties = { [import_constants.idempotentProducerAmqpPropertyNames.epoch]: (0, import_core_util.isDefined)(ownerLevel) ? import_rhea_promise.types.wrap_short(ownerLevel) : null, [import_constants.idempotentProducerAmqpPropertyNames.producerId]: (0, import_core_util.isDefined)(producerGroupId) ? import_rhea_promise.types.wrap_long(producerGroupId) : null, [import_constants.idempotentProducerAmqpPropertyNames.producerSequenceNumber]: (0, import_core_util.isDefined)(sequenceNumber) ? import_rhea_promise.types.wrap_int(sequenceNumber) : null }; return idempotentLinkProperties; } function transformEventsForSend(events, publishingProps, options = {}) { if ((0, import_eventDataBatch.isEventDataBatch)(events)) { return events._generateMessage(publishingProps); } else { const eventCount = events.length; const rheaMessages = []; const tracingProperties = options.tracingProperties ?? []; for (let i = 0; i < eventCount; i++) { const originalEvent = events[i]; const tracingProperty = tracingProperties[i]; const event = tracingProperty ? { ...originalEvent, properties: { ...originalEvent.properties, ...tracingProperty } } : originalEvent; const rheaMessage = (0, import_eventData.toRheaMessage)(event, options.partitionKey); const { lastPublishedSequenceNumber = 0 } = publishingProps; const startingSequenceNumber = lastPublishedSequenceNumber + 1; const pendingPublishSequenceNumber = startingSequenceNumber + i; (0, import_eventData.populateIdempotentMessageAnnotations)(rheaMessage, { ...publishingProps, publishSequenceNumber: pendingPublishSequenceNumber }); if (publishingProps.isIdempotentPublishingEnabled) { originalEvent[import_constants.PENDING_PUBLISH_SEQ_NUM_SYMBOL] = pendingPublishSequenceNumber; } rheaMessages.push(rheaMessage); } const batchMessage = { body: import_rhea_promise.message.data_sections(rheaMessages.map(import_rhea_promise.message.encode)) }; if (rheaMessages[0].message_annotations) { batchMessage.message_annotations = { ...rheaMessages[0].message_annotations }; } return import_rhea_promise.message.encode(batchMessage); } } function commitIdempotentSequenceNumbers(events) { if ((0, import_eventDataBatch.isEventDataBatch)(events)) { events._commitPublish(); } else { for (const event of events) { event._publishedSequenceNumber = event[import_constants.PENDING_PUBLISH_SEQ_NUM_SYMBOL]; delete event[import_constants.PENDING_PUBLISH_SEQ_NUM_SYMBOL]; } } } function rollbackIdempotentSequenceNumbers(events) { if ((0, import_eventDataBatch.isEventDataBatch)(events)) { } else { for (const event of events) { delete event[import_constants.PENDING_PUBLISH_SEQ_NUM_SYMBOL]; } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { EventHubSender, generateIdempotentLinkProperties, transformEventsForSend }); //# sourceMappingURL=eventHubSender.js.map