Skip to content

Commit 9f69383

Browse files
authored
Add function execution throttler (#6742)
Add throttler service to limit the number of function execution
1 parent 81fa3f0 commit 9f69383

13 files changed

+122
-40
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { CustomException } from 'src/utils/custom-exception';
2+
3+
export class ThrottlerException extends CustomException {
4+
code: ThrottlerExceptionCode;
5+
constructor(message: string, code: ThrottlerExceptionCode) {
6+
super(message, code);
7+
}
8+
}
9+
10+
export enum ThrottlerExceptionCode {
11+
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
4+
5+
@Module({
6+
imports: [],
7+
providers: [ThrottlerService],
8+
exports: [ThrottlerService],
9+
})
10+
export class ThrottlerModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
import {
4+
ThrottlerException,
5+
ThrottlerExceptionCode,
6+
} from 'src/engine/core-modules/throttler/throttler.exception';
7+
import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service';
8+
import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator';
9+
import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum';
10+
11+
@Injectable()
12+
export class ThrottlerService {
13+
constructor(
14+
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
15+
private readonly cacheStorage: CacheStorageService,
16+
) {}
17+
18+
async throttle(key: string, limit: number, ttl: number): Promise<void> {
19+
const currentCount = (await this.cacheStorage.get<number>(key)) ?? 0;
20+
21+
if (currentCount >= limit) {
22+
throw new ThrottlerException(
23+
'Too many requests',
24+
ThrottlerExceptionCode.TOO_MANY_REQUESTS,
25+
);
26+
}
27+
28+
await this.cacheStorage.set(key, currentCount + 1, ttl);
29+
}
30+
}

packages/twenty-server/src/engine/integrations/environment/environment-variables.ts

+7
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,13 @@ export class EnvironmentVariables {
421421
AUTH_GOOGLE_APIS_CALLBACK_URL: string;
422422

423423
CHROME_EXTENSION_ID: string;
424+
425+
@CastToPositiveNumber()
426+
SERVERLESS_FUNCTION_EXEC_THROTTLE_LIMIT = 10;
427+
428+
// milliseconds
429+
@CastToPositiveNumber()
430+
SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL = 1000;
424431
}
425432

426433
export const validate = (

packages/twenty-server/src/engine/integrations/integrations.module.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@ import { Module } from '@nestjs/common';
22
import { HttpAdapterHost } from '@nestjs/core';
33
import { EventEmitterModule } from '@nestjs/event-emitter';
44

5-
import { ExceptionHandlerModule } from 'src/engine/integrations/exception-handler/exception-handler.module';
6-
import { exceptionHandlerModuleFactory } from 'src/engine/integrations/exception-handler/exception-handler.module-factory';
7-
import { fileStorageModuleFactory } from 'src/engine/integrations/file-storage/file-storage.module-factory';
8-
import { loggerModuleFactory } from 'src/engine/integrations/logger/logger.module-factory';
9-
import { messageQueueModuleFactory } from 'src/engine/integrations/message-queue/message-queue.module-factory';
10-
import { EmailModule } from 'src/engine/integrations/email/email.module';
11-
import { emailModuleFactory } from 'src/engine/integrations/email/email.module-factory';
125
import { CacheStorageModule } from 'src/engine/integrations/cache-storage/cache-storage.module';
136
import { CaptchaModule } from 'src/engine/integrations/captcha/captcha.module';
147
import { captchaModuleFactory } from 'src/engine/integrations/captcha/captcha.module-factory';
8+
import { EmailModule } from 'src/engine/integrations/email/email.module';
9+
import { emailModuleFactory } from 'src/engine/integrations/email/email.module-factory';
10+
import { ExceptionHandlerModule } from 'src/engine/integrations/exception-handler/exception-handler.module';
11+
import { exceptionHandlerModuleFactory } from 'src/engine/integrations/exception-handler/exception-handler.module-factory';
12+
import { fileStorageModuleFactory } from 'src/engine/integrations/file-storage/file-storage.module-factory';
13+
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
1514
import { LLMChatModelModule } from 'src/engine/integrations/llm-chat-model/llm-chat-model.module';
1615
import { llmChatModelModuleFactory } from 'src/engine/integrations/llm-chat-model/llm-chat-model.module-factory';
1716
import { LLMTracingModule } from 'src/engine/integrations/llm-tracing/llm-tracing.module';
1817
import { llmTracingModuleFactory } from 'src/engine/integrations/llm-tracing/llm-tracing.module-factory';
19-
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
20-
import { serverlessModuleFactory } from 'src/engine/integrations/serverless/serverless-module.factory';
21-
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
18+
import { loggerModuleFactory } from 'src/engine/integrations/logger/logger.module-factory';
19+
import { messageQueueModuleFactory } from 'src/engine/integrations/message-queue/message-queue.module-factory';
2220
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
21+
import { serverlessModuleFactory } from 'src/engine/integrations/serverless/serverless-module.factory';
22+
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
2323

2424
import { EnvironmentModule } from './environment/environment.module';
2525
import { EnvironmentService } from './environment/environment.service';

packages/twenty-server/src/engine/integrations/serverless/drivers/lambda.driver.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import fs from 'fs';
22

33
import {
44
CreateFunctionCommand,
5+
DeleteFunctionCommand,
6+
GetFunctionCommand,
7+
InvokeCommand,
58
Lambda,
69
LambdaClientConfig,
7-
InvokeCommand,
8-
GetFunctionCommand,
9-
UpdateFunctionCodeCommand,
10-
DeleteFunctionCommand,
10+
PublishVersionCommand,
11+
PublishVersionCommandInput,
1112
ResourceNotFoundException,
13+
UpdateFunctionCodeCommand,
1214
waitUntilFunctionUpdatedV2,
13-
PublishVersionCommandInput,
14-
PublishVersionCommand,
1515
} from '@aws-sdk/client-lambda';
1616
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
1717
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
@@ -21,12 +21,12 @@ import {
2121
ServerlessExecuteResult,
2222
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
2323

24-
import { createZipFile } from 'src/engine/integrations/serverless/drivers/utils/create-zip-file';
25-
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
2624
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
2725
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
2826
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
27+
import { createZipFile } from 'src/engine/integrations/serverless/drivers/utils/create-zip-file';
2928
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
29+
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
3030
import {
3131
ServerlessFunctionException,
3232
ServerlessFunctionExceptionCode,

packages/twenty-server/src/engine/integrations/serverless/drivers/local.driver.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
/* eslint-disable no-console */
2-
import { join } from 'path';
3-
import { tmpdir } from 'os';
4-
import { promises as fs } from 'fs';
51
import { fork } from 'child_process';
2+
import { promises as fs } from 'fs';
3+
import { tmpdir } from 'os';
4+
import { join } from 'path';
65

76
import { v4 } from 'uuid';
87

8+
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
99
import {
1010
ServerlessDriver,
1111
ServerlessExecuteError,
1212
ServerlessExecuteResult,
1313
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
14-
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
1514

1615
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
1716
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
18-
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
19-
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
2017
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
18+
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
19+
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
2120
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
21+
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
2222
import {
2323
ServerlessFunctionException,
2424
ServerlessFunctionExceptionCode,
2525
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
26-
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
2726

2827
export interface LocalDriverOptions {
2928
fileStorageService: FileStorageService;

packages/twenty-server/src/engine/integrations/serverless/serverless-module.factory.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
22

3-
import {
4-
ServerlessModuleOptions,
5-
ServerlessDriverType,
6-
} from 'src/engine/integrations/serverless/serverless.interface';
73
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
84
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
95
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
6+
import {
7+
ServerlessDriverType,
8+
ServerlessModuleOptions,
9+
} from 'src/engine/integrations/serverless/serverless.interface';
1010

1111
export const serverlessModuleFactory = async (
1212
environmentService: EnvironmentService,

packages/twenty-server/src/engine/integrations/serverless/serverless.module.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { DynamicModule, Global } from '@nestjs/common';
22

3+
import { LambdaDriver } from 'src/engine/integrations/serverless/drivers/lambda.driver';
4+
import { LocalDriver } from 'src/engine/integrations/serverless/drivers/local.driver';
5+
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
6+
import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants';
37
import {
48
ServerlessDriverType,
59
ServerlessModuleAsyncOptions,
610
} from 'src/engine/integrations/serverless/serverless.interface';
711
import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service';
8-
import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants';
9-
import { LocalDriver } from 'src/engine/integrations/serverless/drivers/local.driver';
10-
import { LambdaDriver } from 'src/engine/integrations/serverless/drivers/lambda.driver';
11-
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
1212

1313
@Global()
1414
export class ServerlessModule {

packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export enum ServerlessFunctionExceptionCode {
1313
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
1414
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
1515
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
16+
SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED = 'SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED',
1617
}

packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
1111
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
1212
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
1313
import { FileModule } from 'src/engine/core-modules/file/file.module';
14+
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
1415
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
1516
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
1617
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
@@ -29,6 +30,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
2930
),
3031
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
3132
FileModule,
33+
ThrottlerModule,
3234
],
3335
services: [ServerlessFunctionService],
3436
resolvers: [

packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
55
import { FileUpload } from 'graphql-upload';
66
import { Repository } from 'typeorm';
77

8-
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
98
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
9+
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
1010

11+
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
12+
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
1113
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
1214
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
1315
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
1416
import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service';
17+
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
1518
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
1619
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
1720
import {
@@ -24,7 +27,6 @@ import {
2427
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
2528
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
2629
import { isDefined } from 'src/utils/is-defined';
27-
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
2830

2931
@Injectable()
3032
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
@@ -33,6 +35,8 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
3335
private readonly serverlessService: ServerlessService,
3436
@InjectRepository(ServerlessFunctionEntity, 'metadata')
3537
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
38+
private readonly throttlerService: ThrottlerService,
39+
private readonly environmentService: EnvironmentService,
3640
) {
3741
super(serverlessFunctionRepository);
3842
}
@@ -86,6 +90,8 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
8690
payload: object | undefined = undefined,
8791
version = 'latest',
8892
): Promise<ServerlessExecuteResult> {
93+
await this.throttleExecution(workspaceId);
94+
8995
const functionToExecute = await this.serverlessFunctionRepository.findOne({
9096
where: {
9197
id,
@@ -268,4 +274,19 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
268274

269275
return await this.findById(createdServerlessFunction.id);
270276
}
277+
278+
private async throttleExecution(workspaceId: string) {
279+
try {
280+
await this.throttlerService.throttle(
281+
`${workspaceId}-serverless-function-execution`,
282+
this.environmentService.get('SERVERLESS_FUNCTION_EXEC_THROTTLE_LIMIT'),
283+
this.environmentService.get('SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL'),
284+
);
285+
} catch (error) {
286+
throw new ServerlessFunctionException(
287+
'Serverless function execution rate limit exceeded',
288+
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED,
289+
);
290+
}
291+
}
271292
}

packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import {
2-
ServerlessFunctionException,
3-
ServerlessFunctionExceptionCode,
4-
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
51
import {
62
ConflictError,
73
ForbiddenError,
84
InternalServerError,
95
NotFoundError,
106
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
7+
import {
8+
ServerlessFunctionException,
9+
ServerlessFunctionExceptionCode,
10+
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
1111

1212
export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
1313
if (error instanceof ServerlessFunctionException) {

0 commit comments

Comments
 (0)