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.
75 lines
2.7 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
);
|