해당 서비스 도커화 성공, 룰 추가, 로그인 오류 수정, 소문자 룰 어느정도 해결
This commit is contained in:
204
fastapi-bridge/cache.py
Normal file
204
fastapi-bridge/cache.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
FastAPI 브릿지 캐싱 시스템
|
||||
Phase 3: Redis 캐싱 (메모리 캐시로 폴백)
|
||||
"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from functools import wraps
|
||||
|
||||
import redis.asyncio as redis
|
||||
|
||||
from config import settings
|
||||
|
||||
class CacheManager:
|
||||
"""캐시 관리자 - Redis 우선, 메모리 캐시 폴백"""
|
||||
|
||||
def __init__(self):
|
||||
self.redis_client: Optional[redis.Redis] = None
|
||||
self.memory_cache: Dict[str, Dict[str, Any]] = {}
|
||||
self.is_redis_available = False
|
||||
|
||||
async def connect(self):
|
||||
"""Redis 연결 시도, 실패시 메모리 캐시 사용"""
|
||||
try:
|
||||
self.redis_client = redis.from_url(
|
||||
settings.REDIS_URL,
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=5,
|
||||
socket_timeout=5
|
||||
)
|
||||
|
||||
# Redis 연결 테스트
|
||||
await self.redis_client.ping()
|
||||
self.is_redis_available = True
|
||||
print("✅ Redis 연결 성공")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Redis 연결 실패, 메모리 캐시 사용: {e}")
|
||||
self.is_redis_available = False
|
||||
self.redis_client = None
|
||||
|
||||
async def disconnect(self):
|
||||
"""연결 종료"""
|
||||
if self.redis_client:
|
||||
await self.redis_client.close()
|
||||
|
||||
def _generate_key(self, prefix: str, *args, **kwargs) -> str:
|
||||
"""캐시 키 생성"""
|
||||
# 파라미터들을 문자열로 변환
|
||||
key_data = f"{prefix}:{':'.join(map(str, args))}"
|
||||
if kwargs:
|
||||
key_data += f":{json.dumps(kwargs, sort_keys=True)}"
|
||||
|
||||
# 긴 키는 해시로 단축
|
||||
if len(key_data) > 100:
|
||||
key_hash = hashlib.md5(key_data.encode()).hexdigest()
|
||||
return f"{prefix}:{key_hash}"
|
||||
|
||||
return key_data
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
"""캐시에서 값 조회"""
|
||||
if self.is_redis_available and self.redis_client:
|
||||
try:
|
||||
value = await self.redis_client.get(key)
|
||||
if value:
|
||||
return json.loads(value)
|
||||
except Exception as e:
|
||||
print(f"Redis GET 오류: {e}")
|
||||
|
||||
# 메모리 캐시 폴백
|
||||
if key in self.memory_cache:
|
||||
cache_entry = self.memory_cache[key]
|
||||
if cache_entry['expires_at'] > time.time():
|
||||
return cache_entry['data']
|
||||
else:
|
||||
# 만료된 캐시 삭제
|
||||
del self.memory_cache[key]
|
||||
|
||||
return None
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: int = 300) -> bool:
|
||||
"""캐시에 값 저장"""
|
||||
json_value = json.dumps(value, ensure_ascii=False)
|
||||
|
||||
if self.is_redis_available and self.redis_client:
|
||||
try:
|
||||
await self.redis_client.setex(key, ttl, json_value)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Redis SET 오류: {e}")
|
||||
|
||||
# 메모리 캐시 폴백
|
||||
self.memory_cache[key] = {
|
||||
'data': value,
|
||||
'expires_at': time.time() + ttl
|
||||
}
|
||||
|
||||
# 메모리 캐시 크기 제한 (최대 1000개)
|
||||
if len(self.memory_cache) > 1000:
|
||||
# 가장 오래된 10개 항목 제거
|
||||
sorted_keys = sorted(
|
||||
self.memory_cache.keys(),
|
||||
key=lambda k: self.memory_cache[k]['expires_at']
|
||||
)
|
||||
for key_to_remove in sorted_keys[:10]:
|
||||
del self.memory_cache[key_to_remove]
|
||||
|
||||
return True
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""캐시에서 값 삭제"""
|
||||
deleted = False
|
||||
|
||||
if self.is_redis_available and self.redis_client:
|
||||
try:
|
||||
result = await self.redis_client.delete(key)
|
||||
deleted = result > 0
|
||||
except Exception as e:
|
||||
print(f"Redis DELETE 오류: {e}")
|
||||
|
||||
# 메모리 캐시에서도 삭제
|
||||
if key in self.memory_cache:
|
||||
del self.memory_cache[key]
|
||||
deleted = True
|
||||
|
||||
return deleted
|
||||
|
||||
async def clear_pattern(self, pattern: str) -> int:
|
||||
"""패턴 매칭으로 키 삭제"""
|
||||
deleted_count = 0
|
||||
|
||||
if self.is_redis_available and self.redis_client:
|
||||
try:
|
||||
keys = await self.redis_client.keys(pattern)
|
||||
if keys:
|
||||
deleted_count = await self.redis_client.delete(*keys)
|
||||
except Exception as e:
|
||||
print(f"Redis CLEAR_PATTERN 오류: {e}")
|
||||
|
||||
# 메모리 캐시에서 패턴 매칭 삭제
|
||||
import fnmatch
|
||||
keys_to_delete = [
|
||||
key for key in self.memory_cache.keys()
|
||||
if fnmatch.fnmatch(key, pattern)
|
||||
]
|
||||
|
||||
for key in keys_to_delete:
|
||||
del self.memory_cache[key]
|
||||
deleted_count += 1
|
||||
|
||||
return deleted_count
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""캐시 통계"""
|
||||
stats = {
|
||||
"redis_available": self.is_redis_available,
|
||||
"memory_cache_size": len(self.memory_cache),
|
||||
"cache_type": "Redis" if self.is_redis_available else "Memory"
|
||||
}
|
||||
|
||||
# 만료된 메모리 캐시 정리
|
||||
current_time = time.time()
|
||||
expired_keys = [
|
||||
key for key, entry in self.memory_cache.items()
|
||||
if entry['expires_at'] <= current_time
|
||||
]
|
||||
|
||||
for key in expired_keys:
|
||||
del self.memory_cache[key]
|
||||
|
||||
stats["expired_keys_cleaned"] = len(expired_keys)
|
||||
|
||||
return stats
|
||||
|
||||
# 전역 캐시 매니저
|
||||
cache_manager = CacheManager()
|
||||
|
||||
def cached(prefix: str = "default", ttl: int = 300):
|
||||
"""캐싱 데코레이터"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# 캐시 키 생성
|
||||
cache_key = cache_manager._generate_key(prefix, *args, **kwargs)
|
||||
|
||||
# 캐시에서 조회
|
||||
cached_result = await cache_manager.get(cache_key)
|
||||
if cached_result is not None:
|
||||
print(f"🟢 캐시 히트: {cache_key}")
|
||||
return cached_result
|
||||
|
||||
# 캐시 미스 - 함수 실행
|
||||
print(f"🟡 캐시 미스: {cache_key}")
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# 결과 캐싱
|
||||
await cache_manager.set(cache_key, result, ttl)
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
Reference in New Issue
Block a user