tt-fix-logic-function-error-handling (#20928)

## Summary
- **Fix `callWithTimeout` timer leak**: the `setTimeout` was never
cleared when the callback resolved first, leaving orphaned timers (up to
15 minutes) in the Node.js event loop. Now uses `try/finally` with
`clearTimeout`.
- **Properly classify timeout errors**: introduced
`ExecutionTimedOutError` so the Lambda driver can throw
`LOGIC_FUNCTION_EXECUTION_TIMEOUT` instead of
`LOGIC_FUNCTION_EXECUTION_FAILED` — users now see "Function execution
timed out" instead of the generic "Function execution failed."

## Test plan
- [x] Execute a logic function normally and verify it still works
- [x] Trigger a logic function timeout and verify the error message says
"Function execution timed out" (not "Function execution failed")
- [x] Verify that a deleted logic function invocation logs a specific
"was deleted" warning
- [x] Verify no orphaned timers remain after logic function execution
completes
This commit is contained in:
Thomas Trompette
2026-05-27 16:32:12 +02:00
committed by GitHub
parent dde6df7a26
commit ff28547a36
2 changed files with 30 additions and 39 deletions
@@ -41,7 +41,6 @@ import { ASSET_PATH } from 'src/constants/assets-path';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { COMMON_LAYER_DEPENDENCIES_DIRNAME } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/common-layer-dependencies-dirname';
import { callWithTimeout } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/call-with-timeout';
import { copyBuilder } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-builder';
import { copyCommonLayerDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-common-layer-dependencies';
import { copyExecutor } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-executor';
@@ -429,17 +428,18 @@ export class LambdaDriver implements LogicFunctionDriver {
const yarnInstallFunctionName = await this.getYarnInstallFunctionName();
const result = await callWithTimeout({
callback: () =>
lambdaClient.send(
new InvokeCommand({
FunctionName: yarnInstallFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
const result = await lambdaClient.send(
new InvokeCommand({
FunctionName: yarnInstallFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
{
abortSignal: AbortSignal.timeout(
YARN_INSTALL_LAMBDA_TIMEOUT_SECONDS * 1000,
),
timeoutMs: YARN_INSTALL_LAMBDA_TIMEOUT_SECONDS * 1000,
});
},
);
if (result.FunctionError) {
const parsedResult = result.Payload
@@ -529,17 +529,16 @@ export class LambdaDriver implements LogicFunctionDriver {
builtFileName,
};
const result = await callWithTimeout({
callback: () =>
lambdaClient.send(
new InvokeCommand({
FunctionName: builderFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
),
timeoutMs: BUILDER_LAMBDA_TIMEOUT_SECONDS * 1000,
});
const result = await lambdaClient.send(
new InvokeCommand({
FunctionName: builderFunctionName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
}),
{
abortSignal: AbortSignal.timeout(BUILDER_LAMBDA_TIMEOUT_SECONDS * 1000),
},
);
if (result.FunctionError) {
const parsedResult = result.Payload
@@ -1195,9 +1194,8 @@ export class LambdaDriver implements LogicFunctionDriver {
const lambdaClient = await this.getLambdaClient();
const invokeStart = Date.now();
const result = await callWithTimeout({
callback: () => lambdaClient.send(command),
timeoutMs,
const result = await lambdaClient.send(command, {
abortSignal: AbortSignal.timeout(timeoutMs),
});
const invokeSendMs = Date.now() - invokeStart;
@@ -1248,6 +1246,13 @@ export class LambdaDriver implements LogicFunctionDriver {
);
}
if (error instanceof Error && error.name === 'TimeoutError') {
throw new LogicFunctionException(
`Lambda invocation timed out for function '${flatLogicFunction.id}': ${error.message}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_TIMEOUT,
);
}
if (error instanceof LogicFunctionException) {
throw error;
}
@@ -1,14 +0,0 @@
export const callWithTimeout = async <T>({
callback,
timeoutMs,
}: {
callback: () => Promise<T>;
timeoutMs: number;
}): Promise<T> => {
return Promise.race([
callback(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs),
),
]);
};