Files
lst_v3/backend/utils/croner.utils.ts

179 lines
4.9 KiB
TypeScript

import { Cron } from "croner";
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { jobAuditLog } from "../db/schema/auditLog.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import type { ReturnHelper } from "./returnHelper.utils.js";
// example createJob
// createCronJob("test Cron", "*/5 * * * * *", async () => {
// console.log("help");
// });
export interface JobInfo {
name: string;
schedule: string;
nextRun: Date | null;
isRunning: boolean;
}
// Store running cronjobs
export const runningCrons: Record<string, Cron> = {};
const activeRuns = new Set<string>();
const log = createLogger({ module: "system", subModule: "croner" });
const cronStats: Record<string, { created: number; replaced: number }> = {};
// how to se the times
// * ┌──────────────── (optional) second (0 - 59)
// * │ ┌────────────── minute (0 - 59)
// * │ │ ┌──────────── hour (0 - 23)
// * │ │ │ ┌────────── day of month (1 - 31)
// * │ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
// * │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon)
// * │ │ │ │ │ │ (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
// * │ │ │ │ │ │ ┌──── (optional) year (1 - 9999)
// * │ │ │ │ │ │ │
// * * 05 * * * * *
// note that when leaving * anywhere means ever part of that. IE * in the seconds part will run every second inside that min
/**
*
* @param name Name of the job we want to run
* @param schedule Cron expression (example: `*\/5 * * * * *`)
* @param task Async function that will run
* @param source we can add where it came from to assist in getting this tracked down, more for debugging
*/
export const createCronJob = async (
name: string,
schedule: string, // cron string with 8 8 IE: */5 * * * * * every 5th second
task: () => Promise<void | ReturnHelper>, // what function are we passing over
source = "unknown",
) => {
// get the timezone based on the os timezone set
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// initial go so just store it this is more for debugging to see if something crazy keeps happening
if (!cronStats[name]) {
cronStats[name] = { created: 0, replaced: 0 };
}
// Destroy existing job if it exist
if (runningCrons[name]) {
cronStats[name].replaced += 1;
log.warn(
{
job: name,
source,
oldSchedule: runningCrons[name].getPattern?.(),
newSchedule: schedule,
replaceCount: cronStats[name].replaced,
},
`Cron job "${name}" already existed and is being replaced`,
);
runningCrons[name].stop();
}
// Create new job with Croner
runningCrons[name] = new Cron(
schedule,
{
timezone: timeZone,
catch: true, // Prevents unhandled rejections
name: name,
},
async () => {
if (activeRuns.has(name)) {
log.warn({ jobName: name }, "Skipping overlapping cron execution");
return;
}
activeRuns.add(name);
const startedAt = new Date();
const start = Date.now();
let executionId: string = "";
try {
const [execution] = await db
.insert(jobAuditLog)
.values({
jobName: name,
startedAt,
status: "running",
})
.returning();
executionId = execution?.id as string;
await task?.();
// tell it we done
await db
.update(jobAuditLog)
.set({
finishedAt: new Date(),
durationMs: Date.now() - start,
status: "success",
})
.where(eq(jobAuditLog.id, executionId));
} catch (e: any) {
if (executionId) {
await db
.update(jobAuditLog)
.set({
finishedAt: new Date(),
durationMs: Date.now() - start,
status: "error",
errorMessage: e.message,
errorStack: e.stack,
})
.where(eq(jobAuditLog.id, executionId));
}
} finally {
activeRuns.delete(name);
}
},
);
log.info({}, `A job for ${name} was just created.`);
};
export const getAllJobs = (): JobInfo[] => {
return Object.entries(runningCrons).map(([name, job]) => ({
name,
schedule: job.getPattern() || "invalid",
nextRun: job.nextRun() || null,
lastRun: job.previousRun() || null,
isRunning: job.isRunning(), //job ? !job.isStopped() : false,
}));
};
export const removeCronJob = (name: string) => {
if (runningCrons[name]) {
runningCrons[name].stop();
delete runningCrons[name];
}
};
export const stopCronJob = (name: string) => {
if (runningCrons[name]) {
runningCrons[name].pause();
log.info(
{},
`${name} was just stopped either manually or due to a setting change.`,
);
}
};
export const resumeCronJob = (name: string) => {
if (runningCrons[name]) {
runningCrons[name].resume();
log.info(
{},
`${name} was just restarted either manually or due to a setting change.`,
);
}
};