Compare commits

...

1 Commits

Author SHA1 Message Date
Sonarly Claude Code 6bdf5f9781 fix: add concurrency guard to Lambda function build/execute lifecycle
https://sonarly.com/issue/19253?type=bug

Concurrent workflow executions of the same logic function race on Lambda create/delete, causing one execution to time out when its function is deleted mid-poll by another execution's `build()` call.

Fix: Three changes to `lambda.driver.ts` to address the concurrent execution race condition:

1. **Import `ResourceConflictException`** from `@aws-sdk/client-lambda` (line 19) — needed to catch 409 Conflict responses from AWS Lambda API.

2. **Handle `ResourceConflictException` on all three `CreateFunctionCommand` call sites:**
   - `ensureYarnInstallLambdaExists()` (line 396-402)
   - `ensureBuilderLambdaExists()` (line 491-496)
   - `build()` (line 968-974)

   When a concurrent execution already created the same function, the 409 is safely caught and execution continues to `waitFunctionActive`, which will wait for the existing function to become ready.

3. **Re-check `isAlreadyBuilt` after layer resolution in `build()`** (lines 926-938) — this is the key fix for the root cause. When `isSdkLayerStale=true`, the first check at line 905 is bypassed, but `ensureSdkLayer()` takes significant time (downloading archive, creating zip, publishing layer). During this time, a concurrent execution may have already rebuilt the function with the correct layers. The re-check prevents this execution from calling `delete()` on a function that another execution is currently waiting on via `waitFunctionActive`, which was the direct cause of the TimeoutError.
2026-03-28 17:48:35 +00:00
@@ -16,6 +16,7 @@ import {
type ListLayerVersionsCommandInput,
LogType,
PublishLayerVersionCommand,
ResourceConflictException,
ResourceNotFoundException,
waitUntilFunctionActiveV2,
} from '@aws-sdk/client-lambda';
@@ -392,7 +393,13 @@ export class LambdaDriver implements LogicFunctionDriver {
MemorySize: YARN_INSTALL_LAMBDA_MEMORY_MB,
};
await lambdaClient.send(new CreateFunctionCommand(params));
try {
await lambdaClient.send(new CreateFunctionCommand(params));
} catch (error) {
if (!(error instanceof ResourceConflictException)) {
throw error;
}
}
} finally {
await temporaryDirManager.clean();
}
@@ -481,7 +488,13 @@ export class LambdaDriver implements LogicFunctionDriver {
MemorySize: BUILDER_LAMBDA_MEMORY_MB,
};
await lambdaClient.send(new CreateFunctionCommand(params));
try {
await lambdaClient.send(new CreateFunctionCommand(params));
} catch (error) {
if (!(error instanceof ResourceConflictException)) {
throw error;
}
}
} finally {
await temporaryDirManager.clean();
}
@@ -900,8 +913,6 @@ export class LambdaDriver implements LogicFunctionDriver {
return;
}
await this.delete(flatLogicFunction);
const depsLayerArn = await this.getLayerArn({
flatApplication,
applicationUniversalIdentifier,
@@ -912,6 +923,22 @@ export class LambdaDriver implements LogicFunctionDriver {
applicationUniversalIdentifier,
});
// Re-check after layer resolution: a concurrent execution may have
// already rebuilt the function with the correct layers while we were
// waiting for ensureSdkLayer. Skipping the delete+create avoids
// destroying a function another execution is currently polling.
if (
await this.isAlreadyBuilt({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier,
})
) {
return;
}
await this.delete(flatLogicFunction);
const temporaryDirManager = new TemporaryDirManager();
const { sourceTemporaryDir, lambdaZipPath } =
@@ -938,7 +965,13 @@ export class LambdaDriver implements LogicFunctionDriver {
const command = new CreateFunctionCommand(params);
await (await this.getLambdaClient()).send(command);
try {
await (await this.getLambdaClient()).send(command);
} catch (error) {
if (!(error instanceof ResourceConflictException)) {
throw error;
}
}
} finally {
await temporaryDirManager.clean();
}