run-cron-job.js
1 #!/usr/bin/env node 2 3 /** 4 * Run a single cron job on-demand 5 * 6 * Usage: node scripts/run-cron-job.js <task_key> 7 */ 8 9 import { createDatabaseConnection } from '../src/utils/db.js'; 10 import { join, dirname } from 'path'; 11 import { fileURLToPath } from 'url'; 12 import Logger from '../src/utils/logger.js'; 13 import dotenv from 'dotenv'; 14 15 dotenv.config(); 16 17 const __filename = fileURLToPath(import.meta.url); 18 const __dirname = dirname(__filename); 19 const projectRoot = join(__dirname, '..'); 20 21 const logger = new Logger('RunCronJob'); 22 const dbPath = process.env.DATABASE_PATH || join(projectRoot, 'db/sites.db'); 23 24 /** 25 * Log task execution to database 26 */ 27 function logTaskStart(db, taskName) { 28 const stmt = db.prepare(` 29 INSERT INTO ops.cron_job_logs (job_name, started_at, status, items_processed, items_failed) 30 VALUES (?, datetime('now'), 'running', 0, 0) 31 `); 32 const info = stmt.run(taskName); 33 return info.lastInsertRowid; 34 } 35 36 /** 37 * Update task log on completion 38 */ 39 function logTaskComplete(db, logId, result) { 40 const summary = result?.summary || 'Task completed'; 41 const fullLog = JSON.stringify(result, null, 2); 42 43 let itemsProcessed = 1; 44 if (result?.metrics?.processed !== undefined) { 45 itemsProcessed = result.metrics.processed; 46 } else if (result?.items_processed !== undefined) { 47 itemsProcessed = result.items_processed; 48 } else if (result?.processed !== undefined) { 49 itemsProcessed = result.processed; 50 } 51 52 db.prepare( 53 ` 54 UPDATE ops.cron_job_logs 55 SET finished_at = datetime('now'), 56 status = 'success', 57 summary = ?, 58 full_log = ?, 59 items_processed = ? 60 WHERE id = ? 61 ` 62 ).run(summary, fullLog, itemsProcessed, logId); 63 } 64 65 /** 66 * Update task log on failure 67 */ 68 function logTaskFailed(db, logId, error) { 69 const errorName = error.name || 'Error'; 70 const summary = `${errorName}: ${error.message}`; 71 72 const fullLog = JSON.stringify( 73 { 74 summary, 75 error: error.message, 76 details: error.details || { 77 stack: error.stack, 78 }, 79 metrics: { 80 success: 0, 81 failed: 1, 82 }, 83 }, 84 null, 85 2 86 ); 87 88 db.prepare( 89 ` 90 UPDATE ops.cron_job_logs 91 SET finished_at = datetime('now'), 92 status = 'failed', 93 summary = ?, 94 full_log = ?, 95 error_message = ?, 96 items_failed = 1 97 WHERE id = ? 98 ` 99 ).run(summary, fullLog, error.message, logId); 100 } 101 102 /** 103 * Execute a command-based job 104 */ 105 async function executeCommand(command) { 106 const { spawn } = await import('child_process'); 107 const startTime = Date.now(); 108 109 return new Promise((resolve, reject) => { 110 const [cmd, ...args] = command.split(' '); 111 112 const proc = spawn(cmd, args, { 113 cwd: projectRoot, 114 stdio: 'inherit', // Show output in real-time 115 env: process.env, 116 }); 117 118 proc.on('close', code => { 119 const duration = ((Date.now() - startTime) / 1000).toFixed(2); 120 121 if (code === 0) { 122 resolve({ 123 summary: `Command completed in ${duration}s`, 124 details: { 125 duration_seconds: parseFloat(duration), 126 exit_code: code, 127 command, 128 }, 129 metrics: { 130 duration_seconds: parseFloat(duration), 131 }, 132 }); 133 } else { 134 const error = new Error(`Command exited with code ${code}`); 135 error.details = { 136 exit_code: code, 137 command, 138 }; 139 reject(error); 140 } 141 }); 142 }); 143 } 144 145 /** 146 * Run a specific job by task_key 147 */ 148 async function runJob(taskKey) { 149 const db = createDatabaseConnection(dbPath); 150 151 try { 152 // Check circuit breaker - global kill switch for all cron jobs 153 // Check settings table first (dashboard-toggleable), fall back to .env 154 const circuitBreakerSetting = db 155 .prepare('SELECT value FROM ops.settings WHERE key = ?') 156 .get('cron_circuit_breaker_enabled'); 157 const circuitBreakerEnabled = 158 circuitBreakerSetting?.value !== 'false' && 159 process.env.CRON_CIRCUIT_BREAKER_ENABLED !== 'false'; 160 161 if (!circuitBreakerEnabled) { 162 logger.warn( 163 '⚠️ Circuit breaker is DISABLED - job execution blocked. Enable it via dashboard or CLI.' 164 ); 165 return 1; 166 } 167 168 // Get job details 169 const job = db.prepare('SELECT * FROM ops.cron_jobs WHERE task_key = ?').get(taskKey); 170 171 if (!job) { 172 logger.error(`Job not found: ${taskKey}`); 173 return 1; 174 } 175 176 logger.info(`Running job: ${job.name}`); 177 178 // Log task start 179 const logId = logTaskStart(db, job.name); 180 181 try { 182 const start = Date.now(); 183 let result; 184 185 if (job.handler_type === 'function') { 186 // Import cron to get HANDLERS 187 const cronModule = await import('../src/cron.js'); 188 const handler = cronModule.default.HANDLERS[job.task_key]; 189 190 if (!handler) { 191 throw new Error(`Handler function not found: ${job.task_key}`); 192 } 193 194 result = await handler(); 195 } else if (job.handler_type === 'command') { 196 result = await executeCommand(job.handler_value); 197 } else { 198 throw new Error(`Unknown handler type: ${job.handler_type}`); 199 } 200 201 const duration = ((Date.now() - start) / 1000).toFixed(2); 202 203 // Log task completion 204 logTaskComplete(db, logId, result); 205 206 // Update last_run_at 207 db.prepare("UPDATE ops.cron_jobs SET last_run_at = datetime('now') WHERE task_key = ?").run( 208 taskKey 209 ); 210 211 logger.success(`✓ ${job.name} completed in ${duration}s`); 212 return 0; 213 } catch (error) { 214 logger.error(`✗ ${job.name} failed:`, error); 215 216 // Log task failure 217 logTaskFailed(db, logId, error); 218 219 // Update last_run_at even on failure to prevent immediate retries 220 db.prepare("UPDATE ops.cron_jobs SET last_run_at = datetime('now') WHERE task_key = ?").run( 221 taskKey 222 ); 223 224 return 1; 225 } 226 } finally { 227 db.close(); 228 } 229 } 230 231 // Main 232 const taskKey = process.argv[2]; 233 234 if (!taskKey) { 235 console.error('Usage: node scripts/run-cron-job.js <task_key>'); 236 process.exit(1); 237 } 238 239 runJob(taskKey) 240 .then(code => process.exit(code)) 241 .catch(error => { 242 console.error('Fatal error:', error); 243 process.exit(1); 244 });