@azure/event-hubs
Version:
Azure Event Hubs SDK for JS.
613 lines (612 loc) • 23.8 kB
JavaScript
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 @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