Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시 - 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시 - 외부링/필러/내부링/추가구성 모든 정보 포함 - 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
267 lines
10 KiB
Python
267 lines
10 KiB
Python
"""
|
|
Redis 캐시 관리 유틸리티
|
|
성능 향상을 위한 캐싱 전략 구현
|
|
"""
|
|
import json
|
|
import redis
|
|
from typing import Any, Optional, Dict, List
|
|
from datetime import timedelta
|
|
import hashlib
|
|
import pickle
|
|
|
|
from ..config import get_settings
|
|
from .logger import get_logger
|
|
|
|
settings = get_settings()
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class CacheManager:
|
|
"""Redis 캐시 관리 클래스"""
|
|
|
|
def __init__(self):
|
|
try:
|
|
# Redis 연결 설정
|
|
self.redis_client = redis.from_url(
|
|
settings.redis.url,
|
|
decode_responses=False, # 바이너리 데이터 지원
|
|
socket_connect_timeout=5,
|
|
socket_timeout=5,
|
|
retry_on_timeout=True
|
|
)
|
|
|
|
# 연결 테스트
|
|
self.redis_client.ping()
|
|
logger.info("Redis 연결 성공")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Redis 연결 실패: {e}")
|
|
self.redis_client = None
|
|
|
|
def _generate_key(self, prefix: str, *args, **kwargs) -> str:
|
|
"""캐시 키 생성"""
|
|
# 인자들을 문자열로 변환하여 해시 생성
|
|
key_parts = [str(arg) for arg in args]
|
|
key_parts.extend([f"{k}:{v}" for k, v in sorted(kwargs.items())])
|
|
|
|
if key_parts:
|
|
key_hash = hashlib.md5("|".join(key_parts).encode()).hexdigest()[:8]
|
|
return f"tkmp:{prefix}:{key_hash}"
|
|
else:
|
|
return f"tkmp:{prefix}"
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
"""캐시에서 데이터 조회"""
|
|
if not self.redis_client:
|
|
return None
|
|
|
|
try:
|
|
data = self.redis_client.get(key)
|
|
if data:
|
|
return pickle.loads(data)
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"캐시 조회 실패 - key: {key}, error: {e}")
|
|
return None
|
|
|
|
def set(self, key: str, value: Any, expire: int = 3600) -> bool:
|
|
"""캐시에 데이터 저장"""
|
|
if not self.redis_client:
|
|
return False
|
|
|
|
try:
|
|
serialized_data = pickle.dumps(value)
|
|
result = self.redis_client.setex(key, expire, serialized_data)
|
|
logger.debug(f"캐시 저장 - key: {key}, expire: {expire}s")
|
|
return result
|
|
except Exception as e:
|
|
logger.warning(f"캐시 저장 실패 - key: {key}, error: {e}")
|
|
return False
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""캐시에서 데이터 삭제"""
|
|
if not self.redis_client:
|
|
return False
|
|
|
|
try:
|
|
result = self.redis_client.delete(key)
|
|
logger.debug(f"캐시 삭제 - key: {key}")
|
|
return bool(result)
|
|
except Exception as e:
|
|
logger.warning(f"캐시 삭제 실패 - key: {key}, error: {e}")
|
|
return False
|
|
|
|
def delete_pattern(self, pattern: str) -> int:
|
|
"""패턴에 맞는 캐시 키들 삭제"""
|
|
if not self.redis_client:
|
|
return 0
|
|
|
|
try:
|
|
keys = self.redis_client.keys(pattern)
|
|
if keys:
|
|
deleted = self.redis_client.delete(*keys)
|
|
logger.info(f"패턴 캐시 삭제 - pattern: {pattern}, deleted: {deleted}")
|
|
return deleted
|
|
return 0
|
|
except Exception as e:
|
|
logger.warning(f"패턴 캐시 삭제 실패 - pattern: {pattern}, error: {e}")
|
|
return 0
|
|
|
|
def exists(self, key: str) -> bool:
|
|
"""캐시 키 존재 여부 확인"""
|
|
if not self.redis_client:
|
|
return False
|
|
|
|
try:
|
|
return bool(self.redis_client.exists(key))
|
|
except Exception as e:
|
|
logger.warning(f"캐시 존재 확인 실패 - key: {key}, error: {e}")
|
|
return False
|
|
|
|
def get_ttl(self, key: str) -> int:
|
|
"""캐시 TTL 조회"""
|
|
if not self.redis_client:
|
|
return -1
|
|
|
|
try:
|
|
return self.redis_client.ttl(key)
|
|
except Exception as e:
|
|
logger.warning(f"캐시 TTL 조회 실패 - key: {key}, error: {e}")
|
|
return -1
|
|
|
|
|
|
class TKMPCache:
|
|
"""TK-MP 프로젝트 전용 캐시 래퍼"""
|
|
|
|
def __init__(self):
|
|
self.cache = CacheManager()
|
|
|
|
# 캐시 TTL 설정 (초 단위)
|
|
self.ttl_config = {
|
|
"file_list": 300, # 5분 - 파일 목록
|
|
"material_list": 600, # 10분 - 자재 목록
|
|
"job_list": 1800, # 30분 - 작업 목록
|
|
"classification": 3600, # 1시간 - 분류 결과
|
|
"statistics": 900, # 15분 - 통계 데이터
|
|
"comparison": 1800, # 30분 - 리비전 비교
|
|
}
|
|
|
|
def get_file_list(self, job_no: Optional[str] = None, show_history: bool = False) -> Optional[List[Dict]]:
|
|
"""파일 목록 캐시 조회"""
|
|
key = self.cache._generate_key("files", job_no=job_no, history=show_history)
|
|
return self.cache.get(key)
|
|
|
|
def set_file_list(self, files: List[Dict], job_no: Optional[str] = None, show_history: bool = False) -> bool:
|
|
"""파일 목록 캐시 저장"""
|
|
key = self.cache._generate_key("files", job_no=job_no, history=show_history)
|
|
return self.cache.set(key, files, self.ttl_config["file_list"])
|
|
|
|
def get_material_list(self, file_id: int) -> Optional[List[Dict]]:
|
|
"""자재 목록 캐시 조회"""
|
|
key = self.cache._generate_key("materials", file_id=file_id)
|
|
return self.cache.get(key)
|
|
|
|
def set_material_list(self, materials: List[Dict], file_id: int) -> bool:
|
|
"""자재 목록 캐시 저장"""
|
|
key = self.cache._generate_key("materials", file_id=file_id)
|
|
return self.cache.set(key, materials, self.ttl_config["material_list"])
|
|
|
|
def get_job_list(self) -> Optional[List[Dict]]:
|
|
"""작업 목록 캐시 조회"""
|
|
key = self.cache._generate_key("jobs")
|
|
return self.cache.get(key)
|
|
|
|
def set_job_list(self, jobs: List[Dict]) -> bool:
|
|
"""작업 목록 캐시 저장"""
|
|
key = self.cache._generate_key("jobs")
|
|
return self.cache.set(key, jobs, self.ttl_config["job_list"])
|
|
|
|
def get_classification_result(self, description: str, category: str) -> Optional[Dict]:
|
|
"""분류 결과 캐시 조회"""
|
|
key = self.cache._generate_key("classification", desc=description, cat=category)
|
|
return self.cache.get(key)
|
|
|
|
def set_classification_result(self, result: Dict, description: str, category: str) -> bool:
|
|
"""분류 결과 캐시 저장"""
|
|
key = self.cache._generate_key("classification", desc=description, cat=category)
|
|
return self.cache.set(key, result, self.ttl_config["classification"])
|
|
|
|
def get_statistics(self, job_no: str, stat_type: str) -> Optional[Dict]:
|
|
"""통계 데이터 캐시 조회"""
|
|
key = self.cache._generate_key("stats", job_no=job_no, type=stat_type)
|
|
return self.cache.get(key)
|
|
|
|
def set_statistics(self, stats: Dict, job_no: str, stat_type: str) -> bool:
|
|
"""통계 데이터 캐시 저장"""
|
|
key = self.cache._generate_key("stats", job_no=job_no, type=stat_type)
|
|
return self.cache.set(key, stats, self.ttl_config["statistics"])
|
|
|
|
def get_revision_comparison(self, job_no: str, rev1: str, rev2: str) -> Optional[Dict]:
|
|
"""리비전 비교 결과 캐시 조회"""
|
|
key = self.cache._generate_key("comparison", job_no=job_no, rev1=rev1, rev2=rev2)
|
|
return self.cache.get(key)
|
|
|
|
def set_revision_comparison(self, comparison: Dict, job_no: str, rev1: str, rev2: str) -> bool:
|
|
"""리비전 비교 결과 캐시 저장"""
|
|
key = self.cache._generate_key("comparison", job_no=job_no, rev1=rev1, rev2=rev2)
|
|
return self.cache.set(key, comparison, self.ttl_config["comparison"])
|
|
|
|
def invalidate_job_cache(self, job_no: str):
|
|
"""특정 작업의 모든 캐시 무효화"""
|
|
patterns = [
|
|
f"tkmp:files:*job_no:{job_no}*",
|
|
f"tkmp:materials:*job_no:{job_no}*",
|
|
f"tkmp:stats:*job_no:{job_no}*",
|
|
f"tkmp:comparison:*job_no:{job_no}*"
|
|
]
|
|
|
|
total_deleted = 0
|
|
for pattern in patterns:
|
|
deleted = self.cache.delete_pattern(pattern)
|
|
total_deleted += deleted
|
|
|
|
logger.info(f"작업 캐시 무효화 완료 - job_no: {job_no}, deleted: {total_deleted}")
|
|
return total_deleted
|
|
|
|
def invalidate_file_cache(self, file_id: int):
|
|
"""특정 파일의 모든 캐시 무효화"""
|
|
patterns = [
|
|
f"tkmp:materials:*file_id:{file_id}*",
|
|
f"tkmp:files:*" # 파일 목록도 갱신 필요
|
|
]
|
|
|
|
total_deleted = 0
|
|
for pattern in patterns:
|
|
deleted = self.cache.delete_pattern(pattern)
|
|
total_deleted += deleted
|
|
|
|
logger.info(f"파일 캐시 무효화 완료 - file_id: {file_id}, deleted: {total_deleted}")
|
|
return total_deleted
|
|
|
|
def get_cache_info(self) -> Dict[str, Any]:
|
|
"""캐시 상태 정보 조회"""
|
|
if not self.cache.redis_client:
|
|
return {"status": "disconnected"}
|
|
|
|
try:
|
|
info = self.cache.redis_client.info()
|
|
return {
|
|
"status": "connected",
|
|
"used_memory": info.get("used_memory_human", "N/A"),
|
|
"connected_clients": info.get("connected_clients", 0),
|
|
"total_commands_processed": info.get("total_commands_processed", 0),
|
|
"keyspace_hits": info.get("keyspace_hits", 0),
|
|
"keyspace_misses": info.get("keyspace_misses", 0),
|
|
"hit_rate": round(
|
|
info.get("keyspace_hits", 0) /
|
|
max(info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0), 1) * 100, 2
|
|
)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"캐시 정보 조회 실패: {e}")
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
# 전역 캐시 인스턴스
|
|
tkmp_cache = TKMPCache()
|