mmday-firebase/src/scheduled/kboRefresh.ts
윤정민 fa7767d172 Stop bulk-invalidating game_detail cache in kboRefresh (W6)
The 02:00 cron prefix-deleted all game_detail__ docs, but those already
carry a response-derived dynamic TTL (7d for finished games, 30s live,
etc.). Bulk-wiping them forces a cold-miss stampede of external KBO
detail re-fetches right after the cron. Drop the deletion and let each
key expire on its own TTL. rank__ and schedule_day__ still get the
safety-net invalidation.
2026-05-28 17:32:40 +09:00

75 lines
2.7 KiB
TypeScript

import { onSchedule } from "firebase-functions/scheduler";
import { logger } from "firebase-functions";
import { firestore } from "../firebase";
import {
fetchRankFromKbo,
fetchScheduleFromKbo,
} from "../repositories/kboRepository";
import {
syncGamesForMonth,
forceSyncDay,
} from "../services/gameSyncService";
import { daysAgoKst } from "../types/dateString";
const CACHE_COLLECTION = "kboCache";
async function invalidateByPrefix(prefix: string): Promise<void> {
const snap = await firestore
.collection(CACHE_COLLECTION)
.where("__name__", ">=", prefix)
.where("__name__", "<", prefix + "\uf8ff")
.get();
const batch = firestore.batch();
snap.docs.forEach((d) => batch.delete(d.ref));
if (!snap.empty) await batch.commit();
}
export const kboDailyRefresh = onSchedule(
{ schedule: "0 2 * * *", timeZone: "Asia/Seoul", region: "asia-northeast3" },
async () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
logger.info(`KBO refresh start: ${year}-${month}`);
await invalidateByPrefix("rank__");
await invalidateByPrefix("schedule_day__");
// game_detail__는 응답 기반 동적 TTL(종료 경기 7d, 라이브 30s 등)을 이미 갖고 있어
// 02:00 일괄 무효화 대상에서 제외한다. 일괄 삭제 시 직후 상세 조회가 한꺼번에 미스나
// 외부 재조회 폭주를 유발하므로, TTL 자연 만료에 위임한다.
try {
await fetchRankFromKbo([year]);
await fetchScheduleFromKbo({ year, month });
const thisMonthCount = await syncGamesForMonth(year, month);
logger.info(`synced ${thisMonthCount} games for ${year}-${month}`);
// 월말 3일 이내면 다음 달도 함께 sync
const lastDay = new Date(year, month, 0).getDate();
if (now.getDate() >= lastDay - 2) {
const next = new Date(year, month, 1);
const ny = next.getFullYear();
const nm = next.getMonth() + 1;
const nextCount = await syncGamesForMonth(ny, nm);
logger.info(`synced ${nextCount} games for ${ny}-${nm}`);
}
// 어제 경기 status 확정: 월간 schedule refresh가 월 마지막 날을 놓치는 케이스를
// 라이브 데이터(`getGameList`)로 보완. 휴장일이면 0건 반환되어 무해.
try {
const yesterdayYmd = daysAgoKst(1).replace(/-/g, "");
const { updated } = await forceSyncDay(yesterdayYmd);
logger.info(`forceSyncDay: ${updated} games updated for ${yesterdayYmd}`);
} catch (err) {
logger.error("forceSyncDay failed", err);
}
logger.info("KBO refresh complete");
} catch (err) {
logger.error("KBO refresh failed", err);
}
}
);