feat(tasks): allow management of job intervals and disabling them (#3408)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { AxiosError } from "axios";
|
||||
import type { ScheduledTask } from "node-cron";
|
||||
import { schedule, validate } from "node-cron";
|
||||
import { createTask, validate } from "node-cron";
|
||||
|
||||
import { Stopwatch } from "@homarr/common";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
import type { Logger } from "./logger";
|
||||
import type { ValidateCron } from "./validation";
|
||||
@@ -18,13 +18,14 @@ export interface CreateCronJobCreatorOptions<TAllowedNames extends string> {
|
||||
|
||||
interface CreateCronJobOptions {
|
||||
runOnStart?: boolean;
|
||||
preventManualExecution?: boolean;
|
||||
expectedMaximumDurationInMillis?: number;
|
||||
beforeStart?: () => MaybePromise<void>;
|
||||
}
|
||||
|
||||
const createCallback = <TAllowedNames extends string, TName extends TAllowedNames>(
|
||||
name: TName,
|
||||
cronExpression: string,
|
||||
defaultCronExpression: string,
|
||||
options: CreateCronJobOptions,
|
||||
creatorOptions: CreateCronJobCreatorOptions<TAllowedNames>,
|
||||
) => {
|
||||
@@ -63,25 +64,30 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* We are not using the runOnInit method as we want to run the job only once we start the cron job schedule manually.
|
||||
* This allows us to always run it once we start it. Additionally, it will not run the callback if only the cron job file is imported.
|
||||
*/
|
||||
let scheduledTask: ScheduledTask | null = null;
|
||||
if (cronExpression !== "never") {
|
||||
scheduledTask = schedule(cronExpression, () => void catchingCallbackAsync(), {
|
||||
name,
|
||||
timezone: creatorOptions.timezone,
|
||||
});
|
||||
creatorOptions.logger.logDebug(
|
||||
`The cron job '${name}' was created with expression ${cronExpression} in timezone ${creatorOptions.timezone} and runOnStart ${options.runOnStart}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
cronExpression,
|
||||
scheduledTask,
|
||||
cronExpression: defaultCronExpression,
|
||||
async createTaskAsync() {
|
||||
const configuration = await db.query.cronJobConfigurations.findFirst({
|
||||
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
|
||||
});
|
||||
|
||||
if (defaultCronExpression === "never") return null;
|
||||
|
||||
const scheduledTask = createTask(
|
||||
configuration?.cronExpression ?? defaultCronExpression,
|
||||
() => void catchingCallbackAsync(),
|
||||
{
|
||||
name,
|
||||
timezone: creatorOptions.timezone,
|
||||
},
|
||||
);
|
||||
creatorOptions.logger.logDebug(
|
||||
`The cron job '${name}' was created with expression ${defaultCronExpression} in timezone ${creatorOptions.timezone} and runOnStart ${options.runOnStart}`,
|
||||
);
|
||||
|
||||
return scheduledTask;
|
||||
},
|
||||
async onStartAsync() {
|
||||
if (options.beforeStart) {
|
||||
creatorOptions.logger.logDebug(`Running beforeStart for job: ${name}`);
|
||||
@@ -93,6 +99,10 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
creatorOptions.logger.logDebug(`The cron job '${name}' is running because runOnStart is set to true`);
|
||||
await catchingCallbackAsync();
|
||||
},
|
||||
async executeAsync() {
|
||||
await catchingCallbackAsync();
|
||||
},
|
||||
preventManualExecution: options.preventManualExecution ?? false,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -106,17 +116,17 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
||||
) => {
|
||||
return <TName extends TAllowedNames, TExpression extends string>(
|
||||
name: TName,
|
||||
cronExpression: TExpression,
|
||||
defaultCronExpression: TExpression,
|
||||
options: CreateCronJobOptions = { runOnStart: false },
|
||||
) => {
|
||||
creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`);
|
||||
if (cronExpression !== "never" && !validate(cronExpression)) {
|
||||
throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`);
|
||||
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`);
|
||||
if (defaultCronExpression !== "never" && !validate(defaultCronExpression)) {
|
||||
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
||||
}
|
||||
creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`);
|
||||
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`);
|
||||
|
||||
const returnValue = {
|
||||
withCallback: createCallback<TAllowedNames, TName>(name, cronExpression, options, creatorOptions),
|
||||
withCallback: createCallback<TAllowedNames, TName>(name, defaultCronExpression, options, creatorOptions),
|
||||
};
|
||||
|
||||
// This is a type guard to check if the cron expression is valid and give the user a type hint
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ScheduledTask } from "node-cron";
|
||||
|
||||
import { objectEntries, objectKeys } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
import type { JobCallback } from "./creator";
|
||||
import type { Logger } from "./logger";
|
||||
@@ -27,45 +30,78 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
});
|
||||
}
|
||||
|
||||
const tasks = new Map<string, ScheduledTask>();
|
||||
|
||||
return {
|
||||
initializeAsync: async () => {
|
||||
const configurations = await db.query.cronJobConfigurations.findMany();
|
||||
for (const job of jobRegistry.values()) {
|
||||
const configuration = configurations.find(({ name }) => name === job.name);
|
||||
if (configuration?.isEnabled === false) {
|
||||
continue;
|
||||
}
|
||||
if (tasks.has(job.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scheduledTask = await job.createTaskAsync();
|
||||
if (!scheduledTask) continue;
|
||||
|
||||
tasks.set(job.name, scheduledTask);
|
||||
}
|
||||
},
|
||||
startAsync: async (name: keyof TJobs) => {
|
||||
const job = jobRegistry.get(name as string);
|
||||
if (!job) return;
|
||||
if (!tasks.has(job.name)) return;
|
||||
|
||||
options.logger.logInfo(`Starting schedule cron job ${job.name}.`);
|
||||
await job.onStartAsync();
|
||||
await job.scheduledTask?.start();
|
||||
await tasks.get(name as string)?.start();
|
||||
},
|
||||
startAllAsync: async () => {
|
||||
for (const job of jobRegistry.values()) {
|
||||
if (!tasks.has(job.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.logger.logInfo(`Starting schedule of cron job ${job.name}.`);
|
||||
await job.onStartAsync();
|
||||
await job.scheduledTask?.start();
|
||||
await tasks.get(job.name)?.start();
|
||||
}
|
||||
},
|
||||
runManuallyAsync: async (name: keyof TJobs) => {
|
||||
const job = jobRegistry.get(name as string);
|
||||
if (!job) return;
|
||||
if (job.preventManualExecution) {
|
||||
throw new Error(`The job "${job.name}" can not be executed manually.`);
|
||||
}
|
||||
|
||||
options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
|
||||
await job.scheduledTask?.execute();
|
||||
await tasks.get(name as string)?.execute();
|
||||
},
|
||||
stopAsync: async (name: keyof TJobs) => {
|
||||
const job = jobRegistry.get(name as string);
|
||||
if (!job) return;
|
||||
|
||||
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
|
||||
await job.scheduledTask?.stop();
|
||||
await tasks.get(name as string)?.stop();
|
||||
},
|
||||
stopAllAsync: async () => {
|
||||
for (const job of jobRegistry.values()) {
|
||||
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
|
||||
await job.scheduledTask?.stop();
|
||||
await tasks.get(job.name)?.stop();
|
||||
}
|
||||
},
|
||||
getJobRegistry() {
|
||||
return jobRegistry as Map<TAllowedNames, ReturnType<JobCallback<TAllowedNames, TAllowedNames>>>;
|
||||
},
|
||||
getTask(name: keyof TJobs) {
|
||||
return tasks.get(name as string) ?? null;
|
||||
},
|
||||
setTask(name: keyof TJobs, task: ScheduledTask) {
|
||||
tasks.set(name as string, task);
|
||||
},
|
||||
getKeys() {
|
||||
return objectKeys(jobs);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user