/*********************************************************************************************************************************************************************** * File Name : retry_stuck_transcodes.cron.js * Type : Cron Job * Description : Safety net for the .mov/.mkv -> faststart .mp4 background * remux (see services/assetTranscode.service.js). Picks up: * - "pending" — the fire-and-forget call in * assets.controller.js#finalizeAssetFromStorage * never actually started (e.g. this process * crashed between the DB commit and the call). * - "processing" for over 30 minutes — the job itself was * running when the process restarted/crashed * mid-remux and never got to flip the status. * * Processes at most 3 per run, sequentially — this runs on a * small droplet, and remuxing is disk/CPU-bound; no reason to * pile up concurrent ffmpeg processes for a background sweep. * * Schedule : Every 10 minutes ("*\/10 * * * *"). Registered by * cron/admin.cron.js. * * Author: Kenneth Obsequio * Date Created: Aug. 1, 2026 ***********************************************************************************************************************************************************************/ const { Op } = require('sequelize'); const mdl_Assets = require('../../models/assets/assets.mdl'); const { transcodeAsset } = require('../../services/assetTranscode.service'); const MAX_PER_RUN = 3; const STUCK_PROCESSING_MINUTES = 30; async function run() { try { const stuckSince = new Date(Date.now() - STUCK_PROCESSING_MINUTES * 60 * 1000); // Demote stale "processing" rows back to "pending" so transcodeAsset()'s // own claim step (pending/failed -> processing) can pick them up again — // it never claims an in-progress "processing" row, by design (avoids // double-processing a job that's actually still running elsewhere). await mdl_Assets.update( { transcode_status: 'pending' }, { where: { transcode_status: 'processing', updatedAt: { [Op.lt]: stuckSince } } }, ); const candidates = await mdl_Assets.findAll({ where: { deletedAt: null, transcode_status: 'pending' }, limit: MAX_PER_RUN, }); if (!candidates.length) return; for (const asset of candidates) { await transcodeAsset(asset); } console.log(`[CRON][RETRY STUCK TRANSCODES] Processed ${candidates.length} asset(s).`); } catch (err) { console.error('[CRON][RETRY STUCK TRANSCODES] Failed:', err); } } module.exports = { name: 'retryStuckTranscodes', schedule: '*/10 * * * *', run, };