From dc87cda0c365c43bf35dd5f8a30037c21d9b566d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Thu, 28 May 2026 17:20:50 +0900 Subject: [PATCH] Share per-date game cache across dailyArchive users (R1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/repositories/gameRepository.ts | 26 ++++++++++++++++++++++++++ src/scheduled/dailyArchive.ts | 7 +++++-- src/services/judgmentService.ts | 19 +++++++++++++------ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/repositories/gameRepository.ts b/src/repositories/gameRepository.ts index cf5c4c3..8626bc0 100644 --- a/src/repositories/gameRepository.ts +++ b/src/repositories/gameRepository.ts @@ -35,3 +35,29 @@ export async function listByDate(date: DateString): Promise { .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; +} + +/** 새 `GameDayCache`를 생성한다. 캐시는 인스턴스 수명 동안만 유효하다. */ +export function createGameDayCache(): GameDayCache { + const cache = new Map>(); + return { + listByDate(date: DateString): Promise { + let p = cache.get(date); + if (!p) { + p = listByDate(date); + cache.set(date, p); + } + return p; + }, + }; +} diff --git a/src/scheduled/dailyArchive.ts b/src/scheduled/dailyArchive.ts index 08be6b3..3973036 100644 --- a/src/scheduled/dailyArchive.ts +++ b/src/scheduled/dailyArchive.ts @@ -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); } diff --git a/src/services/judgmentService.ts b/src/services/judgmentService.ts index 64835f0..66f0dbc 100644 --- a/src/services/judgmentService.ts +++ b/src/services/judgmentService.ts @@ -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 { + 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 { - 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,