// Task runner with retry, timeout, and structured logging import { createLogger } from "./logger.mjs"; import { PrismaClient } from "@prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; const logger = createLogger("task-runner"); function createPrisma() { const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://"); const sep = base.includes("?") ? "&" : "?"; const connectionString = `${base}${sep}connection_limit=3&pool_timeout=30`; const adapter = new PrismaMariaDb(connectionString); return new PrismaClient({ adapter }); } async function runTask(taskKey, taskName, fn, { timeoutMs = 30 * 60 * 1000, maxRetries = 2 } = {}) { const prisma = createPrisma(); const startedAt = new Date(); let attempt = 0; let lastError; logger.info(`Starting task: ${taskName}`, { taskKey }); while (attempt <= maxRetries) { if (attempt > 0) { const delay = Math.min(5000 * Math.pow(2, attempt - 1), 30000); logger.warn(`Retry ${attempt}/${maxRetries} for ${taskName} after ${delay}ms`, { taskKey }); await new Promise((r) => setTimeout(r, delay)); } try { // Timeout wrapper const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Task timed out after ${timeoutMs / 60000} minutes`)), timeoutMs) ); const result = await Promise.race([fn(prisma), timeoutPromise]); // Log success await prisma.taskLog.create({ data: { taskKey, taskName, status: "success", startedAt, finishedAt: new Date(), duration: Date.now() - startedAt.getTime(), triggerBy: "cron", result: result ? JSON.parse(JSON.stringify(result).slice(0, 2000)) : undefined, }, }).catch(() => {}); logger.info(`Task completed: ${taskName}`, { taskKey, durationMs: Date.now() - startedAt.getTime(), attempt, }); await prisma.$disconnect(); return result; } catch (error) { lastError = error; attempt++; logger.error(`Task attempt failed: ${taskName}`, { taskKey, attempt, error: error.message, }); } } // All retries exhausted const errorMsg = (lastError?.message || String(lastError)).slice(0, 1000); await prisma.taskLog.create({ data: { taskKey, taskName, status: "failed", startedAt, finishedAt: new Date(), duration: Date.now() - startedAt.getTime(), triggerBy: "cron", error: errorMsg, }, }).catch(() => {}); logger.error(`Task failed after ${maxRetries + 1} attempts: ${taskName}`, { taskKey, error: errorMsg, }); await prisma.$disconnect(); throw lastError; } export { runTask, createLogger };