From ce991c535bc4af54fb6414849d3f531c618da1d7 Mon Sep 17 00:00:00 2001 From: minij02 Date: Mon, 27 Jul 2026 00:24:19 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Redis=20=EC=9E=A5=EC=95=A0=20=EC=8B=9C?= =?UTF-8?q?=20=EB=B0=A9=EB=AC=B8=EC=9E=90=20=ED=86=B5=EA=B3=84=20API=20han?= =?UTF-8?q?g=20=EB=B0=A9=EC=A7=80=20=E2=80=94=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=95=84=EC=9B=83/=EC=9E=AC=EC=97=B0=EA=B2=B0=20=EC=83=81?= =?UTF-8?q?=ED=95=9C=20(#524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - redis client 에 connectTimeout(3s) + 상한 있는 reconnectStrategy 추가 - admin-visitor-stats.repository 의 pfCount 3개 호출에 2s 타임아웃 + 폴백 0 - Redis 다운 시에도 200/카운트 0 으로 응답, ELB 502 방지 --- src/config/redis.ts | 4 ++++ .../admin-visitor-stats.repository.ts | 22 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) 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] })); }, };