@azure/service-bus
Version:
Azure Service Bus SDK for JavaScript
170 lines • 9.66 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { receiverLogger as logger } from "../log";
import { calculateRenewAfterDuration } from "../util/utils";
/**
* Tracks locks for messages, renewing until a configurable duration.
*
* @internal
*/
export class LockRenewer {
constructor(_context, _maxAutoRenewDurationInMs) {
this._context = _context;
this._maxAutoRenewDurationInMs = _maxAutoRenewDurationInMs;
/**
* A map of link names to individual maps for each
* link that map a message ID to its auto-renewal timer.
*/
this._messageRenewLockTimers = new Map();
this._calculateRenewAfterDuration = calculateRenewAfterDuration;
}
/**
* Creates an AutoLockRenewer.
*
* @param linkEntity - Your link entity instance (probably 'this')
* @param context - The connection context for your link entity (probably 'this._context')
* @param options - The ReceiveOptions passed through to your message receiver.
* @returns if the lock mode is peek lock (or if is unspecified, thus defaulting to peekLock)
* and the options.maxAutoLockRenewalDurationInMs is greater than 0..Otherwise, returns undefined.
*/
static create(context, maxAutoRenewLockDurationInMs, receiveMode) {
if (receiveMode !== "peekLock") {
return undefined;
}
if (maxAutoRenewLockDurationInMs <= 0) {
return undefined;
}
return new LockRenewer(context, maxAutoRenewLockDurationInMs);
}
/**
* Cancels all pending lock renewals for messages on given link and removes all entries from our internal cache.
*/
stopAll(linkEntity) {
logger.verbose(`${linkEntity.logPrefix} Clearing message renew lock timers for all the active messages.`);
const messagesForLink = this._messageRenewLockTimers.get(linkEntity.name);
if (messagesForLink == null) {
return;
}
for (const messageId of messagesForLink.keys()) {
this._stopAndRemoveById(linkEntity, messagesForLink, messageId);
}
this._messageRenewLockTimers.delete(linkEntity.name);
}
/**
* Stops lock renewal for a single message.
*
* @param bMessage - The message whose lock renewal we will stop.
*/
stop(linkEntity, bMessage) {
const messageId = bMessage.messageId;
const messagesForLink = this._messageRenewLockTimers.get(linkEntity.name);
if (messagesForLink == null) {
return;
}
this._stopAndRemoveById(linkEntity, messagesForLink, messageId);
}
/**
* Starts lock renewal for a single message.
*
* @param bMessage - The message whose lock renewal we will start.
*/
start(linkEntity, bMessage, onError) {
try {
const logPrefix = linkEntity.logPrefix;
if (bMessage.lockToken == null) {
throw new Error(`Can't start auto lock renewal for message with message id '${bMessage.messageId}' since it does not have a lock token.`);
}
const lockToken = bMessage.lockToken;
const linkMessageMap = this._getOrCreateMapForLink(linkEntity);
// - We need to renew locks before they expire by looking at bMessage.lockedUntilUtc.
// - This autorenewal needs to happen **NO MORE** than maxAutoRenewDurationInMs
// - We should be able to clear the renewal timer when the user's message handler
// is done (whether it succeeds or fails).
// Setting the messageId with undefined value in the linkMessageMap because we
// track state by checking the presence of messageId in the map. It is removed from the map
// when an attempt is made to settle the message (either by the user or by the sdk) OR
// when the execution of user's message handler completes.
linkMessageMap.set(bMessage.messageId, undefined);
logger.verbose(`${logPrefix} message with id '${bMessage.messageId}' is locked until ${bMessage.lockedUntilUtc.toString()}.`);
const totalAutoLockRenewDuration = Date.now() + this._maxAutoRenewDurationInMs;
const totalAutoLockRenewDurationDate = new Date(totalAutoLockRenewDuration);
logger.verbose(`${logPrefix} Total autolockrenew duration for message with id '${bMessage.messageId}' is: ${totalAutoLockRenewDurationDate.toString()}`);
const autoRenewLockTask = () => {
const renewalNeededToMaintainLock =
// if the lock expires _after_ our max auto-renew duration there's no reason to
// spin up an auto-renewer - it's already held for the duration.
totalAutoLockRenewDurationDate > bMessage.lockedUntilUtc;
if (!renewalNeededToMaintainLock) {
logger.verbose(`${logPrefix} Autolockrenew not needed as message's lockedUntilUtc ${bMessage.lockedUntilUtc} is after the total autolockrenew duration ${totalAutoLockRenewDurationDate} for message with messageId '${bMessage.messageId}'. Hence we will stop the autoLockRenewTask.`);
this.stop(linkEntity, bMessage);
}
else if (Date.now() >= totalAutoLockRenewDuration) {
// once we've exceeded the max amount of time we'll renew we can stop.
logger.verbose(`${logPrefix} Current time ${new Date()} exceeds the total autolockrenew duration ${totalAutoLockRenewDurationDate} for message with messageId '${bMessage.messageId}'. Hence we will stop the autoLockRenewTask.`);
this.stop(linkEntity, bMessage);
}
else {
if (linkMessageMap.has(bMessage.messageId)) {
// TODO: We can run into problems with clock skew between the client and the server.
// It would be better to calculate the duration based on the "lockDuration" property
// of the queue. However, we do not have the management plane of the client ready for
// now. Hence we rely on the lockedUntilUtc property on the message set by ServiceBus.
const amount = this._calculateRenewAfterDuration(bMessage.lockedUntilUtc);
logger.verbose(`${logPrefix} Sleeping for ${amount} milliseconds while renewing the lock for message with id '${bMessage.messageId}'`);
// Setting the value of the messageId to the actual timer. This will be cleared when
// an attempt is made to settle the message (either by the user or by the sdk) OR
// when the execution of user's message handler completes.
const autoRenewTimer = setTimeout(async () => {
try {
logger.verbose(`${logPrefix} Attempting to renew the lock for message with id '${bMessage.messageId}'.`);
bMessage.lockedUntilUtc = await this._context
.getManagementClient(linkEntity.entityPath)
.renewLock(lockToken, {
associatedLinkName: linkEntity.name,
});
logger.verbose(`${logPrefix} Successfully renewed the lock for message with id '${bMessage.messageId}'. Starting next auto-lock-renew cycle for message.`);
autoRenewLockTask();
}
catch (err) {
logger.logError(err, `${logPrefix} An error occurred while auto renewing the message lock '${bMessage.lockToken}' for message with id '${bMessage.messageId}'`);
onError(err);
}
}, amount);
// Prevent the active Timer from keeping the Node.js event loop active.
if (typeof autoRenewTimer.unref === "function") {
autoRenewTimer.unref();
}
linkMessageMap.set(bMessage.messageId, autoRenewTimer);
}
else {
logger.verbose(`${logPrefix} Looks like the message lock renew timer has already been cleared for message with id '${bMessage.messageId}'.`);
}
}
};
// start
autoRenewLockTask();
}
catch (err) {
onError(err);
}
}
_getOrCreateMapForLink(linkEntity) {
if (!this._messageRenewLockTimers.has(linkEntity.name)) {
this._messageRenewLockTimers.set(linkEntity.name, new Map());
}
return this._messageRenewLockTimers.get(linkEntity.name);
}
_stopAndRemoveById(linkEntity, linkMessageMap, messageId) {
if (messageId == null) {
throw new Error("Failed to stop auto lock renewal - no message ID");
}
// TODO: messageId doesn't actually need to be unique. Perhaps we should use lockToken
// instead?
if (linkMessageMap.has(messageId)) {
clearTimeout(linkMessageMap.get(messageId));
logger.verbose(`${linkEntity.logPrefix} Cleared the message renew lock timer for message with id '${messageId}'.`);
linkMessageMap.delete(messageId);
}
}
}
//# sourceMappingURL=autoLockRenewer.js.map