refactor(server): make redis required module (#9121)

This commit is contained in:
forehalo
2024-12-13 06:27:15 +00:00
parent 81c68032e1
commit 0e73737407
40 changed files with 292 additions and 728 deletions
@@ -1,6 +1,6 @@
import { Global, Module } from '@nestjs/common';
import { Locker } from './local-lock';
import { Locker } from './locker';
import { Mutex, RequestMutex } from './mutex';
@Global()
@@ -11,4 +11,4 @@ import { Mutex, RequestMutex } from './mutex';
export class MutexModule {}
export { Locker, Mutex, RequestMutex };
export { type Locker as ILocker, Lock } from './lock';
export { Lock } from './lock';
@@ -1,28 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Cache } from '../cache';
import { Lock, Locker as ILocker } from './lock';
@Injectable()
export class Locker implements ILocker {
constructor(private readonly cache: Cache) {}
async lock(owner: string, key: string): Promise<Lock> {
const lockKey = `MutexLock:${key}`;
const prevOwner = await this.cache.get<string>(lockKey);
if (prevOwner && prevOwner !== owner) {
throw new Error(`Lock for resource [${key}] has been holder by others`);
}
const acquired = await this.cache.set(lockKey, owner);
if (acquired) {
return new Lock(async () => {
await this.cache.delete(lockKey);
});
}
throw new Error(`Failed to acquire lock for resource [${key}]`);
}
}
@@ -17,7 +17,3 @@ export class Lock implements AsyncDisposable {
await this.release();
}
}
export interface Locker {
lock(owner: string, key: string): Promise<Lock>;
}
@@ -0,0 +1,66 @@
import { Injectable, Logger } from '@nestjs/common';
import { Command } from 'ioredis';
import { SessionRedis } from '../redis';
import { Lock } from './lock';
// === atomic mutex lock ===
// acquire lock
// return 1 if lock is acquired
// return 0 if lock is not acquired
const lockScript = `local key = KEYS[1]
local owner = ARGV[1]
-- if lock is not exists or lock is owned by the owner
-- then set lock to the owner and return 1, otherwise return 0
-- if the lock is not released correctly due to unexpected reasons
-- lock will be released after 60 seconds
if redis.call("get", key) == owner or redis.call("set", key, owner, "NX", "EX", 60) then
return 1
else
return 0
end`;
// release lock
// return 1 if lock is released or lock is not exists
// return 0 if lock is not owned by the owner
const unlockScript = `local key = KEYS[1]
local owner = ARGV[1]
local value = redis.call("get", key)
if value == owner then
return redis.call("del", key)
elseif value == nil then
return 1
else
return 0
end`;
@Injectable()
export class Locker {
private readonly logger = new Logger(Locker.name);
constructor(private readonly redis: SessionRedis) {}
async lock(owner: string, key: string): Promise<Lock> {
const lockKey = `MutexLock:${key}`;
this.logger.verbose(`Client ${owner} is trying to lock resource ${key}`);
const success = await this.redis.sendCommand(
new Command('EVAL', [lockScript, '1', lockKey, owner])
);
if (success === 1) {
return new Lock(async () => {
const result = await this.redis.sendCommand(
new Command('EVAL', [unlockScript, '1', lockKey, owner])
);
if (result === 0) {
throw new Error(`Failed to release lock ${key}`);
}
});
}
throw new Error(`Failed to acquire lock for resource [${key}]`);
}
}
@@ -6,7 +6,7 @@ import type { Request } from 'express';
import { GraphqlContext } from '../graphql';
import { retryable } from '../utils/promise';
import { Locker } from './local-lock';
import { Locker } from './locker';
export const MUTEX_RETRY = 5;
export const MUTEX_WAIT = 100;
@@ -26,7 +26,7 @@ export class Mutex {
* ```typescript
* {
* // lock is acquired here
* await using lock = await mutex.lock('resource-key');
* await using lock = await mutex.acquire('resource-key');
* if (lock) {
* // do something
* } else {
@@ -38,7 +38,7 @@ export class Mutex {
* @param key resource key
* @returns LockGuard
*/
async lock(key: string, owner: string = 'global') {
async acquire(key: string, owner: string = 'global') {
try {
return await retryable(
() => this.locker.lock(owner, key),
@@ -83,7 +83,7 @@ export class RequestMutex extends Mutex {
return id;
}
override lock(key: string) {
return super.lock(key, this.getId());
override acquire(key: string) {
return super.acquire(key, this.getId());
}
}