'use strict'; const nconf = require('nconf'); const { CronJob } = require('cron'); const cronstrue = require('cronstrue'); const winston = require('winston'); const db = require('./database'); const utils = require('./utils'); const jobs = Object.create(null); exports.markJobsInactive = async function () { const jobs = await db.getSortedSetRange('cronJobs', 0, -1); await db.setObject(jobs.map(name => `cronJob:${name}`), { active: 0 }); }; exports.addJob = async function (options) { const { name, cronTime, onTick, onComplete = null, start = true, runOnInit = false, runOnAllNodes = false, } = options; const isJobEnabled = nconf.get('runJobs'); if (!isJobEnabled && !runOnAllNodes) { return; } if (!name || !cronTime || typeof onTick !== 'function') { throw new Error('[cron] Invalid options'); } if (Object.hasOwn(jobs, name)) { throw new Error('[cron] Job with that name already exists'); } const job = new CronJob(cronTime, async function () { const start = Date.now(); try { await db.setObjectField(`cronJob:${name}`, 'running', 1); await onTick(); await db.deleteObjectField(`cronJob:${name}`, 'lastError'); } catch (err) { winston.error(`[cron] ${err.stack}`); await db.setObjectField(`cronJob:${name}`, 'lastError', err.stack); } finally { await db.setObject(`cronJob:${name}`, { running: 0, duration: Date.now() - start, nextRun: job.nextDate().toMillis(), }); } }, onComplete, start, null, null, runOnInit); jobs[name] = job; await db.sortedSetAdd('cronJobs', Date.now(), name); await db.setObject(`cronJob:${name}`, { name, cronTime, cronTimeHuman: cronstrue.toString(cronTime), nextRun: job.nextDate().toMillis(), running: runOnInit ? 1 : 0, active: 1, }); winston.verbose(`[cron/jobs] Registered job: ${name} (${cronTime})`); return job; }; exports.getJobs = async function () { const jobNames = await db.getSortedSetRange('cronJobs', 0, -1); const jobs = await db.getObjects(jobNames.map(name => `cronJob:${name}`)); jobs.forEach((job) => { if (job) { job.active = parseInt(job.active, 10) === 1; job.running = parseInt(job.running, 10) === 1; job.duration = job.duration || 0; job.durationReadable = formatDuration(job.duration); job.nextRunISO = utils.toISOString(job.nextRun); } }); jobs.sort((a, b) => b.cronTimeHuman.localeCompare(a.cronTimeHuman)); return jobs; }; function formatDuration(ms) { const totalSeconds = Math.floor(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; if (minutes > 0) { return `${minutes}m${String(seconds).padStart(2, '0')}s`; } return `${seconds}s`; }