mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 09:36:17 +08:00
refactor(server): make redis required module (#9121)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user