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
This commit is contained in:
Paul Rastoin
2026-04-10 14:56:08 +02:00
committed by GitHub
parent 61720470c5
commit a82d078906
@@ -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<LogicFunctionExecuteResult> {
await this.build({
await this.buildLambdaExecutor({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier,
});
await this.waitFunctionActive(flatLogicFunction.id);
const startTime = Date.now();
const compiledCode = await this.logicFunctionResourceService.getBuiltCode({