Share per-date game cache across dailyArchive users (R1)

judgeDay and hasMissedGameDayBetween re-ran the same listByDate query for
every user in an archive run (U× amplification for the archived date plus
its look-back window). Introduce a memoizing GameDayCache and thread one
instance through the whole runDailyArchive pass so each distinct date is
read from Firestore once. Off-day detection is naturally memoized via the
cached query results. Standalone callers (statsService) keep their prior
behavior via a per-call default cache. No change to judgment results.
This commit is contained in:
윤정민 2026-05-28 17:20:50 +09:00
parent 12f21803bc
commit dc87cda0c3
3 changed files with 44 additions and 8 deletions

View File

@ -35,3 +35,29 @@ export async function listByDate(date: DateString): Promise<GameWithId[]> {
.get();
return snap.docs.map((d) => ({ gameId: d.id, ...(d.data() as Game) }));
}
/**
* `listByDate` .
*
* `dailyArchive` run에서 `games`
* , Firestore read를 1 .
* Promise를 .
*/
export interface GameDayCache {
listByDate(date: DateString): Promise<GameWithId[]>;
}
/** 새 `GameDayCache`를 생성한다. 캐시는 인스턴스 수명 동안만 유효하다. */
export function createGameDayCache(): GameDayCache {
const cache = new Map<DateString, Promise<GameWithId[]>>();
return {
listByDate(date: DateString): Promise<GameWithId[]> {
let p = cache.get(date);
if (!p) {
p = listByDate(date);
cache.set(date, p);
}
return p;
},
};
}

View File

@ -9,7 +9,7 @@ import {
snapshotRankForUser,
} from "../services/rankSnapshotService";
import { todayKst } from "../types/dateString";
import { getGame } from "../repositories/gameRepository";
import { getGame, createGameDayCache } from "../repositories/gameRepository";
import { deleteUserVoteGame } from "../repositories/voteRepository";
import { processGameEndWithGame } from "../services/gameResultService";
import type { VoteHistoryDoc } from "../types/panit";
@ -92,6 +92,9 @@ export async function runDailyArchive(
let archived = 0;
const judgedUids: string[] = [];
// 같은 날짜(및 결석 판정용 직전 날짜들)의 games를 유저마다 재조회하지 않도록
// run 전체에서 1개의 날짜별 캐시를 공유한다. 날짜당 Firestore read 1회로 수렴.
const gameCache = createGameDayCache();
try {
const snap = await rtdb.ref("/userVotes").get();
if (!snap.exists()) {
@ -136,7 +139,7 @@ export async function runDailyArchive(
logger.error(`snapshotRank failed uid=${uid} date=${date}`, err);
}
try {
await judgeDay(uid, date, { data });
await judgeDay(uid, date, { data }, gameCache);
} catch (err) {
logger.error(`judgeDay failed uid=${uid} date=${date}`, err);
}

View File

@ -1,5 +1,8 @@
import { logger } from "firebase-functions";
import { listByDate } from "../repositories/gameRepository";
import {
createGameDayCache,
type GameDayCache,
} from "../repositories/gameRepository";
import { getRange, setDay } from "../repositories/voteHistoryRepository";
import {
applyDailyJudgmentTx,
@ -26,12 +29,14 @@ import { addDaysKst, tuesdayOf, type DateString } from "../types/dateString";
*/
export async function hasMissedGameDayBetween(
lastJudged: DateString,
upTo: DateString
upTo: DateString,
gameCache?: GameDayCache
): Promise<boolean> {
const fetch = gameCache ?? createGameDayCache();
const MAX_LOOKBACK = 14;
let cursor = addDaysKst(upTo, -1);
for (let i = 0; i < MAX_LOOKBACK && cursor > lastJudged; i++) {
const games = await listByDate(cursor);
const games = await fetch.listByDate(cursor);
const completed = games.filter((g) => g.status === "completed").length;
if (thresholdsFor(completed) !== "skip") return true;
cursor = addDaysKst(cursor, -1);
@ -53,9 +58,11 @@ export async function hasMissedGameDayBetween(
export async function judgeDay(
uid: string,
date: DateString,
voteDoc: VoteHistoryDoc
voteDoc: VoteHistoryDoc,
gameCache?: GameDayCache
): Promise<void> {
const games = await listByDate(date);
const fetch = gameCache ?? createGameDayCache();
const games = await fetch.listByDate(date);
const completedCount = games.filter((g) => g.status === "completed").length;
const threshold = thresholdsFor(completedCount);
@ -76,7 +83,7 @@ export async function judgeDay(
const streakBrokenIn =
lastJudgedPre != null &&
lastJudgedPre < addDaysKst(date, -1) &&
(await hasMissedGameDayBetween(lastJudgedPre, date));
(await hasMissedGameDayBetween(lastJudgedPre, date, fetch));
const tx = await applyDailyJudgmentTx(uid, date, {
judgment,