Serverless function follow up (#9924)
- remove asynchronous serverless function build - build serverless function synchronously instead on activate workflow or execute - add a loader on workflow code step test tab test button - add a new `ServerlessFunctionSyncStatus` `BUILDING` - add a new route to build a serverless function draft version - delay artificially execution to avoid UI flashing https://github.com/user-attachments/assets/8d958d9a-ef41-4261-999e-6ea374191e33
This commit is contained in:
@ -0,0 +1,9 @@
|
||||
import { ID, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
@InputType()
|
||||
export class BuildDraftServerlessFunctionInput {
|
||||
@IDField(() => ID, { description: 'The id of the function.' })
|
||||
id!: string;
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { IsObject, IsOptional } from 'class-validator';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
export enum ServerlessFunctionExecutionStatus {
|
||||
IDLE = 'IDLE',
|
||||
SUCCESS = 'SUCCESS',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
||||
import {
|
||||
ServerlessFunctionEntity,
|
||||
ServerlessFunctionSyncStatus,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
export type BuildServerlessFunctionBatchEvent = {
|
||||
serverlessFunctions: {
|
||||
serverlessFunctionId: string;
|
||||
serverlessFunctionVersion: string;
|
||||
}[];
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@Processor({
|
||||
queueName: MessageQueue.serverlessFunctionQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class BuildServerlessFunctionJob {
|
||||
constructor(
|
||||
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||
private readonly serverlessService: ServerlessService,
|
||||
) {}
|
||||
|
||||
@Process(BuildServerlessFunctionJob.name)
|
||||
async handle(batchEvent: BuildServerlessFunctionBatchEvent): Promise<void> {
|
||||
for (const {
|
||||
serverlessFunctionId,
|
||||
serverlessFunctionVersion,
|
||||
} of batchEvent.serverlessFunctions) {
|
||||
const serverlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneBy({
|
||||
id: serverlessFunctionId,
|
||||
workspaceId: batchEvent.workspaceId,
|
||||
});
|
||||
|
||||
if (isDefined(serverlessFunction)) {
|
||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||
});
|
||||
await this.serverlessService.build(
|
||||
serverlessFunction,
|
||||
serverlessFunctionVersion,
|
||||
);
|
||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
|
||||
|
||||
export enum ServerlessFunctionSyncStatus {
|
||||
NOT_READY = 'NOT_READY',
|
||||
BUILDING = 'BUILDING',
|
||||
READY = 'READY',
|
||||
}
|
||||
|
||||
|
||||
@ -13,5 +13,6 @@ export enum ServerlessFunctionExceptionCode {
|
||||
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
||||
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
||||
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
||||
SERVERLESS_FUNCTION_BUILDING = 'SERVERLESS_FUNCTION_BUILDING',
|
||||
SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED = 'SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED',
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.mod
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { BuildServerlessFunctionJob } from 'src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -22,11 +21,7 @@ import { BuildServerlessFunctionJob } from 'src/engine/metadata-modules/serverle
|
||||
ThrottlerModule,
|
||||
AnalyticsModule,
|
||||
],
|
||||
providers: [
|
||||
ServerlessFunctionService,
|
||||
ServerlessFunctionResolver,
|
||||
BuildServerlessFunctionJob,
|
||||
],
|
||||
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
||||
exports: [ServerlessFunctionService],
|
||||
})
|
||||
export class ServerlessFunctionModule {}
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||
import { BuildDraftServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/build-draft-serverless-function.input';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@Resolver()
|
||||
@ -204,4 +205,22 @@ export class ServerlessFunctionResolver {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async buildDraftServerlessFunction(
|
||||
@Args('input') input: BuildDraftServerlessFunctionInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
try {
|
||||
await this.checkFeatureFlag(workspaceId);
|
||||
const { id } = input;
|
||||
|
||||
return await this.serverlessFunctionService.buildDraftServerlessFunction(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,10 +26,6 @@ import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/se
|
||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import {
|
||||
BuildServerlessFunctionBatchEvent,
|
||||
BuildServerlessFunctionJob,
|
||||
} from 'src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job';
|
||||
import {
|
||||
ServerlessFunctionEntity,
|
||||
ServerlessFunctionSyncStatus,
|
||||
@ -141,6 +137,16 @@ export class ServerlessFunctionService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
version === 'draft' &&
|
||||
functionToExecute.syncStatus !== ServerlessFunctionSyncStatus.READY
|
||||
) {
|
||||
await this.buildDraftServerlessFunction(
|
||||
functionToExecute.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const resultServerlessFunction = await this.serverlessService.execute(
|
||||
functionToExecute,
|
||||
payload,
|
||||
@ -276,12 +282,6 @@ export class ServerlessFunctionService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.buildServerlessFunction({
|
||||
serverlessFunctionId: existingServerlessFunction.id,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.serverlessFunctionRepository.findOneBy({
|
||||
id: existingServerlessFunction.id,
|
||||
});
|
||||
@ -322,6 +322,7 @@ export class ServerlessFunctionService {
|
||||
...serverlessFunctionInput,
|
||||
workspaceId,
|
||||
layerVersion: LAST_LAYER_VERSION,
|
||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||
});
|
||||
|
||||
const createdServerlessFunction =
|
||||
@ -341,12 +342,6 @@ export class ServerlessFunctionService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.buildServerlessFunction({
|
||||
serverlessFunctionId: createdServerlessFunction.id,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.serverlessFunctionRepository.findOneBy({
|
||||
id: createdServerlessFunction.id,
|
||||
});
|
||||
@ -361,6 +356,10 @@ export class ServerlessFunctionService {
|
||||
version: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (version === 'draft') {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverlessFunction = await this.findOneOrFail({
|
||||
id,
|
||||
workspaceId,
|
||||
@ -381,10 +380,8 @@ export class ServerlessFunctionService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.buildServerlessFunction({
|
||||
serverlessFunctionId: id,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
workspaceId,
|
||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||
});
|
||||
}
|
||||
|
||||
@ -403,24 +400,31 @@ export class ServerlessFunctionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async buildServerlessFunction({
|
||||
serverlessFunctionId,
|
||||
serverlessFunctionVersion,
|
||||
workspaceId,
|
||||
}: {
|
||||
serverlessFunctionId: string;
|
||||
serverlessFunctionVersion: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
await this.messageQueueService.add<BuildServerlessFunctionBatchEvent>(
|
||||
BuildServerlessFunctionJob.name,
|
||||
{
|
||||
serverlessFunctions: [
|
||||
{ serverlessFunctionId, serverlessFunctionVersion },
|
||||
],
|
||||
workspaceId,
|
||||
},
|
||||
{ id: `${serverlessFunctionId}-${serverlessFunctionVersion}` },
|
||||
);
|
||||
async buildDraftServerlessFunction(id: string, workspaceId: string) {
|
||||
const functionToBuild = await this.findOneOrFail({
|
||||
id,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (functionToBuild.syncStatus === ServerlessFunctionSyncStatus.READY) {
|
||||
return functionToBuild;
|
||||
}
|
||||
|
||||
if (functionToBuild.syncStatus === ServerlessFunctionSyncStatus.BUILDING) {
|
||||
throw new ServerlessFunctionException(
|
||||
'This function is currently building. Please try later',
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING,
|
||||
);
|
||||
}
|
||||
|
||||
await this.serverlessFunctionRepository.update(functionToBuild.id, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.BUILDING,
|
||||
});
|
||||
await this.serverlessService.build(functionToBuild, 'draft');
|
||||
await this.serverlessFunctionRepository.update(functionToBuild.id, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||
});
|
||||
|
||||
return functionToBuild;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
||||
throw new ConflictError(error.message);
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING:
|
||||
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
|
||||
throw new ForbiddenError(error.message);
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user