@azure/service-bus
Version:
Azure Service Bus SDK for JavaScript
279 lines • 13.3 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { senderLogger as logger } from "../log";
import { message as RheaMessageUtil, } from "rhea-promise";
import { Constants, ErrorNameConditionMapper, RetryOperationType, retry, } from "@azure/core-amqp";
import { toRheaMessage } from "../serviceBusMessage";
import { LinkEntity } from "./linkEntity";
import { getUniqueName, waitForSendable, waitForTimeoutOrAbortOrResolve } from "../util/utils";
import { throwErrorIfConnectionClosed } from "../util/errors";
import { ServiceBusMessageBatchImpl } from "../serviceBusMessageBatch";
import { ServiceBusError, translateServiceBusError } from "../serviceBusError";
import { isDefined } from "@azure/core-util";
import { defaultDataTransformer } from "../dataTransformer";
/**
* @internal
* Describes the MessageSender that will send messages to ServiceBus.
*/
export class MessageSender extends LinkEntity {
constructor(identifier, connectionContext, entityPath, retryOptions) {
super(entityPath, entityPath, connectionContext, "sender", logger, {
address: entityPath,
audience: `${connectionContext.config.endpoint}${entityPath}`,
});
this.identifier = identifier;
this._retryOptions = retryOptions;
this._onAmqpError = (context) => {
const senderError = context.sender && context.sender.error;
logger.logError(senderError, "%s 'sender_error' event occurred on the sender '%s' with address '%s'. " +
"The associated error", this.logPrefix, this.name, this.address);
// TODO: Consider rejecting promise in trySendBatch() or createBatch()
};
this._onSessionError = (context) => {
const sessionError = context.session && context.session.error;
logger.logError(sessionError, "%s 'session_error' event occurred on the session of sender '%s' with address '%s'. " +
"The associated error", this.logPrefix, this.name, this.address);
// TODO: Consider rejecting promise in trySendBatch() or createBatch()
};
this._onAmqpClose = async (context) => {
const senderError = context.sender && context.sender.error;
logger.logError(senderError, `${this.logPrefix} 'sender_close' event occurred. The associated error is`);
await this.onDetached().catch((err) => {
logger.logError(err, `${this.logPrefix} error when closing sender after 'sender_close' event`);
});
};
this._onSessionClose = async (context) => {
const sessionError = context.session && context.session.error;
logger.logError(sessionError, `${this.logPrefix} 'session_close' event occurred. The associated error is`);
await this.onDetached().catch((err) => {
logger.logError(err, `${this.logPrefix} error when closing sender after 'session_close' event`);
});
};
}
_createSenderOptions(newName) {
if (newName)
this.name = getUniqueName(this.baseName);
const srOptions = {
name: this.name,
target: {
address: this.address,
},
source: this.identifier,
onError: this._onAmqpError,
onClose: this._onAmqpClose,
onSessionError: this._onSessionError,
onSessionClose: this._onSessionClose,
};
logger.verbose(`${this.logPrefix} Creating sender with options: %O`, srOptions);
return srOptions;
}
/**
* Tries to send the message to ServiceBus 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 encodedMessage - The encoded message to be sent to ServiceBus.
* @param sendBatch - Boolean indicating whether the encoded message represents a batch of messages or not
*/
_trySend(encodedMessage, sendBatch, options) {
const abortSignal = options?.abortSignal;
const timeoutInMs = !isDefined(this._retryOptions.timeoutInMs)
? Constants.defaultOperationTimeoutInMs
: this._retryOptions.timeoutInMs;
const sendEventPromise = async () => {
const initStartTime = Date.now();
if (!this.isOpen()) {
try {
await waitForTimeoutOrAbortOrResolve({
actionFn: () => this.open(undefined, options?.abortSignal),
abortSignal: options?.abortSignal,
timeoutMs: timeoutInMs,
timeoutMessage: `[${this._context.connectionId}] Sender "${this.name}" ` +
`with address "${this.address}", was not able to send the message right now, due ` +
`to operation timeout.`,
});
}
catch (err) {
const translatedError = translateServiceBusError(err);
logger.logError(translatedError, "%s An error occurred while creating the sender", this.logPrefix, this.name);
throw translatedError;
}
}
const timeTakenByInit = Date.now() - initStartTime;
logger.verbose("%s Sender '%s', credit: %d available: %d", this.logPrefix, this.name, this.link?.credit, this.link?.session?.outgoing?.available());
const waitingTime = await waitForSendable(logger, this.logPrefix, this.name, timeoutInMs - timeTakenByInit, this.link, this.link?.session?.outgoing?.available());
if (timeoutInMs <= timeTakenByInit + waitingTime) {
const desc = `${this.logPrefix} Sender "${this.name}" ` +
`with address "${this.address}", was not able to send the message right now, due ` +
`to operation timeout.`;
logger.warning(desc);
const e = {
condition: ErrorNameConditionMapper.ServiceUnavailableError,
description: desc,
};
throw translateServiceBusError(e);
}
if (!this.link) {
const msg = `[${this.logPrefix}] Cannot send the message. Link is not ready.`;
logger.warning(msg);
const amqpError = {
condition: ErrorNameConditionMapper.SenderNotReadyError,
description: msg,
};
throw translateServiceBusError(amqpError);
}
try {
const delivery = await this.link.send(encodedMessage, {
format: sendBatch ? 0x80013700 : 0,
timeoutInSeconds: (timeoutInMs - timeTakenByInit - waitingTime) / 1000,
abortSignal,
});
logger.verbose("%s Sender '%s', sent message with delivery id: %d", this.logPrefix, this.name, delivery.id);
}
catch (error) {
const translatedError = translateServiceBusError(error.innerError || error);
logger.logError(translatedError, `${this.logPrefix} An error occurred while sending the message`);
throw translatedError;
}
};
const config = {
operation: sendEventPromise,
connectionId: this._context.connectionId,
operationType: RetryOperationType.sendMessage,
retryOptions: this._retryOptions,
abortSignal: abortSignal,
};
return retry(config);
}
createRheaLink(options) {
return this._context.connection.createAwaitableSender(options);
}
/**
* Initializes the sender session on the connection.
*/
async open(options, abortSignal) {
try {
if (!options) {
options = this._createSenderOptions();
}
await this.initLink(options, abortSignal);
}
catch (err) {
const translatedError = translateServiceBusError(err);
logger.logError(translatedError, `${this.logPrefix} An error occurred while creating the sender`);
// Fix the unhelpful error messages for the OperationTimeoutError that comes from `rhea-promise`.
if (translatedError.code === "OperationTimeoutError") {
translatedError.message =
"Failed to create a sender within allocated time and retry attempts.";
}
throw translatedError;
}
}
/**
* Closes the rhea link.
* To be called when connection is disconnected, onAmqpClose and onSessionClose events.
*/
async onDetached() {
// Clears the token renewal timer. Closes the link and its session if they are open.
// Removes the link and its session if they are present in rhea's cache.
await this.closeLink();
}
/**
* Determines whether the AMQP sender link is open. If open then returns true else returns false.
*/
isOpen() {
const result = this.link == null ? false : this.link.isOpen();
logger.verbose("%s Sender '%s' with address '%s' is open? -> %s", this.logPrefix, this.name, this.address, result);
return result;
}
/**
* Sends the given message, with the given options on this link
*
* @param data - Message to send. Will be sent as UTF8-encoded JSON string.
*/
async send(data, options) {
throwErrorIfConnectionClosed(this._context);
try {
const amqpMessage = toRheaMessage(data, defaultDataTransformer);
// TODO: this body of logic is really similar to what's in sendMessages. Unify what we can.
const encodedMessage = RheaMessageUtil.encode(amqpMessage);
logger.verbose("%s Sender '%s', trying to send message: %O", this.logPrefix, this.name, data);
return await this._trySend(encodedMessage, false, options);
}
catch (err) {
logger.logError(err, "%s An error occurred while sending the message: %O\nError", this.logPrefix, data);
throw err;
}
}
/**
* Returns maximum message size on the AMQP sender link.
*
* Options to configure the `createBatch` method on the `Sender`.
* - `maxSizeInBytes`: The upper limit for the size of batch.
*
* Example usage:
* ```js
* {
* retryOptions: { maxRetries: 5; timeoutInMs: 10 }
* }
* ```
*/
async getMaxMessageSize(options = {}) {
const retryOptions = options.retryOptions || {};
if (this.isOpen()) {
return this.link.maxMessageSize;
}
const config = {
operation: async () => {
await this.open(undefined, options?.abortSignal);
if (this.link) {
return this.link.maxMessageSize;
}
throw new ServiceBusError("Link failed to initialize, cannot get max message size.", "GeneralError");
},
connectionId: this._context.connectionId,
operationType: RetryOperationType.senderLink,
retryOptions: retryOptions,
abortSignal: options?.abortSignal,
};
return retry(config);
}
async createBatch(options) {
throwErrorIfConnectionClosed(this._context);
let maxMessageSize = await this.getMaxMessageSize({
retryOptions: this._retryOptions,
abortSignal: options?.abortSignal,
});
if (options?.maxSizeInBytes) {
if (options.maxSizeInBytes > maxMessageSize) {
const error = new Error(`Max message size (${options.maxSizeInBytes} bytes) is greater than maximum message size (${maxMessageSize} bytes) on the AMQP sender link.`);
throw error;
}
maxMessageSize = options.maxSizeInBytes;
}
return new ServiceBusMessageBatchImpl(this._context, maxMessageSize);
}
async sendBatch(batchMessage, options) {
throwErrorIfConnectionClosed(this._context);
try {
logger.verbose("%s Sender '%s', sending encoded batch message.", this.logPrefix, this.name, batchMessage);
return await this._trySend(batchMessage._generateMessage(), true, options);
}
catch (err) {
logger.logError(err, "%s Sender '%s': An error occurred while sending the messages: %O\nError", this.logPrefix, this.name, batchMessage);
throw err;
}
}
static create(identifier, context, entityPath, retryOptions) {
throwErrorIfConnectionClosed(context);
const sbSender = new MessageSender(identifier, context, entityPath, retryOptions);
context.senders[sbSender.name] = sbSender;
return sbSender;
}
removeLinkFromContext() {
delete this._context.senders[this.name];
}
}
//# sourceMappingURL=messageSender.js.map