mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 03:26:47 +08:00
chore: rename fundamentals to base (#9119)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { Locker } from './local-lock';
|
||||
import { Mutex, RequestMutex } from './mutex';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [Mutex, RequestMutex, Locker],
|
||||
exports: [Mutex, RequestMutex],
|
||||
})
|
||||
export class MutexModule {}
|
||||
|
||||
export { Locker, Mutex, RequestMutex };
|
||||
export { type Locker as ILocker, Lock } from './lock';
|
||||
@@ -0,0 +1,28 @@
|
||||
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}]`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { retryable } from '../utils/promise';
|
||||
|
||||
export class Lock implements AsyncDisposable {
|
||||
private readonly logger = new Logger(Lock.name);
|
||||
|
||||
constructor(private readonly dispose: () => Promise<void>) {}
|
||||
|
||||
async release() {
|
||||
await retryable(() => this.dispose()).catch(e => {
|
||||
this.logger.error('Failed to release lock', e);
|
||||
});
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.release();
|
||||
}
|
||||
}
|
||||
|
||||
export interface Locker {
|
||||
lock(owner: string, key: string): Promise<Lock>;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { Inject, Injectable, Logger, Scope } from '@nestjs/common';
|
||||
import { ModuleRef, REQUEST } from '@nestjs/core';
|
||||
import type { Request } from 'express';
|
||||
|
||||
import { GraphqlContext } from '../graphql';
|
||||
import { retryable } from '../utils/promise';
|
||||
import { Locker } from './local-lock';
|
||||
|
||||
export const MUTEX_RETRY = 5;
|
||||
export const MUTEX_WAIT = 100;
|
||||
|
||||
@Injectable()
|
||||
export class Mutex {
|
||||
protected logger = new Logger(Mutex.name);
|
||||
|
||||
constructor(protected readonly locker: Locker) {}
|
||||
|
||||
/**
|
||||
* lock an resource and return a lock guard, which will release the lock when disposed
|
||||
*
|
||||
* if the lock is not available, it will retry for [MUTEX_RETRY] times
|
||||
*
|
||||
* usage:
|
||||
* ```typescript
|
||||
* {
|
||||
* // lock is acquired here
|
||||
* await using lock = await mutex.lock('resource-key');
|
||||
* if (lock) {
|
||||
* // do something
|
||||
* } else {
|
||||
* // failed to lock
|
||||
* }
|
||||
* }
|
||||
* // lock is released here
|
||||
* ```
|
||||
* @param key resource key
|
||||
* @returns LockGuard
|
||||
*/
|
||||
async lock(key: string, owner: string = 'global') {
|
||||
try {
|
||||
return await retryable(
|
||||
() => this.locker.lock(owner, key),
|
||||
MUTEX_RETRY,
|
||||
MUTEX_WAIT
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Failed to lock resource [${key}] after retry ${MUTEX_RETRY} times`,
|
||||
e
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class RequestMutex extends Mutex {
|
||||
constructor(
|
||||
@Inject(REQUEST) private readonly request: Request | GraphqlContext,
|
||||
ref: ModuleRef
|
||||
) {
|
||||
// nestjs will always find and injecting the locker from local module
|
||||
// so the RedisLocker implemented by the plugin mechanism will not be able to overwrite the internal locker
|
||||
// we need to use find and get the locker from the `ModuleRef` manually
|
||||
//
|
||||
// NOTE: when a `constructor` execute in normal service, the Locker module we expect may not have been initialized
|
||||
// but in the Service with `Scope.REQUEST`, we will create a separate Service instance for each request
|
||||
// at this time, all modules have been initialized, so we able to get the correct Locker instance in `constructor`
|
||||
super(ref.get(Locker));
|
||||
}
|
||||
|
||||
protected getId() {
|
||||
const req = 'req' in this.request ? this.request.req : this.request;
|
||||
let id = req.headers['x-transaction-id'] as string;
|
||||
|
||||
if (!id) {
|
||||
id = randomUUID();
|
||||
req.headers['x-transaction-id'] = id;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
override lock(key: string) {
|
||||
return super.lock(key, this.getId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user