Use exponential backoff for cache-stampede polling (R4)
waitForCache, the schedule month-poll loop, and getOrFetchDynamic all polled Firestore every fixed 500ms (up to ~50 reads) while waiting for a lock holder to populate the cache. Replace the fixed interval with a shared backoffDelayMs helper (300ms base, doubling, 5s cap) bounded by the same ~25s timeout, cutting per-waiter read amplification by roughly 5x. Timeout/fallback semantics are unchanged.
This commit is contained in:
parent
ee3badb112
commit
6fc00b2952
@ -110,6 +110,17 @@ async function releaseLock(key: string): Promise<void> {
|
||||
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<void> {
|
||||
*/
|
||||
async function waitForCache<T>(key: string, ttlMs: number, timeoutMs = 25_000): Promise<T | null> {
|
||||
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<T>(key, ttlMs);
|
||||
if (cached !== null) return cached;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<T>(
|
||||
|
||||
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<T>(key, Number.MAX_SAFE_INTEGER);
|
||||
if (c !== null) return c;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user