@azure/service-bus
Version:
Azure Service Bus SDK for JavaScript
258 lines • 10.9 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { logger, receiverLogger } from "../log";
import { translateServiceBusError } from "../serviceBusError";
import { DispositionType, } from "../serviceBusMessage";
import { Constants, ErrorNameConditionMapper, retry, RetryMode, RetryOperationType, } from "@azure/core-amqp";
import { MessageAlreadySettled } from "../util/errors";
import { delay, isDefined } from "@azure/core-util";
import { toSpanOptions, tracingClient } from "../diagnostics/tracing";
import { extractSpanContextFromServiceBusMessage } from "../diagnostics/instrumentServiceBusMessage";
/**
* @internal
*/
export function assertValidMessageHandlers(handlers) {
if (handlers &&
handlers.processMessage instanceof Function &&
handlers.processError instanceof Function) {
return;
}
throw new TypeError('Invalid "MessageHandlers" provided.');
}
/**
* @internal
*/
export async function* getMessageIterator(receiver, options) {
while (true) {
const messages = await receiver.receiveMessages(1, options);
if (messages.length === 0) {
continue;
}
yield messages[0];
}
}
/**
* @internal
*/
export function wrapProcessErrorHandler(handlers, loggerParam = receiverLogger) {
return async (args) => {
try {
args.error = translateServiceBusError(args.error);
await handlers.processError(args);
}
catch (err) {
loggerParam.logError(err, `An error was thrown from the user's processError handler`);
}
};
}
/**
* @internal
*
*/
export function completeMessage(message, context, entityPath, retryOptions) {
receiverLogger.verbose("[%s] Completing the message with id '%s'.", context.connectionId, message.messageId);
const tracingContext = extractSpanContextFromServiceBusMessage(message);
const spanLinks = tracingContext ? [{ tracingContext }] : [];
return tracingClient.withSpan("ServicebusReceiver.complete", {}, () => settleMessage(message, DispositionType.complete, context, entityPath, {
retryOptions,
}), {
spanLinks,
...toSpanOptions({ entityPath, host: context.config.host }, "settle", "client"),
});
}
/**
* @internal
*
*/
export function abandonMessage(message, context, entityPath, propertiesToModify, retryOptions) {
receiverLogger.verbose("[%s] Abandoning the message with id '%s'.", context.connectionId, message.messageId);
const tracingContext = extractSpanContextFromServiceBusMessage(message);
const spanLinks = tracingContext ? [{ tracingContext }] : [];
return tracingClient.withSpan("ServicebusReceiver.abandon", {}, () => settleMessage(message, DispositionType.abandon, context, entityPath, {
propertiesToModify,
retryOptions,
}), {
spanLinks,
...toSpanOptions({ entityPath, host: context.config.host }, "settle", "client"),
});
}
/**
* @internal
*
*/
export function deferMessage(message, context, entityPath, propertiesToModify, retryOptions) {
receiverLogger.verbose("[%s] Deferring the message with id '%s'.", context.connectionId, message.messageId);
const tracingContext = extractSpanContextFromServiceBusMessage(message);
const spanLinks = tracingContext ? [{ tracingContext }] : [];
return tracingClient.withSpan("ServiceBusReceiver.defer", {}, () => settleMessage(message, DispositionType.defer, context, entityPath, {
retryOptions,
propertiesToModify,
}), {
spanLinks,
...toSpanOptions({ entityPath, host: context.config.host }, "settle", "client"),
});
}
/**
* @internal
*
*/
export function deadLetterMessage(message, context, entityPath, propertiesToModify, retryOptions) {
receiverLogger.verbose("[%s] Deadlettering the message with id '%s'.", context.connectionId, message.messageId);
const actualPropertiesToModify = {
...propertiesToModify,
};
// these two fields are handled specially and don't need to be in here.
delete actualPropertiesToModify.deadLetterErrorDescription;
delete actualPropertiesToModify.deadLetterReason;
const dispositionStatusOptions = {
propertiesToModify: actualPropertiesToModify,
deadLetterReason: propertiesToModify?.deadLetterReason,
deadLetterDescription: propertiesToModify?.deadLetterErrorDescription,
retryOptions,
};
const tracingContext = extractSpanContextFromServiceBusMessage(message);
const spanLinks = tracingContext ? [{ tracingContext }] : [];
return tracingClient.withSpan("ServiceBusReceiver.deadLetter", {}, () => settleMessage(message, DispositionType.deadletter, context, entityPath, dispositionStatusOptions), {
spanLinks,
...toSpanOptions({ entityPath, host: context.config.host }, "settle", "client"),
});
}
/**
* @internal
*/
export function settleMessage(message, operation, context, entityPath, options, _settleMessageOperation = settleMessageOperation) {
return retry({
connectionId: context.connectionId,
operation: () => {
return _settleMessageOperation(message, operation, context, entityPath, options);
},
operationType: RetryOperationType.messageSettlement,
abortSignal: options?.abortSignal,
retryOptions: options?.retryOptions,
});
}
/**
* @internal
*
* NOTE: it's tempting to make this method non-async. However, doing so makes it too easy
* to throw exceptions that will not be "catchable" by people chaining to the returned Promise
* since we can throw exceptions outside of the Promise's scope.
*/
export async function settleMessageOperation(message, operation, context, entityPath, options) {
const isDeferredMessage = !message.delivery.link;
const receiver = isDeferredMessage
? undefined
: context.getReceiverFromCache(message.delivery.link.name, message.sessionId);
const associatedLinkName = receiver?.name;
let error;
if (message.delivery.remote_settled) {
error = new Error(MessageAlreadySettled);
}
else if (!isDeferredMessage &&
(!receiver || !receiver.isOpen()) &&
isDefined(message.sessionId)) {
error = translateServiceBusError({
description: `Failed to ${operation} the message as the AMQP link with which the message was ` +
`received is no longer alive.`,
condition: ErrorNameConditionMapper.SessionLockLostError,
});
}
if (error) {
receiverLogger.logError(error, "[%s] An error occurred when settling a message with id '%s'", context.connectionId, message.messageId);
throw error;
}
// Message Settlement with managementLink
// 1. If the received message is deferred as such messages can only be settled using managementLink
// 2. If the associated receiver link is not available. This does not apply to messages from sessions as we need a lock on the session to do so.
if (isDeferredMessage || ((!receiver || !receiver.isOpen()) && !isDefined(message.sessionId))) {
return context
.getManagementClient(entityPath)
.updateDispositionStatus(message.lockToken, operation, {
...options,
associatedLinkName,
sessionId: message.sessionId,
})
.catch((err) => {
throw translateServiceBusError(err);
});
}
return receiver.settleMessage(message, operation, options).catch((err) => {
throw translateServiceBusError(err);
});
}
/**
* Calculates delay between retries, in milliseconds.
*/
function calculateDelay(attemptCount, retryDelayInMs, maxRetryDelayInMs, mode) {
if (mode === RetryMode.Exponential) {
const boundedRandDelta = retryDelayInMs * 0.8 +
Math.floor(Math.random() * (retryDelayInMs * 1.2 - retryDelayInMs * 0.8));
const incrementDelta = boundedRandDelta * (Math.pow(2, attemptCount) - 1);
return Math.min(incrementDelta, maxRetryDelayInMs);
}
return retryDelayInMs;
}
/**
* Retry infinitely until success, reporting in between retry attempts.
*
* This function will only stop retrying if:
* - args.retryConfig.operation resolves successfully
* - args.retryConfig.operation rejects with an `AbortError`
*
* @internal
*/
export async function retryForever(args, retryFn = retry) {
let numRetryCycles = 0;
const config = args.retryConfig;
if (!config.retryOptions) {
config.retryOptions = {};
}
// eslint-disable-next-line eqeqeq
if (config.retryOptions.retryDelayInMs == undefined || config.retryOptions.retryDelayInMs < 0) {
config.retryOptions.retryDelayInMs = Constants.defaultDelayBetweenOperationRetriesInMs;
}
if (
// eslint-disable-next-line eqeqeq
config.retryOptions.maxRetryDelayInMs == undefined ||
config.retryOptions.maxRetryDelayInMs < 0) {
config.retryOptions.maxRetryDelayInMs = Constants.defaultMaxDelayForExponentialRetryInMs;
}
if (!config.retryOptions.mode) {
config.retryOptions.mode = RetryMode.Fixed;
}
// The retries are broken up into cycles, giving the user some control over how often
// we actually attempt to retry.
// eslint-disable-next-line no-constant-condition
while (true) {
++numRetryCycles;
try {
return await retryFn(args.retryConfig);
}
catch (err) {
// if the user aborts the operation we're immediately done.
// AbortError is also thrown by linkEntity.init() if the connection has been
// permanently closed.
if (err.name === "AbortError") {
logger.warning(`${args.logPrefix} AbortError caught, ending retries.`);
throw err;
}
// we only report the error here - this avoids spamming the user with too many
// redundant reports of errors while still providing them incremental status on failures.
try {
args.onError(err);
}
catch (error) {
logger.error("args.onerror has thrown", error);
}
args.logger.logError(err, `${args.logPrefix} Error thrown in retry cycle ${numRetryCycles}, restarting retry cycle with retry options`, args.retryConfig);
const delayInMs = calculateDelay(numRetryCycles, config.retryOptions.retryDelayInMs, config.retryOptions.maxRetryDelayInMs, config.retryOptions.mode);
logger.verbose("[%s] Sleeping for %d milliseconds for '%s'.", config.connectionId, delayInMs, config.operationType);
await delay(delayInMs, {
abortSignal: config.abortSignal,
abortErrorMsg: "Retry cycle has been cancelled by the user.",
});
continue;
}
}
}
//# sourceMappingURL=receiverCommon.js.map