murlock
Version:
A distributed locking solution for NestJS, providing a decorator for critical sections with Redis-based synchronization.
248 lines • 11.4 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var MurLockService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MurLockService = void 0;
const common_1 = require("@nestjs/common");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const redis_1 = require("redis");
const als_service_1 = require("./als/als.service");
const exceptions_1 = require("./exceptions");
const utils_1 = require("./utils");
let MurLockService = MurLockService_1 = class MurLockService {
constructor(options, asyncStorageService) {
this.options = options;
this.asyncStorageService = asyncStorageService;
this.logger = new common_1.Logger(MurLockService_1.name);
}
onModuleInit() {
return __awaiter(this, void 0, void 0, function* () {
try {
this.lockScript = yield (0, promises_1.readFile)((0, path_1.join)(__dirname, './lua/lock.lua'), 'utf8');
this.unlockScript = yield (0, promises_1.readFile)((0, path_1.join)(__dirname, './lua/unlock.lua'), 'utf8');
}
catch (error) {
throw new exceptions_1.MurLockException(`Failed to load Lua scripts: ${error.message}`);
}
this.redisClient = (0, redis_1.createClient)(Object.assign(Object.assign({}, this.options.redisOptions), { socket: Object.assign(Object.assign({}, this.options.redisOptions.socket), { keepAlive: false, reconnectStrategy: (retries) => {
const delay = Math.min(retries * 500, 5000);
this.log('warn', `MurLock Redis reconnect attempt ${retries}, waiting ${delay} ms...`);
return delay;
} }) }));
this.registerRedisErrorHandlers();
try {
yield this.redisClient.connect();
}
catch (error) {
this.log('error', `Failed to connect to Redis: ${error.message}`);
if (this.options.failFastOnRedisError) {
throw new exceptions_1.MurLockException(`Redis connection failed: ${error.message}`);
}
}
});
}
onApplicationShutdown(signal) {
return __awaiter(this, void 0, void 0, function* () {
this.log('log', 'Shutting down MurLock Redis client.');
if (this.redisClient && this.redisClient.isOpen) {
yield this.redisClient.quit();
}
});
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
log(level, message, context) {
const levels = [
'debug',
'log',
'warn',
'error',
];
if (levels.indexOf(level) >= levels.indexOf(this.options.logLevel)) {
this.logger[level](message, context);
}
}
lock(lockKey, releaseTime, clientId, wait) {
return __awaiter(this, void 0, void 0, function* () {
this.log('debug', `MurLock Client ID is ${clientId}`);
if (this.options.blocking) {
return this.blockingLock(lockKey, releaseTime, clientId);
}
const attemptLock = (attemptsRemaining) => __awaiter(this, void 0, void 0, function* () {
if (attemptsRemaining === 0) {
throw new exceptions_1.MurLockException(`Failed to obtain lock for key ${lockKey} after ${this.options.maxAttempts} attempts.`);
}
try {
const isLockSuccessful = yield this.redisClient.sendCommand([
'EVAL',
this.lockScript,
'1',
lockKey,
clientId,
releaseTime.toString(),
]);
if (isLockSuccessful === 1) {
this.log('log', `Successfully obtained lock for key ${lockKey}`);
return true;
}
else {
const delay = wait
? typeof wait === 'function'
? wait(this.options.maxAttempts - attemptsRemaining + 1)
: wait
: this.options.wait *
(this.options.maxAttempts - attemptsRemaining + 1);
this.log('warn', `Failed to obtain lock for key ${lockKey}, retrying in ${delay} ms...`);
yield this.sleep(delay);
return attemptLock(attemptsRemaining - 1);
}
}
catch (error) {
throw new exceptions_1.MurLockException(`Unexpected error when trying to obtain lock for key ${lockKey}: ${error.message}`);
}
});
return attemptLock(this.options.maxAttempts);
});
}
unlock(lockKey, clientId) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield this.redisClient.sendCommand([
'EVAL',
this.unlockScript,
'1',
lockKey,
clientId,
]);
if (result === 0) {
if (!this.options.ignoreUnlockFail) {
throw new exceptions_1.MurLockException(`Failed to release lock for key ${lockKey}`);
}
else {
this.log('warn', `Failed to release lock for key ${lockKey}, but throwing errors is disabled.`);
}
}
});
}
acquireLock(lockKey, clientId, releaseTime, wait) {
return __awaiter(this, void 0, void 0, function* () {
let isLockSuccessful = false;
try {
isLockSuccessful = yield this.lock(lockKey, releaseTime, clientId, wait);
}
catch (error) {
throw new exceptions_1.MurLockException(`Failed to acquire lock for key ${lockKey}: ${error.message}`);
}
if (!isLockSuccessful) {
throw new exceptions_1.MurLockException(`Could not obtain lock for key ${lockKey}`);
}
});
}
releaseLock(lockKey, clientId) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.unlock(lockKey, clientId);
}
catch (error) {
throw new exceptions_1.MurLockException(`Failed to release lock for key ${lockKey}: ${error.message}`);
}
});
}
runWithLock(lockKey, releaseTime, waitOrFn, fn) {
return __awaiter(this, void 0, void 0, function* () {
let wait;
let operation;
if (fn === undefined) {
operation = waitOrFn;
}
else {
wait = waitOrFn;
operation = fn;
}
this.asyncStorageService.registerContext();
this.asyncStorageService.setClientID('clientId', (0, utils_1.generateUuid)());
const clientId = this.asyncStorageService.get('clientId');
yield this.acquireLock(lockKey, clientId, releaseTime, wait);
try {
return yield operation();
}
finally {
yield this.releaseLock(lockKey, clientId);
}
});
}
registerRedisErrorHandlers() {
this.redisClient.on('error', (err) => {
this.log('error', `MurLock Redis Client Error: ${err.message}`);
if (this.options.failFastOnRedisError) {
this.log('error', 'MurLock Redis entering fail-fast shutdown due to Redis error.');
process.exit(1);
}
});
this.redisClient.on('reconnecting', () => {
this.log('warn', 'MurLock Redis Client attempting reconnect...');
});
this.redisClient.on('ready', () => {
this.log('log', 'MurLock Redis Client connected and ready.');
});
this.redisClient.on('end', () => {
this.log('warn', 'MurLock Redis Client connection closed.');
});
}
blockingLock(lockKey, releaseTime, clientId) {
return __awaiter(this, void 0, void 0, function* () {
while (true) {
try {
const isLockSuccessful = yield this.redisClient.sendCommand([
'EVAL',
this.lockScript,
'1',
lockKey,
clientId,
releaseTime.toString(),
]);
if (isLockSuccessful === 1) {
this.log('log', `Successfully obtained lock for key ${lockKey} in blocking mode`);
return true;
}
else {
this.log('warn', `Lock busy for key ${lockKey}, waiting ${this.options.wait} ms before next attempt (blocking mode)...`);
yield this.sleep(this.options.wait);
}
}
catch (error) {
this.log('error', `Unexpected error in blocking lock for key ${lockKey}: ${error.message}`);
yield this.sleep(this.options.wait);
}
}
});
}
};
exports.MurLockService = MurLockService;
exports.MurLockService = MurLockService = MurLockService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)('MURLOCK_OPTIONS')),
__metadata("design:paramtypes", [Object, als_service_1.AsyncStorageService])
], MurLockService);
//# sourceMappingURL=murlock.service.js.map