Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code f4fdf3384d CronTriggerCronJob missing try-catch causes full abort on query timeout
https://sonarly.com/issue/3104?type=bug

The CronTriggerCronJob iterates over all active workspaces without per-iteration error handling. A single query timeout aborts the entire cron run, preventing all remaining workspaces from having their cron-triggered logic functions checked.

Fix: The fix adds per-workspace error isolation to `CronTriggerCronJob.handle()` by wrapping the loop body in a `try-catch`, exactly matching the pattern already used in every other workspace-iterating cron job (e.g. `WorkflowCronTriggerCronJob`).

Two additions were made to the class:
1. A `Logger` instance (`private readonly logger = new Logger(CronTriggerCronJob.name)`) for structured error logging.
2. An injected `ExceptionHandlerService` (globally provided via `@Global()` `ExceptionHandlerModule`) for Sentry/exception reporting.

The `try-catch` block isolates each workspace iteration so that a query timeout on one workspace (e.g. from connection-pool contention) is caught, logged, and reported — without aborting processing for the remaining workspaces.

```typescript file=packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts lines=51-95
    for (const activeWorkspace of activeWorkspaces) {
      try {
        const logicFunctionsWithCronTrigger =
          await this.logicFunctionRepository.find({ ... });
        // ... inner loop unchanged
      } catch (error) {
        this.logger.error(
          `Error processing workspace ${activeWorkspace.id}: ${error}`,
        );
        this.exceptionHandlerService.captureExceptions([error], {
          workspace: { id: activeWorkspace.id },
        });
      }
    }
```

No module changes are required because `ExceptionHandlerModule` is decorated with `@Global()` and exports `ExceptionHandlerService` globally.
2026-03-04 12:45:05 +00:00
@@ -1,3 +1,4 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
@@ -5,6 +6,7 @@ import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { IsNull, Not, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
@@ -22,6 +24,8 @@ export const CRON_TRIGGER_CRON_PATTERN = '* * * * *';
@Processor(MessageQueue.cronQueue)
export class CronTriggerCronJob {
private readonly logger = new Logger(CronTriggerCronJob.name);
constructor(
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
private readonly messageQueueService: MessageQueueService,
@@ -29,6 +33,7 @@ export class CronTriggerCronJob {
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@Process(CronTriggerCronJob.name)
@@ -44,37 +49,48 @@ export class CronTriggerCronJob {
const now = new Date();
for (const activeWorkspace of activeWorkspaces) {
const logicFunctionsWithCronTrigger =
await this.logicFunctionRepository.find({
where: {
workspaceId: activeWorkspace.id,
cronTriggerSettings: Not(IsNull()),
},
select: ['id', 'cronTriggerSettings', 'workspaceId'],
});
for (const logicFunction of logicFunctionsWithCronTrigger) {
const cronSettings = logicFunction.cronTriggerSettings;
if (!isDefined(cronSettings?.pattern)) {
continue;
}
if (!shouldRunNow(cronSettings.pattern, now)) {
continue;
}
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
LogicFunctionTriggerJob.name,
[
{
logicFunctionId: logicFunction.id,
workspaceId: logicFunction.workspaceId,
payload: {},
try {
const logicFunctionsWithCronTrigger =
await this.logicFunctionRepository.find({
where: {
workspaceId: activeWorkspace.id,
cronTriggerSettings: Not(IsNull()),
},
],
{ retryLimit: 3 },
select: ['id', 'cronTriggerSettings', 'workspaceId'],
});
for (const logicFunction of logicFunctionsWithCronTrigger) {
const cronSettings = logicFunction.cronTriggerSettings;
if (!isDefined(cronSettings?.pattern)) {
continue;
}
if (!shouldRunNow(cronSettings.pattern, now)) {
continue;
}
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
LogicFunctionTriggerJob.name,
[
{
logicFunctionId: logicFunction.id,
workspaceId: logicFunction.workspaceId,
payload: {},
},
],
{ retryLimit: 3 },
);
}
} catch (error) {
this.logger.error(
`Error processing workspace ${activeWorkspace.id}: ${error}`,
);
this.exceptionHandlerService.captureExceptions([error], {
workspace: {
id: activeWorkspace.id,
},
});
}
}
}