Skip to content

Commit a82d078

Browse files
authored
Lambda build update instead of delete existing logic function while building (#19116)
# 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
1 parent 6172047 commit a82d078

1 file changed

Lines changed: 96 additions & 39 deletions

File tree

  • packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers

packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts

Lines changed: 96 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DeleteFunctionCommand,
99
DeleteLayerVersionCommand,
1010
GetFunctionCommand,
11+
GetFunctionCommandOutput,
1112
InvokeCommand,
1213
type InvokeCommandInput,
1314
Lambda,
@@ -17,7 +18,9 @@ import {
1718
LogType,
1819
PublishLayerVersionCommand,
1920
ResourceNotFoundException,
21+
UpdateFunctionConfigurationCommand,
2022
waitUntilFunctionActiveV2,
23+
waitUntilFunctionUpdatedV2,
2124
} from '@aws-sdk/client-lambda';
2225
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
2326
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
@@ -233,6 +236,16 @@ export class LambdaDriver implements LogicFunctionDriver {
233236
);
234237
}
235238

239+
private async waitFunctionUpdated(
240+
functionName: string,
241+
maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS,
242+
) {
243+
await waitUntilFunctionUpdatedV2(
244+
{ client: await this.getLambdaClient(), maxWaitTime },
245+
{ FunctionName: functionName },
246+
);
247+
}
248+
236249
private getDepsLayerName(flatApplication: FlatApplication): string {
237250
const checksum = flatApplication.yarnLockChecksum ?? 'default';
238251

@@ -858,26 +871,18 @@ export class LambdaDriver implements LogicFunctionDriver {
858871
} while (isDefined(marker));
859872
}
860873

861-
private async isAlreadyBuilt({
862-
flatLogicFunction,
874+
private hasExpectedLayers({
875+
lambdaExecutor,
863876
flatApplication,
864877
applicationUniversalIdentifier,
865878
}: {
866-
flatLogicFunction: FlatLogicFunction;
879+
lambdaExecutor: GetFunctionCommandOutput;
867880
flatApplication: FlatApplication;
868881
applicationUniversalIdentifier: string;
869-
}) {
870-
const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction);
871-
872-
if (!isDefined(lambdaExecutor)) {
873-
return false;
874-
}
875-
882+
}): boolean {
876883
const layers = lambdaExecutor.Configuration?.Layers;
877884

878885
if (!isDefined(layers) || layers.length !== 2) {
879-
await this.delete(flatLogicFunction);
880-
881886
return false;
882887
}
883888

@@ -887,20 +892,34 @@ export class LambdaDriver implements LogicFunctionDriver {
887892
applicationUniversalIdentifier,
888893
});
889894

890-
const hasExpectedLayers =
895+
return (
891896
layers.some((layer) => layer.Arn?.includes(depsLayerName)) &&
892-
layers.some((layer) => layer.Arn?.includes(sdkLayerName));
893-
894-
if (hasExpectedLayers) {
895-
return true;
896-
}
897+
layers.some((layer) => layer.Arn?.includes(sdkLayerName))
898+
);
899+
}
897900

898-
await this.delete(flatLogicFunction);
901+
private async updateLambdaExecutorConfiguration({
902+
flatLogicFunction,
903+
depsLayerArn,
904+
sdkLayerArn,
905+
}: {
906+
flatLogicFunction: FlatLogicFunction;
907+
depsLayerArn: string;
908+
sdkLayerArn: string;
909+
}) {
910+
const lambdaClient = await this.getLambdaClient();
899911

900-
return false;
912+
await lambdaClient.send(
913+
new UpdateFunctionConfigurationCommand({
914+
FunctionName: flatLogicFunction.id,
915+
Layers: [depsLayerArn, sdkLayerArn],
916+
Runtime: flatLogicFunction.runtime,
917+
Timeout: 900,
918+
}),
919+
);
901920
}
902921

