Add cron trigger table (#14110)

This Pr begins the extensibility journey
- adds a `core.cronTrigger` table
- add a oneToMany relation between core.serverlessFunction and
`core.cronTrigger` (one serverlessFunction can be triggered by multiple
cronTriggers)
- add a job to trigger a serverless function
- adds a cron to trigger serverlessFunction (via the trigger job) based
on the core.cronTrigger.setting.pattern
- adds a command to register the cron
- add the command in `cron-register-all.command.ts`
This commit is contained in:
martmull
2025-08-28 14:47:48 +02:00
committed by GitHub
parent 85e6fe8849
commit 9b41a3be54
17 changed files with 279 additions and 14 deletions
@@ -6,7 +6,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { AutomatedTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service';
import { WorkflowCronTriggerCronCommand } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/commands/workflow-cron-trigger.cron.command';
import { CronTriggerCronJob } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/jobs/cron-trigger.cron.job';
import { WorkflowCronTriggerCronJob } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/jobs/workflow-cron-trigger-cron.job';
import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener';
@Module({
@@ -18,7 +18,7 @@ import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trig
providers: [
AutomatedTriggerWorkspaceService,
DatabaseEventTriggerListener,
CronTriggerCronJob,
WorkflowCronTriggerCronJob,
WorkflowCronTriggerCronCommand,
],
exports: [AutomatedTriggerWorkspaceService, WorkflowCronTriggerCronCommand],
@@ -4,9 +4,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import {
CRON_TRIGGER_CRON_PATTERN,
CronTriggerCronJob,
} from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/jobs/cron-trigger.cron.job';
WORKFLOW_CRON_TRIGGER_CRON_PATTERN,
WorkflowCronTriggerCronJob,
} from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/jobs/workflow-cron-trigger-cron.job';
@Command({
name: 'cron:workflow:automated-cron-trigger',
@@ -22,11 +22,11 @@ export class WorkflowCronTriggerCronCommand extends CommandRunner {
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: CronTriggerCronJob.name,
jobName: WorkflowCronTriggerCronJob.name,
data: undefined,
options: {
repeat: {
pattern: CRON_TRIGGER_CRON_PATTERN,
pattern: WORKFLOW_CRON_TRIGGER_CRON_PATTERN,
},
},
});
@@ -15,16 +15,16 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
import { type CronTriggerSettings } from 'src/modules/workflow/workflow-trigger/automated-trigger/constants/automated-trigger-settings';
import { shouldRunNow } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/utils/should-run-now.utils';
import {
WorkflowTriggerJob,
type WorkflowTriggerJobData,
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
import { shouldRunNow } from 'src/utils/should-run-now.utils';
export const CRON_TRIGGER_CRON_PATTERN = '* * * * *';
export const WORKFLOW_CRON_TRIGGER_CRON_PATTERN = '* * * * *';
@Processor(MessageQueue.cronQueue)
export class CronTriggerCronJob {
export class WorkflowCronTriggerCronJob {
constructor(
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -35,8 +35,11 @@ export class CronTriggerCronJob {
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@Process(CronTriggerCronJob.name)
@SentryCronMonitor(CronTriggerCronJob.name, CRON_TRIGGER_CRON_PATTERN)
@Process(WorkflowCronTriggerCronJob.name)
@SentryCronMonitor(
WorkflowCronTriggerCronJob.name,
WORKFLOW_CRON_TRIGGER_CRON_PATTERN,
)
async handle() {
const activeWorkspaces = await this.workspaceRepository.find({
where: {
@@ -1,20 +0,0 @@
import { CronExpressionParser } from 'cron-parser';
export const shouldRunNow = (
pattern: string,
now: Date,
rootCronIntervalMs = 60_000,
) => {
try {
const interval = CronExpressionParser.parse(pattern, {
currentDate: now,
});
const prevTriggerDate = interval.prev();
const diff = Math.abs(prevTriggerDate.getTime() - now.getTime());
return diff < rootCronIntervalMs;
} catch {
return false;
}
};
@@ -1,47 +0,0 @@
import { shouldRunNow } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/utils/should-run-now.utils';
const getNowDate = (hour: string) => {
return new Date(`2025-01-01T${hour}.100Z`);
};
describe('shouldRunNow', () => {
it('returns true when now matches cron pattern */1 * * * *', () => {
const cron = '*/1 * * * *';
expect(shouldRunNow(cron, getNowDate('10:00:00'))).toBe(true);
});
it('returns true with a 50s root cron delay', () => {
const cron = '*/1 * * * *';
expect(shouldRunNow(cron, getNowDate('10:00:50'))).toBe(true);
});
it('returns true 5 times in a row for a */5 pattern', () => {
const cron = '*/5 * * * *'; // every 5 minutes
expect(shouldRunNow(cron, getNowDate('09:59:00'))).toBe(false);
expect(shouldRunNow(cron, getNowDate('10:00:00'))).toBe(true);
expect(shouldRunNow(cron, getNowDate('10:01:00'))).toBe(false);
expect(shouldRunNow(cron, getNowDate('10:02:00'))).toBe(false);
expect(shouldRunNow(cron, getNowDate('10:03:00'))).toBe(false);
expect(shouldRunNow(cron, getNowDate('10:04:00'))).toBe(false);
expect(shouldRunNow(cron, getNowDate('10:05:00'))).toBe(true);
expect(shouldRunNow(cron, getNowDate('10:06:00'))).toBe(false);
});
it('returns false for invalid cron pattern', () => {
const cron = 'invalid-cron';
expect(shouldRunNow(cron, getNowDate('10:00:00'))).toBe(false);
});
it('returns false if the next run is outside the interval window (2 minutes)', () => {
const cron = '*/10 * * * *'; // every 10 minutes
const interval2min = 2 * 60_000;
expect(shouldRunNow(cron, getNowDate('10:06:00'), interval2min)).toBe(
false,
);
});
});