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);
|
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> {
|
async function waitForCache<T>(key: string, ttlMs: number, timeoutMs = 25_000): Promise<T | null> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < timeoutMs) {
|
for (let attempt = 0; Date.now() - start < timeoutMs; attempt++) {
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, backoffDelayMs(attempt)));
|
||||||
const cached = await getCached<T>(key, ttlMs);
|
const cached = await getCached<T>(key, ttlMs);
|
||||||
if (cached !== null) return cached;
|
if (cached !== null) return cached;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
setCached,
|
setCached,
|
||||||
acquireLock,
|
acquireLock,
|
||||||
releaseLock,
|
releaseLock,
|
||||||
|
backoffDelayMs,
|
||||||
} from "./kboCacheRepository";
|
} from "./kboCacheRepository";
|
||||||
import { firestore } from "../firebase";
|
import { firestore } from "../firebase";
|
||||||
import { getGameList } from "../services/gameListService";
|
import { getGameList } from "../services/gameListService";
|
||||||
@ -394,10 +395,10 @@ async function fetchScheduleMonth(
|
|||||||
await releaseLock(lockKey);
|
await releaseLock(lockKey);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 다른 요청이 fetch 중. 짧게 polling.
|
// 다른 요청이 fetch 중. 지수 백오프로 polling(대기 중 read 폭증 완화).
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < 25_000) {
|
for (let attempt = 0; Date.now() - start < 25_000; attempt++) {
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, backoffDelayMs(attempt)));
|
||||||
cached = await readDayDocs(keys);
|
cached = await readDayDocs(keys);
|
||||||
missing = allDays.filter((_, i) => cached.get(keys[i]) === null);
|
missing = allDays.filter((_, i) => cached.get(keys[i]) === null);
|
||||||
if (missing.length === 0) break;
|
if (missing.length === 0) break;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
setCached,
|
setCached,
|
||||||
acquireLock,
|
acquireLock,
|
||||||
releaseLock,
|
releaseLock,
|
||||||
|
backoffDelayMs,
|
||||||
} from "../repositories/kboCacheRepository";
|
} from "../repositories/kboCacheRepository";
|
||||||
|
|
||||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
@ -115,8 +116,9 @@ async function getOrFetchDynamic<T>(
|
|||||||
|
|
||||||
const locked = await acquireLock(key);
|
const locked = await acquireLock(key);
|
||||||
if (!locked) {
|
if (!locked) {
|
||||||
for (let i = 0; i < 50; i++) {
|
const start = Date.now();
|
||||||
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)));
|
||||||
const c = await getCached<T>(key, Number.MAX_SAFE_INTEGER);
|
const c = await getCached<T>(key, Number.MAX_SAFE_INTEGER);
|
||||||
if (c !== null) return c;
|
if (c !== null) return c;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user