Skip to content

Commit 682a01e

Browse files
committed
feat(server): make a singleton global mutex service (#7900)
1 parent 6b0c398 commit 682a01e

File tree

6 files changed

+50
-42
lines changed

6 files changed

+50
-42
lines changed

packages/backend/server/src/core/selfhost/controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ActionForbidden,
66
EventEmitter,
77
InternalServerError,
8-
MutexService,
8+
Mutex,
99
PasswordRequired,
1010
} from '../../fundamentals';
1111
import { AuthService, Public } from '../auth';
@@ -23,7 +23,7 @@ export class CustomSetupController {
2323
private readonly user: UserService,
2424
private readonly auth: AuthService,
2525
private readonly event: EventEmitter,
26-
private readonly mutex: MutexService,
26+
private readonly mutex: Mutex,
2727
private readonly server: ServerService
2828
) {}
2929

packages/backend/server/src/core/workspaces/resolvers/workspace.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
InternalServerError,
2121
MailService,
2222
MemberQuotaExceeded,
23-
MutexService,
23+
RequestMutex,
2424
Throttle,
2525
TooManyRequest,
2626
UserNotFound,
@@ -57,7 +57,7 @@ export class WorkspaceResolver {
5757
private readonly users: UserService,
5858
private readonly event: EventEmitter,
5959
private readonly blobStorage: WorkspaceBlobStorage,
60-
private readonly mutex: MutexService
60+
private readonly mutex: RequestMutex
6161
) {}
6262

6363
@ResolveField(() => Permission, {

packages/backend/server/src/fundamentals/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type { GraphqlContext } from './graphql';
1919
export { CryptoHelper, URLHelper } from './helpers';
2020
export { MailService } from './mailer';
2121
export { CallCounter, CallTimer, metrics } from './metrics';
22-
export { type ILocker, Lock, Locker, MutexService } from './mutex';
22+
export { type ILocker, Lock, Locker, Mutex, RequestMutex } from './mutex';
2323
export {
2424
GatewayErrorWrapper,
2525
getOptionalModuleMetadata,
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Global, Module } from '@nestjs/common';
22

33
import { Locker } from './local-lock';
4-
import { MutexService } from './mutex';
4+
import { Mutex, RequestMutex } from './mutex';
55

66
@Global()
77
@Module({
8-
providers: [MutexService, Locker],
9-
exports: [MutexService],
8+
providers: [Mutex, RequestMutex, Locker],
9+
exports: [Mutex, RequestMutex],
1010
})
1111
export class MutexModule {}
1212

13-
export { Locker, MutexService };
13+
export { Locker, Mutex, RequestMutex };
1414
export { type Locker as ILocker, Lock } from './lock';

packages/backend/server/src/fundamentals/mutex/mutex.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,11 @@ import { Locker } from './local-lock';
1111
export const MUTEX_RETRY = 5;
1212
export const MUTEX_WAIT = 100;
1313

14-
@Injectable({ scope: Scope.REQUEST })
15-
export class MutexService {
16-
protected logger = new Logger(MutexService.name);
17-
private readonly locker: Locker;
18-
19-
constructor(
20-
@Inject(REQUEST) private readonly request: Request | GraphqlContext,
21-
private readonly ref: ModuleRef
22-
) {
23-
// nestjs will always find and injecting the locker from local module
24-
// so the RedisLocker implemented by the plugin mechanism will not be able to overwrite the internal locker
25-
// we need to use find and get the locker from the `ModuleRef` manually
26-
//
27-
// NOTE: when a `constructor` execute in normal service, the Locker module we expect may not have been initialized
28-
// but in the Service with `Scope.REQUEST`, we will create a separate Service instance for each request
29-
// at this time, all modules have been initialized, so we able to get the correct Locker instance in `constructor`
30-
this.locker = this.ref.get(Locker, { strict: false });
31-
}
14+
@Injectable()
15+
export class Mutex {
16+
protected logger = new Logger(Mutex.name);
3217

33-
protected getId() {
34-
const req = 'req' in this.request ? this.request.req : this.request;
35-
let id = req.headers['x-transaction-id'] as string;
36-
37-
if (!id) {
38-
id = randomUUID();
39-
req.headers['x-transaction-id'] = id;
40-
}
41-
42-
return id;
43-
}
18+
constructor(protected readonly locker: Locker) {}
4419

4520
/**
4621
* lock an resource and return a lock guard, which will release the lock when disposed
@@ -63,10 +38,10 @@ export class MutexService {
6338
* @param key resource key
6439
* @returns LockGuard
6540
*/
66-
async lock(key: string) {
41+
async lock(key: string, owner: string = 'global') {
6742
try {
6843
return await retryable(
69-
() => this.locker.lock(this.getId(), key),
44+
() => this.locker.lock(owner, key),
7045
MUTEX_RETRY,
7146
MUTEX_WAIT
7247
);
@@ -79,3 +54,36 @@ export class MutexService {
7954
}
8055
}
8156
}
57+
58+
@Injectable({ scope: Scope.REQUEST })
59+
export class RequestMutex extends Mutex {
60+
constructor(
61+
@Inject(REQUEST) private readonly request: Request | GraphqlContext,
62+
ref: ModuleRef
63+
) {
64+
// nestjs will always find and injecting the locker from local module
65+
// so the RedisLocker implemented by the plugin mechanism will not be able to overwrite the internal locker
66+
// we need to use find and get the locker from the `ModuleRef` manually
67+
//
68+
// NOTE: when a `constructor` execute in normal service, the Locker module we expect may not have been initialized
69+
// but in the Service with `Scope.REQUEST`, we will create a separate Service instance for each request
70+
// at this time, all modules have been initialized, so we able to get the correct Locker instance in `constructor`
71+
super(ref.get(Locker));
72+
}
73+
74+
protected getId() {
75+
const req = 'req' in this.request ? this.request.req : this.request;
76+
let id = req.headers['x-transaction-id'] as string;
77+
78+
if (!id) {
79+
id = randomUUID();
80+
req.headers['x-transaction-id'] = id;
81+
}
82+
83+
return id;
84+
}
85+
86+
override lock(key: string) {
87+
return super.lock(key, this.getId());
88+
}
89+
}

packages/backend/server/src/plugins/copilot/resolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { UserType } from '../../core/user';
2626
import {
2727
CopilotFailedToCreateMessage,
2828
FileUpload,
29-
MutexService,
29+
RequestMutex,
3030
Throttle,
3131
TooManyRequest,
3232
} from '../../fundamentals';
@@ -265,7 +265,7 @@ export class CopilotType {
265265
export class CopilotResolver {
266266
constructor(
267267
private readonly permissions: PermissionService,
268-
private readonly mutex: MutexService,
268+
private readonly mutex: RequestMutex,
269269
private readonly chatSession: ChatSessionService,
270270
private readonly storage: CopilotStorage
271271
) {}

0 commit comments

Comments
 (0)