Add workflow runner (#6458)

- add workflow runner module
- add an endpoint to trigger a workflow via api
- improve error handling for serverless functions

## Testing
- create 2 serverless functions
- create a workflow
- create this workflow Version
```
{
  "type": "MANUAL",
  "input": {"b": "bb"},
  "nextAction": {
    "name": "step_1",
    "displayName": "Code",
    "type": "CODE",
    "valid": true,
    "settings": {
      "serverlessFunctionId": "Serverless function 1 Id",
      "errorHandlingOptions": {
        "retryOnFailure": {
          "value": false
        },
        "continueOnFailure": {
          "value": false
        }
      }
    },
    "nextAction": {
      "name": "step_1",
      "displayName": "Code",
      "type": "CODE",
      "valid": true,
      "settings": {
        "serverlessFunctionId": "Serverless function 1 Id",
        "errorHandlingOptions": {
          "retryOnFailure": {
            "value": false
          },
          "continueOnFailure": {
            "value": false
          }
        }
      },
      "nextAction": {
        "name": "step_1",
        "displayName": "Code",
        "type": "CODE",
        "valid": true,
        "settings": {
          "serverlessFunctionId": "Serverless function 2 Id",
          "errorHandlingOptions": {
            "retryOnFailure": {
              "value": false
            },
            "continueOnFailure": {
              "value": false
            }
          }
        }
      }
    }
  }
}

`
``
- call 
```
mutation Trigger {
  triggerWorkflow(workflowVersionId: "WORKFLOW_VERSION_ID") {
    result
  }
}
```
- try when errors are injected in serverless function
This commit is contained in:
martmull
2024-07-31 12:48:33 +02:00
committed by GitHub
parent b8496d22b6
commit 6b4c79ff0d
42 changed files with 639 additions and 150 deletions

View File

@ -16,4 +16,5 @@ export enum MessageQueue {
recordPositionBackfillQueue = 'record-position-backfill-queue',
entityEventsToDbQueue = 'entity-events-to-db-queue',
testQueue = 'test-queue',
workflowQueue = 'workflow-queue',
}

View File

@ -1,4 +1,18 @@
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
export type ServerlessExecuteError = {
errorType: string;
errorMessage: string;
stackTrace: string;
};
export type ServerlessExecuteResult = {
data: object | null;
duration: number;
status: ServerlessFunctionExecutionStatus;
error?: ServerlessExecuteError;
};
export interface ServerlessDriver {
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
@ -6,5 +20,5 @@ export interface ServerlessDriver {
execute(
serverlessFunction: ServerlessFunctionEntity,
payload: object | undefined,
): Promise<object>;
): Promise<ServerlessExecuteResult>;
}

View File

@ -13,13 +13,17 @@ import {
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import {
ServerlessDriver,
ServerlessExecuteResult,
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import { createZipFile } from 'src/engine/integrations/serverless/drivers/utils/create-zip-file';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
export interface LambdaDriverOptions extends LambdaClientConfig {
fileStorageService: FileStorageService;
@ -134,7 +138,8 @@ export class LambdaDriver
async execute(
functionToExecute: ServerlessFunctionEntity,
payload: object | undefined = undefined,
): Promise<object> {
): Promise<ServerlessExecuteResult> {
const startTime = Date.now();
const params = {
FunctionName: functionToExecute.id,
Payload: JSON.stringify(payload),
@ -144,10 +149,25 @@ export class LambdaDriver
const result = await this.lambdaClient.send(command);
if (!result.Payload) {
return {};
const parsedResult = result.Payload
? JSON.parse(result.Payload.transformToString())
: {};
const duration = Date.now() - startTime;
if (result.FunctionError) {
return {
data: null,
duration,
status: ServerlessFunctionExecutionStatus.ERROR,
error: parsedResult,
};
}
return JSON.parse(result.Payload.transformToString());
return {
data: parsedResult,
duration,
status: ServerlessFunctionExecutionStatus.SUCCESS,
};
}
}

View File

@ -5,13 +5,18 @@ import { fork } from 'child_process';
import { v4 } from 'uuid';
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import {
ServerlessDriver,
ServerlessExecuteError,
ServerlessExecuteResult,
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
export interface LocalDriverOptions {
fileStorageService: FileStorageService;
@ -51,7 +56,8 @@ export class LocalDriver
async execute(
serverlessFunction: ServerlessFunctionEntity,
payload: object | undefined = undefined,
): Promise<object> {
): Promise<ServerlessExecuteResult> {
const startTime = Date.now();
const fileStream = await this.fileStorageService.read({
folderPath: this.getFolderPath(serverlessFunction),
filename: BUILD_FILE_NAME,
@ -83,8 +89,23 @@ export class LocalDriver
return await new Promise((resolve, reject) => {
const child = fork(tmpFilePath, { silent: true });
child.on('message', (message: object) => {
resolve(message);
child.on('message', (message: object | ServerlessExecuteError) => {
const duration = Date.now() - startTime;
if ('errorType' in message) {
resolve({
data: null,
duration,
error: message,
status: ServerlessFunctionExecutionStatus.ERROR,
});
} else {
resolve({
data: message,
duration,
status: ServerlessFunctionExecutionStatus.SUCCESS,
});
}
child.kill();
fs.unlink(tmpFilePath);
});
@ -93,8 +114,8 @@ export class LocalDriver
const stackTrace = data
.toString()
.split('\n')
.filter((line) => line.trim() !== '');
const errorTrace = stackTrace.filter((line) =>
.filter((line: string) => line.trim() !== '');
const errorTrace = stackTrace.filter((line: string) =>
line.includes('Error: '),
)?.[0];
@ -105,11 +126,17 @@ export class LocalDriver
errorType = errorTrace.split(':')[0];
errorMessage = errorTrace.split(': ')[1];
}
const duration = Date.now() - startTime;
resolve({
errorType,
errorMessage,
stackTrace: stackTrace,
data: null,
duration,
status: ServerlessFunctionExecutionStatus.ERROR,
error: {
errorType,
errorMessage,
stackTrace: stackTrace,
},
});
child.kill();
fs.unlink(tmpFilePath);

View File

@ -1,6 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import {
ServerlessDriver,
ServerlessExecuteResult,
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@ -20,7 +23,7 @@ export class ServerlessService implements ServerlessDriver {
async execute(
serverlessFunction: ServerlessFunctionEntity,
payload: object | undefined = undefined,
) {
): Promise<ServerlessExecuteResult> {
return this.driver.execute(serverlessFunction, payload);
}
}