Serverless function timeout concerns (#9689)

closes https://github.com/twentyhq/core-team-issues/issues/242
- unify timeout behavior between local and lambda
- add timeout in serverless entity
- set timeout default to 300s (5min)
This commit is contained in:
martmull
2025-01-17 14:49:02 +01:00
committed by GitHub
parent 679429447d
commit f8f9bb2b78
14 changed files with 100 additions and 15 deletions

View File

@ -237,7 +237,7 @@ export class LambdaDriver implements ServerlessDriver {
Role: this.lambdaRole,
Runtime: serverlessFunction.runtime,
Description: 'Lambda function to run user script',
Timeout: 900,
Timeout: serverlessFunction.timeoutSeconds,
};
const command = new CreateFunctionCommand(params);
@ -259,6 +259,7 @@ export class LambdaDriver implements ServerlessDriver {
Variables: envVariables,
},
FunctionName: serverlessFunction.id,
Timeout: serverlessFunction.timeoutSeconds,
};
const updateConfigurationCommand = new UpdateFunctionConfigurationCommand(

View File

@ -183,7 +183,17 @@ export class LocalDriver implements ServerlessDriver {
return await new Promise((resolve, reject) => {
const child = fork(listenerFile, { silent: true });
const timeoutMs = serverlessFunction.timeoutSeconds * 1_000;
const timeoutHandler = setTimeout(() => {
child.kill();
const duration = Date.now() - startTime;
reject(new Error(`Task timed out after ${duration / 1_000} seconds`));
}, timeoutMs);
child.on('message', (message: object | ServerlessExecuteError) => {
clearTimeout(timeoutHandler);
const duration = Date.now() - startTime;
if ('errorType' in message) {
@ -204,6 +214,7 @@ export class LocalDriver implements ServerlessDriver {
});
child.stderr?.on('data', (data) => {
clearTimeout(timeoutHandler);
const stackTrace = data
.toString()
.split('\n')
@ -235,11 +246,13 @@ export class LocalDriver implements ServerlessDriver {
});
child.on('error', (error) => {
clearTimeout(timeoutHandler);
reject(error);
child.kill();
});
child.on('exit', (code) => {
clearTimeout(timeoutHandler);
if (code && code !== 0) {
reject(new Error(`Child process exited with code ${code}`));
}

View File

@ -1,6 +1,13 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
@InputType()
export class CreateServerlessFunctionInput {
@ -13,4 +20,11 @@ export class CreateServerlessFunctionInput {
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsNumber()
@Field({ nullable: true })
@Min(1)
@Max(900)
@IsOptional()
timeoutSeconds?: number;
}

View File

@ -15,6 +15,7 @@ import {
IsDateString,
IsEnum,
IsNotEmpty,
IsNumber,
IsString,
IsUUID,
} from 'class-validator';
@ -58,6 +59,10 @@ export class ServerlessFunctionDTO {
@Field()
runtime: string;
@IsNumber()
@Field()
timeoutSeconds: number;
@IsString()
@Field({ nullable: true })
latestVersion: string;

View File

@ -1,6 +1,15 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsString, IsUUID } from 'class-validator';
import {
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
IsUUID,
Max,
Min,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ -20,8 +29,16 @@ export class UpdateServerlessFunctionInput {
@IsString()
@Field({ nullable: true })
@IsOptional()
description?: string;
@IsNumber()
@Field({ nullable: true })
@Min(1)
@Max(900)
@IsOptional()
timeoutSeconds?: number;
@Field(() => graphqlTypeJson)
@IsObject()
code: JSON;

View File

@ -1,4 +1,5 @@
import {
Check,
Column,
CreateDateColumn,
Entity,
@ -8,6 +9,8 @@ import {
import { InputSchema } from 'src/modules/workflow/workflow-builder/types/input-schema.type';
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
export enum ServerlessFunctionSyncStatus {
NOT_READY = 'NOT_READY',
READY = 'READY',
@ -40,6 +43,10 @@ export class ServerlessFunctionEntity {
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
runtime: ServerlessFunctionRuntime;
@Column({ nullable: false, default: DEFAULT_SERVERLESS_TIMEOUT_SECONDS })
@Check(`"timeoutSeconds" >= 1 AND "timeoutSeconds" <= 900`)
timeoutSeconds: number;
@Column({ nullable: true })
layerVersion: number;

View File

@ -159,10 +159,7 @@ export class ServerlessFunctionResolver {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.createOneServerlessFunction(
{
name: input.name,
description: input.description,
},
input,
workspaceId,
);
} catch (error) {

View File

@ -273,6 +273,7 @@ export class ServerlessFunctionService {
name: serverlessFunctionInput.name,
description: serverlessFunctionInput.description,
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
timeoutSeconds: serverlessFunctionInput.timeoutSeconds,
},
);
@ -393,6 +394,7 @@ export class ServerlessFunctionService {
{
name: serverlessFunctionToCopy?.name,
description: serverlessFunctionToCopy?.description,
timeoutSeconds: serverlessFunctionToCopy?.timeoutSeconds,
workspaceId,
layerVersion: LAST_LAYER_VERSION,
},