fix: add per-workspace try-catch in CronTriggerCronJob handle loop

https://sonarly.com/issue/19618?type=bug

The CronTriggerCronJob, which runs every minute to trigger cron-based logic functions, fails entirely if any single workspace's cache computation throws an error, blocking cron triggers for ALL workspaces.

Fix: Added per-workspace error isolation to `CronTriggerCronJob.handle()`, matching the exact pattern used in `WorkflowCronTriggerCronJob`.

Three changes to `cron-trigger.cron.job.ts`:

1. **Added `Logger` instance** (`private readonly logger = new Logger(CronTriggerCronJob.name)`) for structured error logging per workspace.

2. **Injected `ExceptionHandlerService`** into the constructor. This service is globally available via `@Global()` `ExceptionHandlerModule`, so no module import changes are needed.

3. **Wrapped the per-workspace loop body in `try-catch`** so that if `workspaceCacheService.getOrRecompute()` (or any other operation) throws for one workspace, the error is logged and reported to Sentry with workspace context, and processing continues for the remaining workspaces.

Before this fix, a single workspace with problematic data (e.g., duplicate `universalIdentifier` in logic functions) would cause the entire cron job to abort, blocking cron-triggered logic functions for ALL workspaces.
This commit is contained in:
Sonarly Claude Code
2026-03-30 13:44:30 +00:00
parent b8374f5531
commit 6a5186a59f
@@ -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 { 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,12 +24,15 @@ 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,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@Process(CronTriggerCronJob.name)
@@ -43,45 +48,56 @@ export class CronTriggerCronJob {
const now = new Date();
for (const activeWorkspace of activeWorkspaces) {
const { flatLogicFunctionMaps } =
await this.workspaceCacheService.getOrRecompute(activeWorkspace.id, [
'flatLogicFunctionMaps',
]);
try {
const { flatLogicFunctionMaps } =
await this.workspaceCacheService.getOrRecompute(activeWorkspace.id, [
'flatLogicFunctionMaps',
]);
const logicFunctions = Object.values(
flatLogicFunctionMaps.byUniversalIdentifier,
);
for (const logicFunction of logicFunctions) {
if (!isDefined(logicFunction)) {
continue;
}
const cronSettings = logicFunction.cronTriggerSettings;
if (!isDefined(cronSettings?.pattern)) {
continue;
}
if (isDefined(logicFunction.deletedAt)) {
continue;
}
if (!shouldRunNow(cronSettings.pattern, now)) {
continue;
}
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
LogicFunctionTriggerJob.name,
[
{
logicFunctionId: logicFunction.id,
workspaceId: activeWorkspace.id,
payload: {},
},
],
{ retryLimit: 3 },
const logicFunctions = Object.values(
flatLogicFunctionMaps.byUniversalIdentifier,
);
for (const logicFunction of logicFunctions) {
if (!isDefined(logicFunction)) {
continue;
}
const cronSettings = logicFunction.cronTriggerSettings;
if (!isDefined(cronSettings?.pattern)) {
continue;
}
if (isDefined(logicFunction.deletedAt)) {
continue;
}
if (!shouldRunNow(cronSettings.pattern, now)) {
continue;
}
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
LogicFunctionTriggerJob.name,
[
{
logicFunctionId: logicFunction.id,
workspaceId: activeWorkspace.id,
payload: {},
},
],
{ retryLimit: 3 },
);
}
} catch (error) {
this.logger.error(
`Error processing workspace ${activeWorkspace.id}: ${error}`,
);
this.exceptionHandlerService.captureExceptions([error], {
workspace: {
id: activeWorkspace.id,
},
});
}
}
}