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 = {}; const activeRuns = new Set(); const log = createLogger({ module: "system", subModule: "croner" }); const cronStats: Record = {}; // 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, // 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.`, ); } };