diff --git a/src/repositories/kboCacheRepository.ts b/src/repositories/kboCacheRepository.ts index c395059..d7eaa8f 100644 --- a/src/repositories/kboCacheRepository.ts +++ b/src/repositories/kboCacheRepository.ts @@ -110,6 +110,17 @@ async function releaseLock(key: string): Promise { await firestore.collection(LOCK_COLLECTION).doc(key).delete().catch(() => undefined); } +/** + * 캐시 스탬피드 폴링용 지수 백오프 지연(ms)을 계산한다. + * + * `attempt`(0부터)에 대해 `baseMs * 2^attempt`를 `maxMs`로 클램프한다. + * 고정 간격(500ms) 대신 점증 간격을 쓰면 락 보유자가 채울 때까지 대기하는 + * 동안의 캐시 read 횟수를 크게 줄인다(예: 25초 윈도우에서 ~50회 → ~9회). + */ +export function backoffDelayMs(attempt: number, baseMs = 300, maxMs = 5_000): number { + return Math.min(maxMs, baseMs * 2 ** attempt); +} + /** * 락이 걸려있는 동안 주기적으로 캐시를 폴링한다. * @@ -122,8 +133,8 @@ async function releaseLock(key: string): Promise { */ async function waitForCache(key: string, ttlMs: number, timeoutMs = 25_000): Promise { const start = Date.now(); - while (Date.now() - start < timeoutMs) { - await new Promise((r) => setTimeout(r, 500)); + for (let attempt = 0; Date.now() - start < timeoutMs; attempt++) { + await new Promise((r) => setTimeout(r, backoffDelayMs(attempt))); const cached = await getCached(key, ttlMs); if (cached !== null) return cached; } diff --git a/src/repositories/kboRepository.ts b/src/repositories/kboRepository.ts index 8e07177..534842f 100644 --- a/src/repositories/kboRepository.ts +++ b/src/repositories/kboRepository.ts @@ -24,6 +24,7 @@ import { setCached, acquireLock, releaseLock, + backoffDelayMs, } from "./kboCacheRepository"; import { firestore } from "../firebase"; import { getGameList } from "../services/gameListService"; @@ -394,10 +395,10 @@ async function fetchScheduleMonth( await releaseLock(lockKey); } } else { - // 다른 요청이 fetch 중. 짧게 polling. + // 다른 요청이 fetch 중. 지수 백오프로 polling(대기 중 read 폭증 완화). const start = Date.now(); - while (Date.now() - start < 25_000) { - await new Promise((r) => setTimeout(r, 500)); + for (let attempt = 0; Date.now() - start < 25_000; attempt++) { + await new Promise((r) => setTimeout(r, backoffDelayMs(attempt))); cached = await readDayDocs(keys); missing = allDays.filter((_, i) => cached.get(keys[i]) === null); if (missing.length === 0) break; diff --git a/src/services/gameDetailService.ts b/src/services/gameDetailService.ts index 1853d23..760ee28 100644 --- a/src/services/gameDetailService.ts +++ b/src/services/gameDetailService.ts @@ -9,6 +9,7 @@ import { setCached, acquireLock, releaseLock, + backoffDelayMs, } from "../repositories/kboCacheRepository"; const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; @@ -115,8 +116,9 @@ async function getOrFetchDynamic( const locked = await acquireLock(key); if (!locked) { - for (let i = 0; i < 50; i++) { - await new Promise((r) => setTimeout(r, 500)); + const start = Date.now(); + for (let attempt = 0; Date.now() - start < 25_000; attempt++) { + await new Promise((r) => setTimeout(r, backoffDelayMs(attempt))); const c = await getCached(key, Number.MAX_SAFE_INTEGER); if (c !== null) return c; }