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:
parent
12f21803bc
commit
dc87cda0c3
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user