mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
fix(server): inject correct locker to request scope mutex (#6140)
This commit is contained in:
@@ -1,27 +1,14 @@
|
||||
import { Global, Provider, Type } from '@nestjs/common';
|
||||
import { CONTEXT } from '@nestjs/graphql';
|
||||
import { Redis, type RedisOptions } from 'ioredis';
|
||||
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
|
||||
|
||||
import {
|
||||
BucketService,
|
||||
Cache,
|
||||
type GraphqlContext,
|
||||
MutexService,
|
||||
SessionCache,
|
||||
} from '../../fundamentals';
|
||||
import { Cache, Locker, SessionCache } from '../../fundamentals';
|
||||
import { ThrottlerStorage } from '../../fundamentals/throttler';
|
||||
import { SocketIoAdapterImpl } from '../../fundamentals/websocket';
|
||||
import { Plugin } from '../registry';
|
||||
import { RedisCache } from './cache';
|
||||
import {
|
||||
CacheRedis,
|
||||
MutexRedis,
|
||||
SessionRedis,
|
||||
SocketIoRedis,
|
||||
ThrottlerRedis,
|
||||
} from './instances';
|
||||
import { MutexRedisService } from './mutex';
|
||||
import { CacheRedis, SessionRedis, SocketIoRedis } from './instances';
|
||||
import { RedisMutexLocker } from './mutex';
|
||||
import { createSockerIoAdapterImpl } from './ws-adapter';
|
||||
|
||||
function makeProvider(token: Type, impl: Type<Redis>): Provider {
|
||||
@@ -44,7 +31,7 @@ const throttlerStorageProvider: Provider = {
|
||||
useFactory: (redis: Redis) => {
|
||||
return new ThrottlerStorageRedisService(redis);
|
||||
},
|
||||
inject: [ThrottlerRedis],
|
||||
inject: [SessionRedis],
|
||||
};
|
||||
|
||||
// socket io
|
||||
@@ -58,23 +45,14 @@ const socketIoRedisAdapterProvider: Provider = {
|
||||
|
||||
// mutex
|
||||
const mutexRedisAdapterProvider: Provider = {
|
||||
provide: MutexService,
|
||||
useFactory: (redis: Redis, ctx: GraphqlContext, bucket: BucketService) => {
|
||||
return new MutexRedisService(redis, ctx, bucket);
|
||||
},
|
||||
inject: [MutexRedis, CONTEXT, BucketService],
|
||||
provide: Locker,
|
||||
useClass: RedisMutexLocker,
|
||||
};
|
||||
|
||||
@Global()
|
||||
@Plugin({
|
||||
name: 'redis',
|
||||
providers: [
|
||||
CacheRedis,
|
||||
SessionRedis,
|
||||
ThrottlerRedis,
|
||||
SocketIoRedis,
|
||||
MutexRedis,
|
||||
],
|
||||
providers: [CacheRedis, SessionRedis, SocketIoRedis],
|
||||
overrides: [
|
||||
cacheProvider,
|
||||
sessionCacheProvider,
|
||||
|
||||
@@ -34,13 +34,6 @@ export class CacheRedis extends Redis {
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ThrottlerRedis extends Redis {
|
||||
constructor(config: Config) {
|
||||
super({ ...config.plugins.redis, db: (config.plugins.redis?.db ?? 0) + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionRedis extends Redis {
|
||||
constructor(config: Config) {
|
||||
@@ -54,10 +47,3 @@ export class SocketIoRedis extends Redis {
|
||||
super({ ...config.plugins.redis, db: (config.plugins.redis?.db ?? 0) + 3 });
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MutexRedis extends Redis {
|
||||
constructor(config: Config) {
|
||||
super({ ...config.plugins.redis, db: (config.plugins.redis?.db ?? 0) + 4 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import Redis, { Command } from 'ioredis';
|
||||
import { Command } from 'ioredis';
|
||||
|
||||
import {
|
||||
BucketService,
|
||||
type GraphqlContext,
|
||||
LockGuard,
|
||||
MUTEX_RETRY,
|
||||
MUTEX_WAIT,
|
||||
MutexService,
|
||||
} from '../../fundamentals';
|
||||
import { ILocker, Lock } from '../../fundamentals';
|
||||
import { SessionRedis } from './instances';
|
||||
|
||||
const lockScript = `local key = KEYS[1]
|
||||
local clientId = ARGV[1]
|
||||
local releaseTime = ARGV[2]
|
||||
|
||||
if redis.call("get", key) == clientId or redis.call("set", key, clientId, "NX", "PX", releaseTime) then
|
||||
if redis.call("get", key) == clientId or redis.call("set", key, clientId, "NX", "EX", 60) then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
@@ -31,66 +22,31 @@ else
|
||||
end`;
|
||||
|
||||
@Injectable()
|
||||
export class MutexRedisService extends MutexService {
|
||||
constructor(
|
||||
private readonly redis: Redis,
|
||||
context: GraphqlContext,
|
||||
bucket: BucketService
|
||||
) {
|
||||
super(context, bucket);
|
||||
this.logger = new Logger(MutexRedisService.name);
|
||||
}
|
||||
export class RedisMutexLocker implements ILocker {
|
||||
private readonly logger = new Logger(RedisMutexLocker.name);
|
||||
constructor(private readonly redis: SessionRedis) {}
|
||||
|
||||
override async lock(
|
||||
key: string,
|
||||
releaseTimeInMS: number = 200
|
||||
): Promise<LockGuard | undefined> {
|
||||
const clientId = this.getId();
|
||||
this.logger.debug(`Client ${clientId} lock try to lock ${key}`);
|
||||
const releaseTime = releaseTimeInMS.toString();
|
||||
async lock(owner: string, key: string): Promise<Lock> {
|
||||
const lockKey = `MutexLock:${key}`;
|
||||
this.logger.debug(`Client ${owner} is trying to lock resource ${key}`);
|
||||
|
||||
const fetchLock = async (retry: number): Promise<LockGuard | undefined> => {
|
||||
if (retry === 0) {
|
||||
this.logger.error(
|
||||
`Failed to fetch lock ${key} after ${MUTEX_RETRY} retry`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const success = await this.redis.sendCommand(
|
||||
new Command('EVAL', [lockScript, '1', key, clientId, releaseTime])
|
||||
);
|
||||
if (success === 1) {
|
||||
return new LockGuard(this, key);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Failed to fetch lock ${key}, retrying in ${MUTEX_WAIT} ms`
|
||||
);
|
||||
await setTimeout(MUTEX_WAIT * (MUTEX_RETRY - retry + 1));
|
||||
return fetchLock(retry - 1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Unexpected error when fetch lock ${key}: ${error.message}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return fetchLock(MUTEX_RETRY);
|
||||
}
|
||||
|
||||
override async unlock(key: string, ignoreUnlockFail = false): Promise<void> {
|
||||
const clientId = this.getId();
|
||||
const result = await this.redis.sendCommand(
|
||||
new Command('EVAL', [unlockScript, '1', key, clientId])
|
||||
const success = await this.redis.sendCommand(
|
||||
new Command('EVAL', [lockScript, '1', lockKey, owner])
|
||||
);
|
||||
if (result === 0) {
|
||||
if (!ignoreUnlockFail) {
|
||||
throw new Error(`Failed to release lock ${key}`);
|
||||
} else {
|
||||
this.logger.warn(`Failed to release lock ${key}`);
|
||||
}
|
||||
|
||||
if (success === 1) {
|
||||
return new Lock(async () => {
|
||||
const result = await this.redis.sendCommand(
|
||||
new Command('EVAL', [unlockScript, '1', lockKey, owner])
|
||||
);
|
||||
|
||||
// TODO(@darksky): lock expired condition is not handled
|
||||
if (result === 0) {
|
||||
throw new Error(`Failed to release lock ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Failed to acquire lock for resource [${key}]`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user