From a82d07890600ebab33f4586365e946a808e3d6c5 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:56:08 +0200 Subject: [PATCH] Lambda build update instead of delete existing logic function while building (#19116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Introduction Avoid having a time window where the logic function is unavailable while being built, by implementing an update flow instead of delete and create everytime fixes https://twenty-v7.sentry.io/issues/7371110591/ **Note: executor code propagation (pre-existing limitation)** The Lambda executor shim (`logic-function-drivers/constants/executor/index.mjs`) is only deployed when a Lambda is first created. If this file is edited, the change won't propagate to existing Lambdas — neither before nor after this PR. A follow-up could compare the deployed `CodeSha256` against a local checksum to detect drift and trigger a code update via `UpdateFunctionCodeCommand`. Should implem as code versioning or checksum diffing --- .../drivers/lambda.driver.ts | 139 ++++++++++++------ 1 file changed, 98 insertions(+), 41 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts index 668553ab02..b7bf561f5e 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts @@ -8,6 +8,7 @@ import { DeleteFunctionCommand, DeleteLayerVersionCommand, GetFunctionCommand, + GetFunctionCommandOutput, InvokeCommand, type InvokeCommandInput, Lambda, @@ -17,7 +18,9 @@ import { LogType, PublishLayerVersionCommand, ResourceNotFoundException, + UpdateFunctionConfigurationCommand, waitUntilFunctionActiveV2, + waitUntilFunctionUpdatedV2, } from '@aws-sdk/client-lambda'; import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'; @@ -233,6 +236,16 @@ export class LambdaDriver implements LogicFunctionDriver { ); } + private async waitFunctionUpdated( + functionName: string, + maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS, + ) { + await waitUntilFunctionUpdatedV2( + { client: await this.getLambdaClient(), maxWaitTime }, + { FunctionName: functionName }, + ); + } + private getDepsLayerName(flatApplication: FlatApplication): string { const checksum = flatApplication.yarnLockChecksum ?? 'default'; @@ -858,26 +871,18 @@ export class LambdaDriver implements LogicFunctionDriver { } while (isDefined(marker)); } - private async isAlreadyBuilt({ - flatLogicFunction, + private hasExpectedLayers({ + lambdaExecutor, flatApplication, applicationUniversalIdentifier, }: { - flatLogicFunction: FlatLogicFunction; + lambdaExecutor: GetFunctionCommandOutput; flatApplication: FlatApplication; applicationUniversalIdentifier: string; - }) { - const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction); - - if (!isDefined(lambdaExecutor)) { - return false; - } - + }): boolean { const layers = lambdaExecutor.Configuration?.Layers; if (!isDefined(layers) || layers.length !== 2) { - await this.delete(flatLogicFunction); - return false; } @@ -887,20 +892,34 @@ export class LambdaDriver implements LogicFunctionDriver { applicationUniversalIdentifier, }); - const hasExpectedLayers = + return ( layers.some((layer) => layer.Arn?.includes(depsLayerName)) && - layers.some((layer) => layer.Arn?.includes(sdkLayerName)); - - if (hasExpectedLayers) { - return true; - } - - await this.delete(flatLogicFunction); - - return false; + layers.some((layer) => layer.Arn?.includes(sdkLayerName)) + ); } - private async build({ + private async updateLambdaExecutorConfiguration({ + flatLogicFunction, + depsLayerArn, + sdkLayerArn, + }: { + flatLogicFunction: FlatLogicFunction; + depsLayerArn: string; + sdkLayerArn: string; + }) { + const lambdaClient = await this.getLambdaClient(); + + await lambdaClient.send( + new UpdateFunctionConfigurationCommand({ + FunctionName: flatLogicFunction.id, + Layers: [depsLayerArn, sdkLayerArn], + Runtime: flatLogicFunction.runtime, + Timeout: 900, + }), + ); + } + + private async buildLambdaExecutor({ flatLogicFunction, flatApplication, applicationUniversalIdentifier, @@ -915,7 +934,9 @@ export class LambdaDriver implements LogicFunctionDriver { applicationUniversalIdentifier, }; - if (await this.canSkipBuild(buildArgs)) { + const { canSkip } = await this.checkLambdaExecutorBuildStatus(buildArgs); + + if (canSkip) { return; } @@ -926,11 +947,14 @@ export class LambdaDriver implements LogicFunctionDriver { await this.cacheLockService.withLock( async () => { // Need to check again inside the lock in case lock was not acquired immediately. - if (await this.canSkipBuild(buildArgs)) { + const { canSkip, lambdaExecutor } = + await this.checkLambdaExecutorBuildStatus(buildArgs); + + if (canSkip) { return; } - await this.createLambdaExecutor(buildArgs); + await this.ensureLambdaExecutor({ ...buildArgs, lambdaExecutor }); }, `lambda-build:${flatLogicFunction.id}`, { @@ -941,7 +965,7 @@ export class LambdaDriver implements LogicFunctionDriver { ); } - private async canSkipBuild({ + private async checkLambdaExecutorBuildStatus({ flatLogicFunction, flatApplication, applicationUniversalIdentifier, @@ -949,28 +973,35 @@ export class LambdaDriver implements LogicFunctionDriver { flatLogicFunction: FlatLogicFunction; flatApplication: FlatApplication; applicationUniversalIdentifier: string; - }) { - return ( + }): Promise<{ + canSkip: boolean; + lambdaExecutor: GetFunctionCommandOutput | undefined; + }> { + const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction); + + const canSkip = + isDefined(lambdaExecutor) && !flatApplication.isSdkLayerStale && - (await this.isAlreadyBuilt({ - flatLogicFunction, + this.hasExpectedLayers({ + lambdaExecutor, flatApplication, applicationUniversalIdentifier, - })) - ); + }); + + return { canSkip, lambdaExecutor }; } - private async createLambdaExecutor({ + private async ensureLambdaExecutor({ flatLogicFunction, flatApplication, applicationUniversalIdentifier, + lambdaExecutor, }: { flatLogicFunction: FlatLogicFunction; flatApplication: FlatApplication; applicationUniversalIdentifier: string; + lambdaExecutor: GetFunctionCommandOutput | undefined; }) { - await this.delete(flatLogicFunction); - const depsLayerArn = await this.getLayerArn({ flatApplication, applicationUniversalIdentifier, @@ -981,6 +1012,34 @@ export class LambdaDriver implements LogicFunctionDriver { applicationUniversalIdentifier, }); + if (!isDefined(lambdaExecutor)) { + await this.createLambdaExecutor({ + flatLogicFunction, + depsLayerArn, + sdkLayerArn, + }); + await this.waitFunctionActive(flatLogicFunction.id); + + return; + } + + await this.updateLambdaExecutorConfiguration({ + flatLogicFunction, + depsLayerArn, + sdkLayerArn, + }); + await this.waitFunctionUpdated(flatLogicFunction.id); + } + + private async createLambdaExecutor({ + flatLogicFunction, + depsLayerArn, + sdkLayerArn, + }: { + flatLogicFunction: FlatLogicFunction; + depsLayerArn: string; + sdkLayerArn: string; + }) { const temporaryDirManager = new TemporaryDirManager(); const { sourceTemporaryDir, lambdaZipPath } = @@ -1006,9 +1065,9 @@ export class LambdaDriver implements LogicFunctionDriver { EphemeralStorage: { Size: LAMBDA_EPHEMERAL_STORAGE_MB }, }; - const command = new CreateFunctionCommand(params); + const lambdaClient = await this.getLambdaClient(); - await (await this.getLambdaClient()).send(command); + await lambdaClient.send(new CreateFunctionCommand(params)); } finally { await temporaryDirManager.clean(); } @@ -1037,14 +1096,12 @@ export class LambdaDriver implements LogicFunctionDriver { env, timeoutMs = 900_000, }: LogicFunctionExecuteParams): Promise { - await this.build({ + await this.buildLambdaExecutor({ flatLogicFunction, flatApplication, applicationUniversalIdentifier, }); - await this.waitFunctionActive(flatLogicFunction.id); - const startTime = Date.now(); const compiledCode = await this.logicFunctionResourceService.getBuiltCode({