diff --git a/src/config/redis.ts b/src/config/redis.ts index 6ce447d..42f05a7 100644 --- a/src/config/redis.ts +++ b/src/config/redis.ts @@ -2,6 +2,10 @@ import { createClient } from 'redis'; const redisClient = createClient({ url: process.env.REDIS_URL, + socket: { + connectTimeout: 3000, + reconnectStrategy: (retries) => Math.min(retries * 200, 5000), + }, }); redisClient.on('connect', () => { diff --git a/src/stats/repositories/admin-visitor-stats.repository.ts b/src/stats/repositories/admin-visitor-stats.repository.ts index b64239b..9124283 100644 --- a/src/stats/repositories/admin-visitor-stats.repository.ts +++ b/src/stats/repositories/admin-visitor-stats.repository.ts @@ -1,15 +1,27 @@ import redisClient from '../../config/redis'; import { buildHllKey } from '../../utils/visitor-tracking'; +const REDIS_TIMEOUT_MS = 2000; + +// ponytail: Redis 장애 시 응답이 hang 되지 않도록 짧은 타임아웃 + 폴백. 근본 복구는 인프라 몫. +const withTimeout = (op: Promise, fallback: T): Promise => + Promise.race([ + op, + new Promise((resolve) => setTimeout(() => resolve(fallback), REDIS_TIMEOUT_MS)), + ]); + export const AdminVisitorStatsRepository = { countUniqueOnDate: async (kstDate: string): Promise => { - return Number(await redisClient.pfCount(buildHllKey(kstDate))); + return withTimeout( + redisClient.pfCount(buildHllKey(kstDate)).then(Number), + 0, + ); }, countUniqueOverRange: async (kstDates: string[]): Promise => { if (kstDates.length === 0) return 0; const keys = kstDates.map(buildHllKey); - return Number(await redisClient.pfCount(keys)); + return withTimeout(redisClient.pfCount(keys).then(Number), 0); }, countUniquePerDay: async ( @@ -17,8 +29,10 @@ export const AdminVisitorStatsRepository = { ): Promise<{ date: string; count: number }[]> => { if (kstDates.length === 0) return []; const counts = await Promise.all( - kstDates.map((d) => redisClient.pfCount(buildHllKey(d))), + kstDates.map((d) => + withTimeout(redisClient.pfCount(buildHllKey(d)).then(Number), 0), + ), ); - return kstDates.map((date, i) => ({ date, count: Number(counts[i]) })); + return kstDates.map((date, i) => ({ date, count: counts[i] })); }, };