77
88import json
99import logging
10+ import time
1011from datetime import datetime
1112from typing import Optional , Dict , Any
1213from 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
1724from ..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