903-
private async build({
922+
private async buildLambdaExecutor({
904923
flatLogicFunction,
905924
flatApplication,
906925
applicationUniversalIdentifier,
@@ -915,7 +934,9 @@ export class LambdaDriver implements LogicFunctionDriver {
915934
applicationUniversalIdentifier,
916935
};
917936

918-
if (await this.canSkipBuild(buildArgs)) {
937+
const { canSkip } = await this.checkLambdaExecutorBuildStatus(buildArgs);
938+
939+
if (canSkip) {
919940
return;
920941
}
921942

@@ -926,11 +947,14 @@ export class LambdaDriver implements LogicFunctionDriver {
926947
await this.cacheLockService.withLock(
927948
async () => {
928949
// Need to check again inside the lock in case lock was not acquired immediately.
929-
if (await this.canSkipBuild(buildArgs)) {
950+
const { canSkip, lambdaExecutor } =
951+
await this.checkLambdaExecutorBuildStatus(buildArgs);
952+
953+
if (canSkip) {
930954
return;
931955
}
932956

933-
await this.createLambdaExecutor(buildArgs);
957+
await this.ensureLambdaExecutor({ ...buildArgs, lambdaExecutor });
934958
},
935959
`lambda-build:${flatLogicFunction.id}`,
936960
{
@@ -941,36 +965,43 @@ export class LambdaDriver implements LogicFunctionDriver {
941965
);
942966
}
943967

944-
private async canSkipBuild({
968+
private async checkLambdaExecutorBuildStatus({
945969
flatLogicFunction,
946970
flatApplication,
947971
applicationUniversalIdentifier,
948972
}: {
949973
flatLogicFunction: FlatLogicFunction;
950974
flatApplication: FlatApplication;
951975
applicationUniversalIdentifier: string;
952-
}) {
953-
return (
976+
}): Promise<{
977+
canSkip: boolean;
978+
lambdaExecutor: GetFunctionCommandOutput | undefined;
979+
}> {
980+
const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction);
981+
982+
const canSkip =
983+
isDefined(lambdaExecutor) &&
954984
!flatApplication.isSdkLayerStale &&
955-
(await this.isAlreadyBuilt({
956-
flatLogicFunction,
985+
this.hasExpectedLayers({
986+
lambdaExecutor,
957987
flatApplication,
958988
applicationUniversalIdentifier,
959-
}))
960-
);
989+
});
990+
991+
return { canSkip, lambdaExecutor };
961992
}
962993

963-
private async createLambdaExecutor({
994+
private async ensureLambdaExecutor({
964995
flatLogicFunction,
965996
flatApplication,
966997
applicationUniversalIdentifier,
998+
lambdaExecutor,
967999
}: {
9681000
flatLogicFunction: FlatLogicFunction;
9691001
flatApplication: FlatApplication;
9701002
applicationUniversalIdentifier: string;
1003+
lambdaExecutor: GetFunctionCommandOutput | undefined;
9711004
}) {
972-
await this.delete(flatLogicFunction);
973-
9741005
const depsLayerArn = await this.getLayerArn({
9751006
flatApplication,
9761007
applicationUniversalIdentifier,
@@ -981,6 +1012,34 @@ export class LambdaDriver implements LogicFunctionDriver {
9811012
applicationUniversalIdentifier,
9821013
});
9831014

1015+
if (!isDefined(lambdaExecutor)) {
1016+
await this.createLambdaExecutor({
1017+
flatLogicFunction,
1018+
depsLayerArn,
1019+
sdkLayerArn,
1020+
});
1021+
await this.waitFunctionActive(flatLogicFunction.id);
1022+
1023+
return;
1024+
}
1025+
1026+
await this.updateLambdaExecutorConfiguration({
1027+
flatLogicFunction,
1028+
depsLayerArn,
1029+
sdkLayerArn,
1030+
});
1031+
await this.waitFunctionUpdated(flatLogicFunction.id);
1032+
}
1033+
1034+
private async createLambdaExecutor({
1035+
flatLogicFunction,
1036+
depsLayerArn,
1037+
sdkLayerArn,
1038+
}: {
1039+
flatLogicFunction: FlatLogicFunction;
1040+
depsLayerArn: string;
1041+
sdkLayerArn: string;
1042+
}) {
9841043
const temporaryDirManager = new TemporaryDirManager();
9851044

9861045
const { sourceTemporaryDir, lambdaZipPath } =
@@ -1006,9 +1065,9 @@ export class LambdaDriver implements LogicFunctionDriver {
10061065
EphemeralStorage: { Size: LAMBDA_EPHEMERAL_STORAGE_MB },
10071066
};
10081067

1009-
const command = new CreateFunctionCommand(params);
1068+
const lambdaClient = await this.getLambdaClient();
10101069

1011-
await (await this.getLambdaClient()).send(command);
1070+
await lambdaClient.send(new CreateFunctionCommand(params));
10121071
} finally {
10131072
await temporaryDirManager.clean();
10141073
}
@@ -1037,14 +1096,12 @@ export class LambdaDriver implements LogicFunctionDriver {
10371096
env,
10381097
timeoutMs = 900_000,
10391098
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
1040-
await this.build({
1099+
await this.buildLambdaExecutor({
10411100
flatLogicFunction,
10421101
flatApplication,
10431102
applicationUniversalIdentifier,
10441103
});
10451104

1046-
await this.waitFunctionActive(flatLogicFunction.id);
1047-
10481105
const startTime = Date.now();
10491106

10501107
const compiledCode = await this.logicFunctionResourceService.getBuiltCode({

0 commit comments

Comments
 (0)