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();
|
.get();
|
||||||
return snap.docs.map((d) => ({ gameId: d.id, ...(d.data() as Game) }));
|
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,
|
snapshotRankForUser,
|
||||||
} from "../services/rankSnapshotService";
|
} from "../services/rankSnapshotService";
|
||||||
import { todayKst } from "../types/dateString";
|
import { todayKst } from "../types/dateString";
|
||||||
import { getGame } from "../repositories/gameRepository";
|
import { getGame, createGameDayCache } from "../repositories/gameRepository";
|
||||||
import { deleteUserVoteGame } from "../repositories/voteRepository";
|
import { deleteUserVoteGame } from "../repositories/voteRepository";
|
||||||
import { processGameEndWithGame } from "../services/gameResultService";
|
import { processGameEndWithGame } from "../services/gameResultService";
|
||||||
import type { VoteHistoryDoc } from "../types/panit";
|
import type { VoteHistoryDoc } from "../types/panit";
|
||||||
@ -92,6 +92,9 @@ export async function runDailyArchive(
|
|||||||
|
|
||||||
let archived = 0;
|
let archived = 0;
|
||||||
const judgedUids: string[] = [];
|
const judgedUids: string[] = [];
|
||||||
|
// 같은 날짜(및 결석 판정용 직전 날짜들)의 games를 유저마다 재조회하지 않도록
|
||||||
|
// run 전체에서 1개의 날짜별 캐시를 공유한다. 날짜당 Firestore read 1회로 수렴.
|
||||||
|
const gameCache = createGameDayCache();
|
||||||
try {
|
try {
|
||||||
const snap = await rtdb.ref("/userVotes").get();
|
const snap = await rtdb.ref("/userVotes").get();
|
||||||
if (!snap.exists()) {
|
if (!snap.exists()) {
|
||||||
@ -136,7 +139,7 @@ export async function runDailyArchive(
|
|||||||
logger.error(`snapshotRank failed uid=${uid} date=${date}`, err);
|
logger.error(`snapshotRank failed uid=${uid} date=${date}`, err);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await judgeDay(uid, date, { data });
|
await judgeDay(uid, date, { data }, gameCache);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`judgeDay failed uid=${uid} date=${date}`, err);
|
logger.error(`judgeDay failed uid=${uid} date=${date}`, err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { logger } from "firebase-functions";
|
import { logger } from "firebase-functions";
|
||||||
import { listByDate } from "../repositories/gameRepository";
|
import {
|
||||||
|
createGameDayCache,
|
||||||
|
type GameDayCache,
|
||||||
|
} from "../repositories/gameRepository";
|
||||||
import { getRange, setDay } from "../repositories/voteHistoryRepository";
|
import { getRange, setDay } from "../repositories/voteHistoryRepository";
|
||||||
import {
|
import {
|
||||||
applyDailyJudgmentTx,
|
applyDailyJudgmentTx,
|
||||||
@ -26,12 +29,14 @@ import { addDaysKst, tuesdayOf, type DateString } from "../types/dateString";
|
|||||||
*/
|
*/
|
||||||
export async function hasMissedGameDayBetween(
|
export async function hasMissedGameDayBetween(
|
||||||
lastJudged: DateString,
|
lastJudged: DateString,
|
||||||
upTo: DateString
|
upTo: DateString,
|
||||||
|
gameCache?: GameDayCache
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const fetch = gameCache ?? createGameDayCache();
|
||||||
const MAX_LOOKBACK = 14;
|
const MAX_LOOKBACK = 14;
|
||||||
let cursor = addDaysKst(upTo, -1);
|
let cursor = addDaysKst(upTo, -1);
|
||||||
for (let i = 0; i < MAX_LOOKBACK && cursor > lastJudged; i++) {
|
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;
|
const completed = games.filter((g) => g.status === "completed").length;
|
||||||
if (thresholdsFor(completed) !== "skip") return true;
|
if (thresholdsFor(completed) !== "skip") return true;
|
||||||
cursor = addDaysKst(cursor, -1);
|
cursor = addDaysKst(cursor, -1);
|
||||||
@ -53,9 +58,11 @@ export async function hasMissedGameDayBetween(
|
|||||||
export async function judgeDay(
|
export async function judgeDay(
|
||||||
uid: string,
|
uid: string,
|
||||||
date: DateString,
|
date: DateString,
|
||||||
voteDoc: VoteHistoryDoc
|
voteDoc: VoteHistoryDoc,
|
||||||
|
gameCache?: GameDayCache
|
||||||
): Promise<void> {
|
): 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 completedCount = games.filter((g) => g.status === "completed").length;
|
||||||
const threshold = thresholdsFor(completedCount);
|
const threshold = thresholdsFor(completedCount);
|
||||||
|
|
||||||
@ -76,7 +83,7 @@ export async function judgeDay(
|
|||||||
const streakBrokenIn =
|
const streakBrokenIn =
|
||||||
lastJudgedPre != null &&
|
lastJudgedPre != null &&
|
||||||
lastJudgedPre < addDaysKst(date, -1) &&
|
lastJudgedPre < addDaysKst(date, -1) &&
|
||||||
(await hasMissedGameDayBetween(lastJudgedPre, date));
|
(await hasMissedGameDayBetween(lastJudgedPre, date, fetch));
|
||||||
|
|
||||||
const tx = await applyDailyJudgmentTx(uid, date, {
|
const tx = await applyDailyJudgmentTx(uid, date, {
|
||||||
judgment,
|
judgment,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user