mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(core): Increase Cron observability (#17626)
This commit is contained in:
@@ -19,10 +19,22 @@ export const LOG_SCOPES = [
|
|||||||
'insights',
|
'insights',
|
||||||
'workflow-activation',
|
'workflow-activation',
|
||||||
'ssh-client',
|
'ssh-client',
|
||||||
|
'cron',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type LogScope = (typeof LOG_SCOPES)[number];
|
export type LogScope = (typeof LOG_SCOPES)[number];
|
||||||
|
|
||||||
|
@Config
|
||||||
|
export class CronLoggingConfig {
|
||||||
|
/**
|
||||||
|
* Interval in minutes to log currently active cron jobs. Set to `0` to disable.
|
||||||
|
*
|
||||||
|
* @example `N8N_LOG_CRON_ACTIVE_INTERVAL=30` will log active crons every 30 minutes.
|
||||||
|
*/
|
||||||
|
@Env('N8N_LOG_CRON_ACTIVE_INTERVAL')
|
||||||
|
activeInterval: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Config
|
@Config
|
||||||
class FileLoggingConfig {
|
class FileLoggingConfig {
|
||||||
/**
|
/**
|
||||||
@@ -79,6 +91,9 @@ export class LoggingConfig {
|
|||||||
@Nested
|
@Nested
|
||||||
file: FileLoggingConfig;
|
file: FileLoggingConfig;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
cron: CronLoggingConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scopes to filter logs by. Nothing is filtered by default.
|
* Scopes to filter logs by. Nothing is filtered by default.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export { MfaConfig } from './configs/mfa.config';
|
|||||||
export { HiringBannerConfig } from './configs/hiring-banner.config';
|
export { HiringBannerConfig } from './configs/hiring-banner.config';
|
||||||
export { PersonalizationConfig } from './configs/personalization.config';
|
export { PersonalizationConfig } from './configs/personalization.config';
|
||||||
export { NodesConfig } from './configs/nodes.config';
|
export { NodesConfig } from './configs/nodes.config';
|
||||||
|
export { CronLoggingConfig } from './configs/logging.config';
|
||||||
|
|
||||||
const protocolSchema = z.enum(['http', 'https']);
|
const protocolSchema = z.enum(['http', 'https']);
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,9 @@ describe('GlobalConfig', () => {
|
|||||||
location: 'logs/n8n.log',
|
location: 'logs/n8n.log',
|
||||||
},
|
},
|
||||||
scopes: [],
|
scopes: [],
|
||||||
|
cron: {
|
||||||
|
activeInterval: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
multiMainSetup: {
|
multiMainSetup: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Logger } from '@n8n/backend-common';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -5,6 +6,8 @@ import type { InstanceSettings } from '@/instance-settings';
|
|||||||
|
|
||||||
import { ScheduledTaskManager } from '../scheduled-task-manager';
|
import { ScheduledTaskManager } from '../scheduled-task-manager';
|
||||||
|
|
||||||
|
const logger = mock<Logger>({ scoped: jest.fn().mockReturnValue(mock<Logger>()) });
|
||||||
|
|
||||||
describe('ScheduledTaskManager', () => {
|
describe('ScheduledTaskManager', () => {
|
||||||
const instanceSettings = mock<InstanceSettings>({ isLeader: true });
|
const instanceSettings = mock<InstanceSettings>({ isLeader: true });
|
||||||
const workflow = mock<Workflow>({ timezone: 'GMT' });
|
const workflow = mock<Workflow>({ timezone: 'GMT' });
|
||||||
@@ -16,14 +19,14 @@ describe('ScheduledTaskManager', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
scheduledTaskManager = new ScheduledTaskManager(instanceSettings);
|
scheduledTaskManager = new ScheduledTaskManager(instanceSettings, logger, mock());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when workflow timezone is invalid', () => {
|
it('should throw when workflow timezone is invalid', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
scheduledTaskManager.registerCron(
|
scheduledTaskManager.registerCron(
|
||||||
mock<Workflow>({ timezone: 'somewhere' }),
|
mock<Workflow>({ timezone: 'somewhere' }),
|
||||||
everyMinute,
|
{ expression: everyMinute },
|
||||||
onTick,
|
onTick,
|
||||||
),
|
),
|
||||||
).toThrow('Invalid timezone.');
|
).toThrow('Invalid timezone.');
|
||||||
@@ -36,17 +39,21 @@ describe('ScheduledTaskManager', () => {
|
|||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should register valid CronJobs', async () => {
|
it('should register valid CronJobs', () => {
|
||||||
scheduledTaskManager.registerCron(workflow, everyMinute, onTick);
|
scheduledTaskManager.registerCron(workflow, { expression: everyMinute }, onTick);
|
||||||
|
|
||||||
expect(onTick).not.toHaveBeenCalled();
|
expect(onTick).not.toHaveBeenCalled();
|
||||||
jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes
|
jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes
|
||||||
expect(onTick).toHaveBeenCalledTimes(10);
|
expect(onTick).toHaveBeenCalledTimes(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should should not invoke on follower instances', async () => {
|
it('should not invoke on follower instances', () => {
|
||||||
scheduledTaskManager = new ScheduledTaskManager(mock<InstanceSettings>({ isLeader: false }));
|
scheduledTaskManager = new ScheduledTaskManager(
|
||||||
scheduledTaskManager.registerCron(workflow, everyMinute, onTick);
|
mock<InstanceSettings>({ isLeader: false }),
|
||||||
|
logger,
|
||||||
|
mock(),
|
||||||
|
);
|
||||||
|
scheduledTaskManager.registerCron(workflow, { expression: everyMinute }, onTick);
|
||||||
|
|
||||||
expect(onTick).not.toHaveBeenCalled();
|
expect(onTick).not.toHaveBeenCalled();
|
||||||
jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes
|
jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes
|
||||||
@@ -54,18 +61,26 @@ describe('ScheduledTaskManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should deregister CronJobs for a workflow', async () => {
|
it('should deregister CronJobs for a workflow', async () => {
|
||||||
scheduledTaskManager.registerCron(workflow, everyMinute, onTick);
|
scheduledTaskManager.registerCron(workflow, { expression: everyMinute }, onTick);
|
||||||
scheduledTaskManager.registerCron(workflow, everyMinute, onTick);
|
scheduledTaskManager.registerCron(workflow, { expression: everyMinute }, onTick);
|
||||||
scheduledTaskManager.registerCron(workflow, everyMinute, onTick);
|
scheduledTaskManager.registerCron(workflow, { expression: everyMinute }, onTick);
|
||||||
|
|
||||||
expect(scheduledTaskManager.cronJobs.get(workflow.id)?.length).toBe(3);
|
expect(scheduledTaskManager.cronMap.get(workflow.id)).toHaveLength(3);
|
||||||
|
|
||||||
scheduledTaskManager.deregisterCrons(workflow.id);
|
scheduledTaskManager.deregisterCrons(workflow.id);
|
||||||
|
|
||||||
expect(scheduledTaskManager.cronJobs.get(workflow.id)?.length).toBe(0);
|
expect(scheduledTaskManager.cronMap.get(workflow.id)).toBeUndefined();
|
||||||
|
|
||||||
expect(onTick).not.toHaveBeenCalled();
|
expect(onTick).not.toHaveBeenCalled();
|
||||||
jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes
|
jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes
|
||||||
expect(onTick).not.toHaveBeenCalled();
|
expect(onTick).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not set up log interval when activeInterval is 0', () => {
|
||||||
|
const configWithZeroInterval = mock({ activeInterval: 0 });
|
||||||
|
const manager = new ScheduledTaskManager(instanceSettings, logger, configWithZeroInterval);
|
||||||
|
|
||||||
|
// @ts-expect-error Private property
|
||||||
|
expect(manager.logInterval).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export class ActiveWorkflows {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get all the trigger times
|
// Get all the trigger times
|
||||||
const cronTimes = (pollTimes.item || []).map(toCronExpression);
|
const cronExpressions = (pollTimes.item || []).map(toCronExpression);
|
||||||
// The trigger function to execute when the cron-time got reached
|
// The trigger function to execute when the cron-time got reached
|
||||||
const executeTrigger = async (testingTrigger = false) => {
|
const executeTrigger = async (testingTrigger = false) => {
|
||||||
this.logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, {
|
this.logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, {
|
||||||
@@ -177,15 +177,15 @@ export class ActiveWorkflows {
|
|||||||
// Execute the trigger directly to be able to know if it works
|
// Execute the trigger directly to be able to know if it works
|
||||||
await executeTrigger(true);
|
await executeTrigger(true);
|
||||||
|
|
||||||
for (const cronTime of cronTimes) {
|
for (const expression of cronExpressions) {
|
||||||
const cronTimeParts = cronTime.split(' ');
|
const cronTimeParts = expression.split(' ');
|
||||||
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
|
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
|
||||||
throw new ApplicationError(
|
throw new ApplicationError(
|
||||||
'The polling interval is too short. It has to be at least a minute.',
|
'The polling interval is too short. It has to be at least a minute.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scheduledTaskManager.registerCron(workflow, cronTime, executeTrigger);
|
this.scheduledTaskManager.registerCron(workflow, { expression }, executeTrigger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ describe('getSchedulingFunctions', () => {
|
|||||||
|
|
||||||
describe('registerCron', () => {
|
describe('registerCron', () => {
|
||||||
it('should invoke scheduledTaskManager.registerCron', () => {
|
it('should invoke scheduledTaskManager.registerCron', () => {
|
||||||
schedulingFunctions.registerCron(cronExpression, onTick);
|
schedulingFunctions.registerCron({ expression: cronExpression }, onTick);
|
||||||
|
|
||||||
expect(scheduledTaskManager.registerCron).toHaveBeenCalledWith(
|
expect(scheduledTaskManager.registerCron).toHaveBeenCalledWith(
|
||||||
workflow,
|
workflow,
|
||||||
cronExpression,
|
{ expression: cronExpression },
|
||||||
onTick,
|
onTick,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ScheduledTaskManager } from '../../scheduled-task-manager';
|
|||||||
export const getSchedulingFunctions = (workflow: Workflow): SchedulingFunctions => {
|
export const getSchedulingFunctions = (workflow: Workflow): SchedulingFunctions => {
|
||||||
const scheduledTaskManager = Container.get(ScheduledTaskManager);
|
const scheduledTaskManager = Container.get(ScheduledTaskManager);
|
||||||
return {
|
return {
|
||||||
registerCron: (cronExpression, onTick) =>
|
registerCron: (cron, onTick) => scheduledTaskManager.registerCron(workflow, cron, onTick),
|
||||||
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,44 +1,115 @@
|
|||||||
|
import { Logger } from '@n8n/backend-common';
|
||||||
|
import { CronLoggingConfig } from '@n8n/config';
|
||||||
|
import { Time } from '@n8n/constants';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { CronJob } from 'cron';
|
import { CronJob } from 'cron';
|
||||||
import type { CronExpression, Workflow } from 'n8n-workflow';
|
import type { Cron, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
import { InstanceSettings } from '@/instance-settings';
|
import { InstanceSettings } from '@/instance-settings';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ScheduledTaskManager {
|
export class ScheduledTaskManager {
|
||||||
constructor(private readonly instanceSettings: InstanceSettings) {}
|
readonly cronMap = new Map<string, Array<{ job: CronJob; displayableCron: string }>>();
|
||||||
|
|
||||||
readonly cronJobs = new Map<string, CronJob[]>();
|
private readonly logInterval?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly instanceSettings: InstanceSettings,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly config: CronLoggingConfig,
|
||||||
|
) {
|
||||||
|
this.logger = this.logger.scoped('cron');
|
||||||
|
|
||||||
|
if (this.config.activeInterval === 0) return;
|
||||||
|
|
||||||
|
this.logInterval = setInterval(
|
||||||
|
() => this.logActiveCrons(),
|
||||||
|
this.config.activeInterval * Time.minutes.toMilliseconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private logActiveCrons() {
|
||||||
|
const activeCrons: Record<string, string[]> = {};
|
||||||
|
for (const [workflowId, cronJobs] of this.cronMap) {
|
||||||
|
activeCrons[`workflow-${workflowId}`] = cronJobs.map(
|
||||||
|
({ displayableCron }) => displayableCron,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(activeCrons).length === 0) return;
|
||||||
|
|
||||||
|
this.logger.debug('Currently active crons', { activeCrons });
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCron(workflow: Workflow, { expression, recurrence }: Cron, onTick: () => void) {
|
||||||
|
const recurrenceStr = recurrence?.activated
|
||||||
|
? `every ${recurrence.intervalSize} ${recurrence.typeInterval}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const displayableCron = recurrenceStr ? `${expression} (${recurrenceStr})` : expression;
|
||||||
|
|
||||||
registerCron(workflow: Workflow, cronExpression: CronExpression, onTick: () => void) {
|
|
||||||
const cronJob = new CronJob(
|
const cronJob = new CronJob(
|
||||||
cronExpression,
|
expression,
|
||||||
() => {
|
() => {
|
||||||
if (this.instanceSettings.isLeader) onTick();
|
if (this.instanceSettings.isLeader) {
|
||||||
|
this.logger.debug('Executing cron for workflow', {
|
||||||
|
workflowId: workflow.id,
|
||||||
|
cron: displayableCron,
|
||||||
|
instanceRole: this.instanceSettings.instanceRole,
|
||||||
|
});
|
||||||
|
onTick();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
workflow.timezone,
|
workflow.timezone,
|
||||||
);
|
);
|
||||||
const cronJobsForWorkflow = this.cronJobs.get(workflow.id);
|
|
||||||
if (cronJobsForWorkflow) {
|
const workflowCronEntries = this.cronMap.get(workflow.id);
|
||||||
cronJobsForWorkflow.push(cronJob);
|
const cronEntry = { job: cronJob, displayableCron };
|
||||||
|
|
||||||
|
if (workflowCronEntries) {
|
||||||
|
workflowCronEntries.push(cronEntry);
|
||||||
} else {
|
} else {
|
||||||
this.cronJobs.set(workflow.id, [cronJob]);
|
this.cronMap.set(workflow.id, [cronEntry]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug('Registered cron for workflow', {
|
||||||
|
workflowId: workflow.id,
|
||||||
|
cron: displayableCron,
|
||||||
|
instanceRole: this.instanceSettings.instanceRole,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deregisterCrons(workflowId: string) {
|
deregisterCrons(workflowId: string) {
|
||||||
const cronJobs = this.cronJobs.get(workflowId) ?? [];
|
const cronJobs = this.cronMap.get(workflowId) ?? [];
|
||||||
|
|
||||||
|
if (cronJobs.length === 0) return;
|
||||||
|
|
||||||
|
const crons: string[] = [];
|
||||||
|
|
||||||
while (cronJobs.length) {
|
while (cronJobs.length) {
|
||||||
const cronJob = cronJobs.pop();
|
const cronEntry = cronJobs.pop();
|
||||||
if (cronJob) cronJob.stop();
|
if (cronEntry) {
|
||||||
|
crons.push(cronEntry.displayableCron);
|
||||||
|
cronEntry.job.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cronMap.delete(workflowId);
|
||||||
|
|
||||||
|
this.logger.info('Deregistered all crons for workflow', {
|
||||||
|
workflowId,
|
||||||
|
crons,
|
||||||
|
instanceRole: this.instanceSettings.instanceRole,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deregisterAllCrons() {
|
deregisterAllCrons() {
|
||||||
for (const workflowId of Object.keys(this.cronJobs)) {
|
for (const workflowId of this.cronMap.keys()) {
|
||||||
this.deregisterCrons(workflowId);
|
this.deregisterCrons(workflowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.logInterval) clearInterval(this.logInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class Cron implements INodeType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get all the trigger times
|
// Get all the trigger times
|
||||||
const cronTimes = (triggerTimes.item || []).map(toCronExpression);
|
const expressions = (triggerTimes.item || []).map(toCronExpression);
|
||||||
|
|
||||||
// The trigger function to execute when the cron-time got reached
|
// The trigger function to execute when the cron-time got reached
|
||||||
// or when manually triggered
|
// or when manually triggered
|
||||||
@@ -65,7 +65,7 @@ export class Cron implements INodeType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Register the cron-jobs
|
// Register the cron-jobs
|
||||||
cronTimes.forEach((cronTime) => this.helpers.registerCron(cronTime, executeTrigger));
|
expressions.forEach((expression) => this.helpers.registerCron({ expression }, executeTrigger));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
manualTriggerFunction: async () => executeTrigger(),
|
manualTriggerFunction: async () => executeTrigger(),
|
||||||
|
|||||||
@@ -451,7 +451,8 @@ export class ScheduleTrigger implements INodeType {
|
|||||||
if (this.getMode() !== 'manual') {
|
if (this.getMode() !== 'manual') {
|
||||||
for (const { interval, cronExpression, recurrence } of rules) {
|
for (const { interval, cronExpression, recurrence } of rules) {
|
||||||
try {
|
try {
|
||||||
this.helpers.registerCron(cronExpression, () => executeTrigger(recurrence));
|
const cron = { expression: cronExpression, recurrence };
|
||||||
|
this.helpers.registerCron(cron, () => executeTrigger(recurrence));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (interval.field === 'cronExpression') {
|
if (interval.field === 'cronExpression') {
|
||||||
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
|
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ import {
|
|||||||
type Workflow,
|
type Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
const logger = mock({
|
||||||
|
scoped: jest.fn().mockReturnValue(
|
||||||
|
mock({
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
type MockDeepPartial<T> = Parameters<typeof mock<T>>[0];
|
type MockDeepPartial<T> = Parameters<typeof mock<T>>[0];
|
||||||
|
|
||||||
type TestTriggerNodeOptions = {
|
type TestTriggerNodeOptions = {
|
||||||
@@ -70,7 +81,11 @@ export async function testTriggerNode(
|
|||||||
) as INode;
|
) as INode;
|
||||||
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' });
|
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' });
|
||||||
|
|
||||||
const scheduledTaskManager = new ScheduledTaskManager(mock<InstanceSettings>());
|
const scheduledTaskManager = new ScheduledTaskManager(
|
||||||
|
mock<InstanceSettings>(),
|
||||||
|
logger as any,
|
||||||
|
mock(),
|
||||||
|
);
|
||||||
const helpers = mock<ITriggerFunctions['helpers']>({
|
const helpers = mock<ITriggerFunctions['helpers']>({
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
returnJsonArray,
|
returnJsonArray,
|
||||||
@@ -130,7 +145,11 @@ export async function testWebhookTriggerNode(
|
|||||||
) as INode;
|
) as INode;
|
||||||
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' });
|
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' });
|
||||||
|
|
||||||
const scheduledTaskManager = new ScheduledTaskManager(mock<InstanceSettings>());
|
const scheduledTaskManager = new ScheduledTaskManager(
|
||||||
|
mock<InstanceSettings>(),
|
||||||
|
logger as any,
|
||||||
|
mock(),
|
||||||
|
);
|
||||||
const helpers = mock<ITriggerFunctions['helpers']>({
|
const helpers = mock<ITriggerFunctions['helpers']>({
|
||||||
returnJsonArray,
|
returnJsonArray,
|
||||||
registerCron: (cronExpression, onTick) =>
|
registerCron: (cronExpression, onTick) =>
|
||||||
|
|||||||
@@ -843,8 +843,19 @@ type CronUnit = number | '*' | `*/${number}`;
|
|||||||
export type CronExpression =
|
export type CronExpression =
|
||||||
`${CronUnit} ${CronUnit} ${CronUnit} ${CronUnit} ${CronUnit} ${CronUnit}`;
|
`${CronUnit} ${CronUnit} ${CronUnit} ${CronUnit} ${CronUnit} ${CronUnit}`;
|
||||||
|
|
||||||
|
type RecurrenceRule =
|
||||||
|
| { activated: false }
|
||||||
|
| {
|
||||||
|
activated: true;
|
||||||
|
index: number;
|
||||||
|
intervalSize: number;
|
||||||
|
typeInterval: 'hours' | 'days' | 'weeks' | 'months';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Cron = { expression: CronExpression; recurrence?: RecurrenceRule };
|
||||||
|
|
||||||
export interface SchedulingFunctions {
|
export interface SchedulingFunctions {
|
||||||
registerCron(cronExpression: CronExpression, onTick: () => void): void;
|
registerCron(cron: Cron, onTick: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeTypeAndVersion = {
|
export type NodeTypeAndVersion = {
|
||||||
|
|||||||
Reference in New Issue
Block a user