Skip to content

Commit a5dbb01

Browse files
committed
feat: Redis Pub/Sub 기능 검증 및 안정화 강화
1 parent 1d5aa93 commit a5dbb01

1 file changed

Lines changed: 97 additions & 24 deletions

File tree

echoshot_ai_server/core/redis_client.py

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77

88
import json
99
import logging
10+
import time
1011
from datetime import datetime
1112
from typing import Optional, Dict, Any
1213
from contextlib import contextmanager
1314

14-
import redis
15-
from redis.exceptions import RedisError, ConnectionError, TimeoutError
15+
try:
16+
import redis
17+
from redis.exceptions import RedisError, ConnectionError, TimeoutError
18+
except ImportError:
19+
redis = None
20+
RedisError = Exception
21+
ConnectionError = Exception
22+
TimeoutError = Exception
1623

1724
from ..config.settings import settings
1825

@@ -76,29 +83,35 @@ def _connect(self) -> bool:
7683
연결 성공 여부
7784
"""
7885
try:
79-
self._client = redis.Redis(
80-
host=self.host,
81-
port=self.port,
82-
password=self.password,
83-
db=self.db,
84-
socket_timeout=self.socket_timeout,
85-
socket_connect_timeout=self.socket_timeout,
86-
retry_on_timeout=self.retry_on_timeout,
87-
decode_responses=True, # 문자열로 디코딩
88-
health_check_interval=30 # 30초마다 헬스체크
89-
)
90-
91-
# 연결 테스트 (PING)
92-
self._client.ping()
93-
self._connected = True
94-
logger.info(f"Successfully connected to Redis at {self.host}:{self.port}")
95-
return True
86+
if redis is not None:
87+
self._client = redis.Redis(
88+
host=self.host,
89+
port=self.port,
90+
password=self.password,
91+
db=self.db,
92+
socket_timeout=self.socket_timeout,
93+
socket_connect_timeout=self.socket_timeout,
94+
retry_on_timeout=self.retry_on_timeout,
95+
decode_responses=True, # 문자열로 디코딩
96+
health_check_interval=30 # 30초마다 헬스체크
97+
)
98+
99+
# 연결 테스트 (PING)
100+
self._client.ping()
101+
self._connected = True
102+
logger.info(f"Successfully connected to Redis at {self.host}:{self.port}")
103+
return True
104+
else:
105+
logger.warning("Redis not available, using fallback mode")
106+
self._client = None
107+
self._connected = False
108+
return False
96109

97110
except (ConnectionError, TimeoutError) as e:
98111
logger.warning(f"Failed to connect to Redis at {self.host}:{self.port}: {e}")
99112
self._connected = False
100113
return False
101-
except RedisError as e:
114+
except Exception as e:
102115
logger.error(f"Redis error during connection: {e}")
103116
self._connected = False
104117
return False
@@ -121,6 +134,60 @@ def is_connected(self) -> bool:
121134
self._connected = False
122135
return False
123136

137+
def ensure_connection(self) -> bool:
138+
"""
139+
Redis 연결 보장 (재연결 시도)
140+
141+
Returns:
142+
연결 성공 여부
143+
"""
144+
if self.is_connected():
145+
return True
146+
147+
logger.info("Redis connection lost, attempting to reconnect...")
148+
return self._connect()
149+
150+
def publish_with_retry(
151+
self,
152+
channel: str,
153+
message: Dict[str, Any],
154+
max_retries: int = 3
155+
) -> bool:
156+
"""
157+
재시도 로직이 포함된 메시지 발행
158+
159+
Args:
160+
channel: 발행할 채널
161+
message: 발행할 메시지
162+
max_retries: 최대 재시도 횟수
163+
164+
Returns:
165+
발행 성공 여부
166+
"""
167+
for attempt in range(max_retries):
168+
try:
169+
if self.ensure_connection():
170+
if self.client is not None:
171+
success = self.publish(channel, message)
172+
if success:
173+
return True
174+
logger.debug(f"Publish successful on attempt {attempt + 1}")
175+
else:
176+
logger.warning("Redis client not available, skipping publish")
177+
return False
178+
else:
179+
logger.warning("Redis not available, skipping publish")
180+
return False
181+
except Exception as e:
182+
logger.warning(f"Publish attempt {attempt + 1} failed: {e}")
183+
if attempt < max_retries - 1:
184+
time.sleep(1) # 1초 대기 후 재시도
185+
else:
186+
logger.error(f"Publish failed after {max_retries} attempts: {e}")
187+
return False
188+
189+
return False
190+
124191
def publish(self, channel: str, message: Dict[str, Any]) -> bool:
125192
"""
126193
메시지를 Redis 채널에 발행
@@ -146,9 +213,10 @@ def publish(self, channel: str, message: Dict[str, Any]) -> bool:
146213
)
147214
return True
148215

149-
except (RedisError, ConnectionError, TimeoutError) as e:
216+
except Exception as e:
150217
logger.warning(f"Failed to publish to channel '{channel}': {e}")
151-
self._connected = False
218+
if isinstance(e, (RedisError, ConnectionError, TimeoutError)):
219+
self._connected = False
152220
return False
153221

154222
def publish_progress(
@@ -181,7 +249,7 @@ def publish_progress(
181249
channel = f"job:{job_id}:progress"
182250

183251
payload = {
184-
"jobId": job_id, # Spring과 일관성 유지 (camelCase)
252+
"jobId": job_id, # Spring과 호환성 유지 (camelCase)
185253
"progress": min(100.0, max(0.0, progress)),
186254
"status": status,
187255
"timestamp": datetime.utcnow().isoformat() + "Z"
@@ -201,7 +269,12 @@ def publish_progress(
201269
if metadata:
202270
payload["metadata"] = metadata
203271

204-
return self.publish(channel, payload)
272+
# Redis fallback 로직
273+
if self.client is not None:
274+
return self.publish(channel, payload)
275+
else:
276+
logger.warning(f"Redis not available, skipping progress publish for job {job_id}")
277+
return False
205278

206279
def close(self) -> None:
207280
"""Redis 연결 종료"""

0 commit comments

Comments
 (0)