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:
+53
-37
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user