hyerimmy 910bd902b1
Some checks failed
HealthSync Intelligence CI / build-and-push (push) Has been cancelled
feat : initial commit
2025-06-20 05:28:30 +00:00

226 lines
8.5 KiB
Python

# app/utils/redis_client.py
"""
HealthSync AI Redis 캐시 클라이언트 (Azure Cache for Redis 지원)
"""
import redis.asyncio as redis
import json
import logging
from typing import Any, Optional, List
from app.config.settings import settings
logger = logging.getLogger(__name__)
class RedisClient:
"""Redis 캐시 연동 클라이언트 (Azure Cache for Redis 지원)"""
def __init__(self):
self.redis_host = settings.redis_host
self.redis_port = settings.redis_port
self.redis_password = settings.redis_password
self.redis_db = settings.redis_db
self.default_ttl = settings.redis_cache_ttl
self.client = None
self._connected = False
async def connect(self):
"""Azure Cache for Redis 연결"""
if self._connected:
return
try:
# Azure Cache for Redis 연결 설정
if self.redis_password and self.redis_password != "":
# Azure Cache for Redis (SSL + 인증)
self.client = redis.Redis(
host=self.redis_host,
port=self.redis_port,
password=self.redis_password,
db=self.redis_db,
ssl=True, # Azure Cache는 SSL 필수
ssl_cert_reqs=None, # SSL 인증서 검증 비활성화
decode_responses=True,
socket_timeout=10,
socket_connect_timeout=10,
retry_on_timeout=True,
health_check_interval=30
)
logger.info(f"🔐 Azure Cache for Redis 연결 시도 - {self.redis_host}:{self.redis_port}")
else:
# 로컬 Redis (비SSL)
self.client = redis.Redis(
host=self.redis_host,
port=self.redis_port,
db=self.redis_db,
decode_responses=True,
socket_timeout=5,
socket_connect_timeout=5
)
logger.info(f"🔓 로컬 Redis 연결 시도 - {self.redis_host}:{self.redis_port}")
# 연결 테스트
await self.client.ping()
self._connected = True
logger.info(f"✅ Redis 클라이언트 연결 완료 - {self.redis_host}:{self.redis_port}")
except redis.AuthenticationError as e:
logger.error(f"❌ Redis 인증 실패 - 패스워드 확인 필요: {str(e)}")
raise Exception(f"Redis 인증 실패: {str(e)}")
except redis.ConnectionError as e:
logger.error(f"❌ Redis 연결 실패 - 호스트/포트 확인 필요: {str(e)}")
raise Exception(f"Redis 연결 실패: {str(e)}")
except redis.TimeoutError as e:
logger.error(f"❌ Redis 연결 타임아웃: {str(e)}")
raise Exception(f"Redis 연결 타임아웃: {str(e)}")
except Exception as e:
logger.error(f"❌ Redis 연결 실패: {str(e)}")
raise Exception(f"Redis 연결 실패: {str(e)}")
async def disconnect(self):
"""Redis 연결 해제"""
if self.client and self._connected:
try:
await self.client.aclose() # redis.asyncio의 올바른 종료 메소드
self._connected = False
logger.info("✅ Redis 연결 해제 완료")
except Exception as e:
logger.error(f"❌ Redis 연결 해제 실패: {str(e)}")
async def get(self, key: str) -> Optional[Any]:
"""캐시에서 값 조회"""
try:
if not self._connected:
await self.connect()
value = await self.client.get(key)
if value:
result = json.loads(value)
logger.info(f"✅ 캐시 조회 성공 - key: {key}")
return result
else:
logger.info(f"❌ 캐시 미스 - key: {key}")
return None
except json.JSONDecodeError as e:
logger.error(f"❌ JSON 파싱 실패 - key: {key}, error: {str(e)}")
return None
except redis.ConnectionError as e:
logger.error(f"❌ Redis 연결 오류 - key: {key}, error: {str(e)}")
return None
except Exception as e:
logger.error(f"❌ 캐시 조회 실패 - key: {key}, error: {str(e)}")
return None
async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
"""캐시에 값 저장"""
try:
if not self._connected:
await self.connect()
ttl = ttl or self.default_ttl
json_value = json.dumps(value, ensure_ascii=False, default=str)
await self.client.setex(key, ttl, json_value)
logger.info(f"✅ 캐시 저장 성공 - key: {key}, ttl: {ttl}s")
return True
except redis.ConnectionError as e:
logger.error(f"❌ Redis 연결 오류 - key: {key}, error: {str(e)}")
return False
except Exception as e:
logger.error(f"❌ 캐시 저장 실패 - key: {key}, error: {str(e)}")
return False
async def delete(self, key: str) -> bool:
"""캐시에서 값 삭제"""
try:
if not self._connected:
await self.connect()
result = await self.client.delete(key)
if result:
logger.info(f"✅ 캐시 삭제 성공 - key: {key}")
return True
else:
logger.info(f"❌ 캐시 삭제 실패 (키 없음) - key: {key}")
return False
except redis.ConnectionError as e:
logger.error(f"❌ Redis 연결 오류 - key: {key}, error: {str(e)}")
return False
except Exception as e:
logger.error(f"❌ 캐시 삭제 실패 - key: {key}, error: {str(e)}")
return False
async def get_or_set(self, key: str, fetch_func, ttl: Optional[int] = None) -> Any:
"""캐시 조회 후 없으면 fetch_func 실행하여 저장 (Cache Aside 패턴)"""
try:
# 1. 캐시에서 조회
cached_value = await self.get(key)
if cached_value is not None:
return cached_value
# 2. 캐시 미스 시 데이터 fetch
fresh_value = await fetch_func()
if fresh_value is not None:
# 3. 캐시에 저장
await self.set(key, fresh_value, ttl)
return fresh_value
return None
except Exception as e:
logger.error(f"❌ Cache Aside 패턴 실행 실패 - key: {key}, error: {str(e)}")
# 캐시 실패 시에도 fresh_value 반환 시도
try:
return await fetch_func()
except:
return None
def generate_similar_users_key(self, user_id: int) -> str:
"""유사 사용자 캐시 키 생성"""
return f"similar_users:{user_id}"
def generate_mission_news_key(self, user_id: int) -> str:
"""미션 소식 캐시 키 생성 (짧은 TTL용)"""
return f"mission_news:{user_id}"
async def test_connection(self) -> dict:
"""Redis 연결 테스트"""
try:
if not self._connected:
await self.connect()
# 기본 명령 테스트
test_key = "healthsync:connection_test"
test_value = "test_connection_success"
await self.client.set(test_key, test_value, ex=10) # 10초 TTL
retrieved_value = await self.client.get(test_key)
await self.client.delete(test_key)
info = await self.client.info()
return {
"status": "connected",
"host": self.redis_host,
"port": self.redis_port,
"ssl_enabled": bool(self.redis_password),
"test_result": retrieved_value == test_value,
"redis_version": info.get("redis_version", "unknown"),
"connected_clients": info.get("connected_clients", 0),
"used_memory_human": info.get("used_memory_human", "unknown")
}
except Exception as e:
return {
"status": "failed",
"error": str(e),
"error_type": type(e).__name__,
"host": self.redis_host,
"port": self.redis_port
}
# 전역 클라이언트 인스턴스
redis_client = RedisClient()