import cron from "node-cron"; import pool from "../lib/db.js"; import { syncNewVideos } from "./youtube-bot.js"; import { syncNewTweets } from "./x-bot.js"; import { syncAllSchedules } from "./meilisearch-bot.js"; // 봇별 스케줄러 인스턴스 저장 const schedulers = new Map(); /** * 봇 타입에 따라 적절한 동기화 함수 호출 */ async function syncBot(botId) { const [bots] = await pool.query("SELECT type FROM bots WHERE id = ?", [ botId, ]); if (bots.length === 0) throw new Error("봇을 찾을 수 없습니다."); const botType = bots[0].type; if (botType === "youtube") { return await syncNewVideos(botId); } else if (botType === "x") { return await syncNewTweets(botId); } else if (botType === "meilisearch") { return await syncAllSchedules(botId); } else { throw new Error(`지원하지 않는 봇 타입: ${botType}`); } } /** * 봇이 메모리에서 실행 중인지 확인 */ export function isBotRunning(botId) { const id = parseInt(botId); return schedulers.has(id); } /** * 개별 봇 스케줄 등록 */ export function registerBot(botId, intervalMinutes = 2, cronExpression = null) { const id = parseInt(botId); // 기존 스케줄이 있으면 제거 unregisterBot(id); // cron 표현식: 지정된 표현식 사용, 없으면 기본값 생성 const expression = cronExpression || `1-59/${intervalMinutes} * * * *`; const task = cron.schedule(expression, async () => { console.log(`[Bot ${id}] 동기화 시작...`); try { const result = await syncBot(id); console.log(`[Bot ${id}] 동기화 완료: ${result.addedCount}개 추가`); } catch (error) { console.error(`[Bot ${id}] 동기화 오류:`, error.message); } }); schedulers.set(id, task); console.log(`[Bot ${id}] 스케줄 등록됨 (cron: ${expression})`); } /** * 개별 봇 스케줄 해제 */ export function unregisterBot(botId) { const id = parseInt(botId); if (schedulers.has(id)) { schedulers.get(id).stop(); schedulers.delete(id); console.log(`[Bot ${id}] 스케줄 해제됨`); } } /** * 10초 간격으로 메모리 상태와 DB status 동기화 */ async function syncBotStatuses() { try { const [bots] = await pool.query("SELECT id, status FROM bots"); for (const bot of bots) { const botId = parseInt(bot.id); const isRunningInMemory = schedulers.has(botId); const isRunningInDB = bot.status === "running"; // 메모리에 없는데 DB가 running이면 → 서버 크래시 등으로 불일치 // 이 경우 DB를 stopped로 변경하는 대신, 메모리에 봇을 다시 등록 if (!isRunningInMemory && isRunningInDB) { console.log(`[Scheduler] Bot ${botId} 메모리에 없음, 재등록 시도...`); try { const [botInfo] = await pool.query( "SELECT check_interval, cron_expression FROM bots WHERE id = ?", [botId] ); if (botInfo.length > 0) { const { check_interval, cron_expression } = botInfo[0]; // 직접 registerBot 함수 호출 (import 순환 방지를 위해 내부 로직 사용) const expression = cron_expression || `1-59/${check_interval} * * * *`; const task = cron.schedule(expression, async () => { console.log(`[Bot ${botId}] 동기화 시작...`); try { const result = await syncBot(botId); console.log( `[Bot ${botId}] 동기화 완료: ${result.addedCount}개 추가` ); } catch (error) { console.error(`[Bot ${botId}] 동기화 오류:`, error.message); } }); schedulers.set(botId, task); console.log( `[Scheduler] Bot ${botId} 재등록 완료 (cron: ${expression})` ); } } catch (error) { console.error(`[Scheduler] Bot ${botId} 재등록 오류:`, error.message); // 재등록 실패 시에만 stopped로 변경 await pool.query("UPDATE bots SET status = 'stopped' WHERE id = ?", [ botId, ]); console.log(`[Scheduler] Bot ${botId} 상태 동기화: stopped`); } } } } catch (error) { console.error("[Scheduler] 상태 동기화 오류:", error.message); } } /** * 서버 시작 시 실행 중인 봇들 스케줄 등록 */ export async function initScheduler() { try { const [bots] = await pool.query( "SELECT id, check_interval, cron_expression FROM bots WHERE status = 'running'" ); for (const bot of bots) { registerBot(bot.id, bot.check_interval, bot.cron_expression); } console.log(`[Scheduler] ${bots.length}개 봇 스케줄 등록됨`); // 10초 간격으로 상태 동기화 (DB status와 메모리 상태 일치 유지) setInterval(syncBotStatuses, 10000); console.log(`[Scheduler] 10초 간격 상태 동기화 시작`); } catch (error) { console.error("[Scheduler] 초기화 오류:", error); } } /** * 봇 시작 */ export async function startBot(botId) { const [bots] = await pool.query("SELECT * FROM bots WHERE id = ?", [botId]); if (bots.length === 0) { throw new Error("봇을 찾을 수 없습니다."); } const bot = bots[0]; // 스케줄 등록 (cron_expression 우선 사용) registerBot(botId, bot.check_interval, bot.cron_expression); // 상태 업데이트 await pool.query( "UPDATE bots SET status = 'running', error_message = NULL WHERE id = ?", [botId] ); // 즉시 1회 실행 try { await syncBot(botId); } catch (error) { console.error(`[Bot ${botId}] 초기 동기화 오류:`, error.message); } } /** * 봇 정지 */ export async function stopBot(botId) { unregisterBot(botId); await pool.query("UPDATE bots SET status = 'stopped' WHERE id = ?", [botId]); } export default { initScheduler, registerBot, unregisterBot, startBot, stopBot, isBotRunning, };