fix(logic-function): treat invoke timeout as a user-level error, not a platform error (#21779)
Fixes https://twenty-v7.sentry.io/issues/7527156270?project=4507072499810304 ## Problem Sentry was flooded with high-severity alerts for logic functions that simply ran too long: > Lambda timed out for function '…' during invoke (functionState=Active, phase=invoke …) A function exceeding its configured `timeoutSeconds` is a **user-level outcome** (their code is too slow), not a platform failure — but it was being reported as one. ## Root cause The two timeout mechanisms were classified inconsistently: - **Lambda's own timeout** → returns `{ status: ERROR, … }` → handled as a user error (route returns 500 with `shouldBeCapturedBySentry: false`, queue job records it without failing). Not in Sentry. ✓ - **Client-side `AbortSignal` timeout** → **threw** `LOGIC_FUNCTION_EXECUTION_TIMEOUT`, which isn't mapped in `mapErrorToRouteTriggerCode`, so it fell through to `ROUTE_TRIGGER_PLATFORM_ERROR` (Sentry) and failed the BullMQ job (Sentry). ✗ Since the executor Lambda is fixed at 900s, the client abort is the *sole* timeout enforcement for every function with `timeoutSeconds < 900` — so essentially every slow function paged the team. The `local` driver already returns an ERROR result here; only the Lambda driver threw. ## Fix On an **invoke-phase** `TimeoutError`, return a structured ERROR result instead of throwing — mirroring the Lambda's own timeout and the local driver. The timeout now flows through the normal result path: surfaced to the caller as `status: ERROR`, recorded via `handleExecutionResult` (which the throw path skipped), and kept out of Sentry. **Build- and fetch-phase timeouts still throw** and stay in Sentry — those are platform-side (executor build / code fetch too slow, even for short user code), which is exactly what the phase instrumentation exists to catch. ## Tests - Unit test on the new `buildLogicFunctionTimeoutResult` util - `npx jest logic-function-drivers/drivers/lambda` green
This commit is contained in:
+15
-1
@@ -25,6 +25,7 @@ import { LambdaAwsClientService } from 'src/engine/core-modules/logic-function/l
|
||||
import { LambdaExecutorManagerService } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-executor-manager.service';
|
||||
import { LambdaLayerManagerService } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-layer-manager.service';
|
||||
import { LambdaToolFunctionsService } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-tool-functions.service';
|
||||
import { buildLogicFunctionTimeoutResult } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-logic-function-timeout-result.util';
|
||||
import { parseLambdaLogResult } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/parse-lambda-log-result.util';
|
||||
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
|
||||
import { LogicFunctionExecutionMode } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
@@ -217,6 +218,18 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
} catch (error) {
|
||||
const phaseTiming = `phase=${currentPhase} buildMs=${buildExecutorMs} fetchCodeMs=${getBuiltCodeMs}`;
|
||||
|
||||
const isTimeoutError =
|
||||
error instanceof Error && error.name === 'TimeoutError';
|
||||
|
||||
if (isTimeoutError && currentPhase === LambdaExecutionPhase.INVOKE) {
|
||||
// User-level outcome (function ran too long), not a platform error: return, don't throw.
|
||||
this.logger.warn(
|
||||
`Logic function '${flatLogicFunction.id}' timed out during invoke [${phaseTiming}]`,
|
||||
);
|
||||
|
||||
return buildLogicFunctionTimeoutResult(timeoutMs);
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`Lambda invocation failed for function ${flatLogicFunction.id} [${phaseTiming}]: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
@@ -229,7 +242,8 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
if (isTimeoutError) {
|
||||
// Build/fetch-phase timeouts are platform-side — keep throwing so they reach Sentry.
|
||||
const executor = await this.executorManager
|
||||
.getLambdaExecutor(flatLogicFunction)
|
||||
.catch(() => undefined);
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { buildLogicFunctionTimeoutResult } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-logic-function-timeout-result.util';
|
||||
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
|
||||
|
||||
describe('buildLogicFunctionTimeoutResult', () => {
|
||||
it('returns an ERROR result for a timeout', () => {
|
||||
const result = buildLogicFunctionTimeoutResult(30_000);
|
||||
|
||||
expect(result.status).toBe(LogicFunctionExecutionStatus.ERROR);
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error?.errorType).toBe('TimeoutError');
|
||||
expect(result.error?.errorMessage).toBe(
|
||||
'Function execution timed out after 30s',
|
||||
);
|
||||
expect(result.duration).toBe(30_000);
|
||||
});
|
||||
|
||||
it('rounds the timeout to whole seconds', () => {
|
||||
expect(buildLogicFunctionTimeoutResult(900_000).error?.errorMessage).toBe(
|
||||
'Function execution timed out after 900s',
|
||||
);
|
||||
expect(buildLogicFunctionTimeoutResult(1_500).error?.errorMessage).toBe(
|
||||
'Function execution timed out after 2s',
|
||||
);
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { type LogicFunctionExecuteResult } from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
|
||||
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
|
||||
|
||||
export const buildLogicFunctionTimeoutResult = (
|
||||
timeoutMs: number,
|
||||
): LogicFunctionExecuteResult => ({
|
||||
data: null,
|
||||
logs: '',
|
||||
duration: timeoutMs,
|
||||
status: LogicFunctionExecutionStatus.ERROR,
|
||||
error: {
|
||||
errorType: 'TimeoutError',
|
||||
errorMessage: `Function execution timed out after ${Math.round(timeoutMs / 1_000)}s`,
|
||||
stackTrace: [],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user