475 lines
15 KiB
Python
475 lines
15 KiB
Python
|
|
"""
|
|||
|
|
TraceStudio 缓存管理系统 (v2.0)
|
|||
|
|
支持内存缓存和磁盘缓存,带 TTL 和淘汰策略
|
|||
|
|
"""
|
|||
|
|
from typing import Any, Dict, Optional, Tuple
|
|||
|
|
from enum import Enum
|
|||
|
|
from pathlib import Path
|
|||
|
|
import json
|
|||
|
|
import time
|
|||
|
|
import hashlib
|
|||
|
|
from threading import Lock
|
|||
|
|
from collections import OrderedDict
|
|||
|
|
|
|||
|
|
|
|||
|
|
class CacheEvictionPolicy(Enum):
|
|||
|
|
"""缓存淘汰策略"""
|
|||
|
|
LRU = "lru" # 最近最少使用
|
|||
|
|
FIFO = "fifo" # 先进先出
|
|||
|
|
LFU = "lfu" # 最少使用频率
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MemoryCache:
|
|||
|
|
"""内存缓存实现 (支持 TTL 和 LRU 淘汰)"""
|
|||
|
|
|
|||
|
|
def __init__(self, max_size: int = 1000, ttl: Optional[int] = None, policy: CacheEvictionPolicy = CacheEvictionPolicy.LRU):
|
|||
|
|
"""
|
|||
|
|
初始化内存缓存
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
max_size: 最大条目数
|
|||
|
|
ttl: 过期时间(秒),None 表示不过期
|
|||
|
|
policy: 淘汰策略
|
|||
|
|
"""
|
|||
|
|
self.max_size = max_size
|
|||
|
|
self.ttl = ttl
|
|||
|
|
self.policy = policy
|
|||
|
|
self._cache: OrderedDict = OrderedDict()
|
|||
|
|
self._timestamps: Dict[str, float] = {}
|
|||
|
|
self._access_count: Dict[str, int] = {} # 用于 LFU
|
|||
|
|
self._lock = Lock()
|
|||
|
|
|
|||
|
|
def get(self, key: str) -> Optional[Any]:
|
|||
|
|
"""获取缓存值"""
|
|||
|
|
with self._lock:
|
|||
|
|
# 检查是否过期
|
|||
|
|
if self._is_expired(key):
|
|||
|
|
self._cache.pop(key, None)
|
|||
|
|
self._timestamps.pop(key, None)
|
|||
|
|
self._access_count.pop(key, None)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
if key not in self._cache:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 更新访问信息
|
|||
|
|
self._cache.move_to_end(key) # 移到末尾(LRU)
|
|||
|
|
self._access_count[key] = self._access_count.get(key, 0) + 1
|
|||
|
|
|
|||
|
|
return self._cache[key]
|
|||
|
|
|
|||
|
|
def set(self, key: str, value: Any):
|
|||
|
|
"""设置缓存值"""
|
|||
|
|
with self._lock:
|
|||
|
|
# 如果键已存在,删除它(更新)
|
|||
|
|
if key in self._cache:
|
|||
|
|
del self._cache[key]
|
|||
|
|
|
|||
|
|
# 如果缓存满了,执行淘汰
|
|||
|
|
if len(self._cache) >= self.max_size:
|
|||
|
|
self._evict_one()
|
|||
|
|
|
|||
|
|
# 设置值
|
|||
|
|
self._cache[key] = value
|
|||
|
|
self._timestamps[key] = time.time()
|
|||
|
|
self._access_count[key] = 1
|
|||
|
|
|
|||
|
|
def delete(self, key: str) -> bool:
|
|||
|
|
"""删除缓存项"""
|
|||
|
|
with self._lock:
|
|||
|
|
if key in self._cache:
|
|||
|
|
del self._cache[key]
|
|||
|
|
self._timestamps.pop(key, None)
|
|||
|
|
self._access_count.pop(key, None)
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def clear(self):
|
|||
|
|
"""清空所有缓存"""
|
|||
|
|
with self._lock:
|
|||
|
|
self._cache.clear()
|
|||
|
|
self._timestamps.clear()
|
|||
|
|
self._access_count.clear()
|
|||
|
|
|
|||
|
|
def get_stats(self) -> Dict:
|
|||
|
|
"""获取缓存统计"""
|
|||
|
|
with self._lock:
|
|||
|
|
return {
|
|||
|
|
"current_size": len(self._cache),
|
|||
|
|
"max_size": self.max_size,
|
|||
|
|
"policy": self.policy.value,
|
|||
|
|
"ttl": self.ttl,
|
|||
|
|
"hit_count": sum(self._access_count.values())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _is_expired(self, key: str) -> bool:
|
|||
|
|
"""检查键是否过期"""
|
|||
|
|
if self.ttl is None or key not in self._timestamps:
|
|||
|
|
return False
|
|||
|
|
return time.time() - self._timestamps[key] > self.ttl
|
|||
|
|
|
|||
|
|
def _evict_one(self):
|
|||
|
|
"""执行一次淘汰"""
|
|||
|
|
if not self._cache:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if self.policy == CacheEvictionPolicy.LRU:
|
|||
|
|
# 移除最古老的项(先进先出)
|
|||
|
|
key, _ = self._cache.popitem(last=False)
|
|||
|
|
elif self.policy == CacheEvictionPolicy.FIFO:
|
|||
|
|
# 移除第一个添加的项
|
|||
|
|
key, _ = self._cache.popitem(last=False)
|
|||
|
|
elif self.policy == CacheEvictionPolicy.LFU:
|
|||
|
|
# 移除访问次数最少的项
|
|||
|
|
key = min(self._access_count, key=self._access_count.get)
|
|||
|
|
del self._cache[key]
|
|||
|
|
|
|||
|
|
# 清理元数据
|
|||
|
|
self._timestamps.pop(key, None)
|
|||
|
|
self._access_count.pop(key, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DiskCache:
|
|||
|
|
"""磁盘缓存实现 (支持 TTL 和清理)"""
|
|||
|
|
|
|||
|
|
def __init__(self, cache_dir: Path, ttl: Optional[int] = None):
|
|||
|
|
"""
|
|||
|
|
初始化磁盘缓存
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
cache_dir: 缓存目录路径
|
|||
|
|
ttl: 过期时间(秒),None 表示不过期
|
|||
|
|
"""
|
|||
|
|
self.cache_dir = Path(cache_dir)
|
|||
|
|
self.ttl = ttl
|
|||
|
|
self._metadata_file = self.cache_dir / "_metadata.json"
|
|||
|
|
self._lock = Lock()
|
|||
|
|
|
|||
|
|
# 创建缓存目录
|
|||
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 加载元数据
|
|||
|
|
self._load_metadata()
|
|||
|
|
|
|||
|
|
def get(self, key: str) -> Optional[Any]:
|
|||
|
|
"""获取缓存值"""
|
|||
|
|
with self._lock:
|
|||
|
|
# 检查是否过期
|
|||
|
|
if self._is_expired(key):
|
|||
|
|
self._delete_file(key)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
cache_file = self._get_cache_file(key)
|
|||
|
|
if not cache_file.exists():
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
|||
|
|
data = json.load(f)
|
|||
|
|
return data.get('value')
|
|||
|
|
except Exception:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def set(self, key: str, value: Any):
|
|||
|
|
"""设置缓存值"""
|
|||
|
|
with self._lock:
|
|||
|
|
cache_file = self._get_cache_file(key)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
data = {
|
|||
|
|
'value': value,
|
|||
|
|
'timestamp': time.time()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
with open(cache_file, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(data, f)
|
|||
|
|
|
|||
|
|
# 更新元数据
|
|||
|
|
self._metadata[key] = {
|
|||
|
|
'timestamp': time.time(),
|
|||
|
|
'file': str(cache_file)
|
|||
|
|
}
|
|||
|
|
self._save_metadata()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def delete(self, key: str) -> bool:
|
|||
|
|
"""删除缓存项"""
|
|||
|
|
with self._lock:
|
|||
|
|
return self._delete_file(key)
|
|||
|
|
|
|||
|
|
def clear(self):
|
|||
|
|
"""清空所有缓存"""
|
|||
|
|
with self._lock:
|
|||
|
|
# 删除所有缓存文件
|
|||
|
|
for cache_file in self.cache_dir.glob("*.json"):
|
|||
|
|
if cache_file != self._metadata_file:
|
|||
|
|
try:
|
|||
|
|
cache_file.unlink()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 清空元数据
|
|||
|
|
self._metadata.clear()
|
|||
|
|
self._save_metadata()
|
|||
|
|
|
|||
|
|
def cleanup_expired(self) -> int:
|
|||
|
|
"""清理已过期的缓存项,返回删除数"""
|
|||
|
|
with self._lock:
|
|||
|
|
if self.ttl is None:
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
deleted = 0
|
|||
|
|
expired_keys = []
|
|||
|
|
|
|||
|
|
for key in self._metadata:
|
|||
|
|
if self._is_expired(key):
|
|||
|
|
expired_keys.append(key)
|
|||
|
|
|
|||
|
|
for key in expired_keys:
|
|||
|
|
if self._delete_file(key):
|
|||
|
|
deleted += 1
|
|||
|
|
|
|||
|
|
return deleted
|
|||
|
|
|
|||
|
|
def get_stats(self) -> Dict:
|
|||
|
|
"""获取缓存统计"""
|
|||
|
|
with self._lock:
|
|||
|
|
total_size = sum(
|
|||
|
|
f.stat().st_size
|
|||
|
|
for f in self.cache_dir.glob("*.json")
|
|||
|
|
if f != self._metadata_file
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"items_count": len(self._metadata),
|
|||
|
|
"total_size_bytes": total_size,
|
|||
|
|
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
|||
|
|
"cache_dir": str(self.cache_dir),
|
|||
|
|
"ttl": self.ttl
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _get_cache_file(self, key: str) -> Path:
|
|||
|
|
"""获取缓存文件路径"""
|
|||
|
|
# 使用键的哈希值作为文件名
|
|||
|
|
hashed = hashlib.md5(key.encode()).hexdigest()
|
|||
|
|
return self.cache_dir / f"{hashed}.json"
|
|||
|
|
|
|||
|
|
def _is_expired(self, key: str) -> bool:
|
|||
|
|
"""检查键是否过期"""
|
|||
|
|
if self.ttl is None or key not in self._metadata:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
timestamp = self._metadata[key].get('timestamp', 0)
|
|||
|
|
return time.time() - timestamp > self.ttl
|
|||
|
|
|
|||
|
|
def _delete_file(self, key: str) -> bool:
|
|||
|
|
"""删除缓存文件"""
|
|||
|
|
if key not in self._metadata:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
cache_file = self._get_cache_file(key)
|
|||
|
|
try:
|
|||
|
|
if cache_file.exists():
|
|||
|
|
cache_file.unlink()
|
|||
|
|
del self._metadata[key]
|
|||
|
|
self._save_metadata()
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _load_metadata(self):
|
|||
|
|
"""加载元数据"""
|
|||
|
|
self._metadata = {}
|
|||
|
|
if self._metadata_file.exists():
|
|||
|
|
try:
|
|||
|
|
with open(self._metadata_file, 'r', encoding='utf-8') as f:
|
|||
|
|
self._metadata = json.load(f)
|
|||
|
|
except Exception:
|
|||
|
|
self._metadata = {}
|
|||
|
|
|
|||
|
|
def _save_metadata(self):
|
|||
|
|
"""保存元数据"""
|
|||
|
|
try:
|
|||
|
|
with open(self._metadata_file, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(self._metadata, f)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class CacheManager:
|
|||
|
|
"""缓存管理器 - 统一的缓存接口"""
|
|||
|
|
|
|||
|
|
_memory_cache: Optional[MemoryCache] = None
|
|||
|
|
_disk_cache: Optional[DiskCache] = None
|
|||
|
|
_lock = Lock()
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def init_memory_cache(cls, max_size: int = 1000, ttl: Optional[int] = None,
|
|||
|
|
policy: CacheEvictionPolicy = CacheEvictionPolicy.LRU):
|
|||
|
|
"""初始化内存缓存"""
|
|||
|
|
# Construct the MemoryCache instance before acquiring the class-level lock.
|
|||
|
|
# This avoids a potential deadlock if the MemoryCache constructor
|
|||
|
|
# (or code it calls) touches CacheManager and attempts to acquire
|
|||
|
|
# the same class-level lock (threading.Lock is not re-entrant).
|
|||
|
|
instance = MemoryCache(max_size, ttl, policy)
|
|||
|
|
with cls._lock:
|
|||
|
|
cls._memory_cache = instance
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def init_disk_cache(cls, cache_dir: Path, ttl: Optional[int] = None):
|
|||
|
|
"""初始化磁盘缓存"""
|
|||
|
|
# DiskCache constructor performs I/O (mkdir, file reads). Create the
|
|||
|
|
# instance first, then assign it under the lock to avoid holding the
|
|||
|
|
# class-level lock during potentially blocking I/O operations.
|
|||
|
|
instance = DiskCache(cache_dir, ttl)
|
|||
|
|
with cls._lock:
|
|||
|
|
cls._disk_cache = instance
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def get(cls, key: str, prefer: str = "memory") -> Optional[Any]:
|
|||
|
|
"""
|
|||
|
|
获取缓存值
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
key: 缓存键
|
|||
|
|
prefer: 优先级 ("memory" 或 "disk")
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
缓存值,未找到返回 None
|
|||
|
|
"""
|
|||
|
|
if prefer == "memory":
|
|||
|
|
if cls._memory_cache:
|
|||
|
|
value = cls._memory_cache.get(key)
|
|||
|
|
if value is not None:
|
|||
|
|
return value
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
return cls._disk_cache.get(key)
|
|||
|
|
else:
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
value = cls._disk_cache.get(key)
|
|||
|
|
if value is not None:
|
|||
|
|
return value
|
|||
|
|
if cls._memory_cache:
|
|||
|
|
return cls._memory_cache.get(key)
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def set(cls, key: str, value: Any, storage: str = "memory"):
|
|||
|
|
"""
|
|||
|
|
设置缓存值
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
key: 缓存键
|
|||
|
|
value: 缓存值
|
|||
|
|
storage: 存储位置 ("memory" 或 "disk")
|
|||
|
|
"""
|
|||
|
|
if storage == "memory" and cls._memory_cache:
|
|||
|
|
cls._memory_cache.set(key, value)
|
|||
|
|
elif storage == "disk" and cls._disk_cache:
|
|||
|
|
cls._disk_cache.set(key, value)
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def set_both(cls, key: str, value: Any):
|
|||
|
|
"""同时设置内存和磁盘缓存"""
|
|||
|
|
if cls._memory_cache:
|
|||
|
|
cls._memory_cache.set(key, value)
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
cls._disk_cache.set(key, value)
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def delete(cls, key: str, storage: Optional[str] = None) -> bool:
|
|||
|
|
"""
|
|||
|
|
删除缓存项
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
key: 缓存键
|
|||
|
|
storage: 存储位置,None 表示删除所有存储中的项
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
是否删除成功
|
|||
|
|
"""
|
|||
|
|
deleted = False
|
|||
|
|
|
|||
|
|
if storage is None or storage == "memory":
|
|||
|
|
if cls._memory_cache:
|
|||
|
|
deleted |= cls._memory_cache.delete(key)
|
|||
|
|
|
|||
|
|
if storage is None or storage == "disk":
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
deleted |= cls._disk_cache.delete(key)
|
|||
|
|
|
|||
|
|
return deleted
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def clear(cls, storage: Optional[str] = None):
|
|||
|
|
"""
|
|||
|
|
清空缓存
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
storage: 存储位置,None 表示清空所有存储
|
|||
|
|
"""
|
|||
|
|
if storage is None or storage == "memory":
|
|||
|
|
if cls._memory_cache:
|
|||
|
|
cls._memory_cache.clear()
|
|||
|
|
|
|||
|
|
if storage is None or storage == "disk":
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
cls._disk_cache.clear()
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def cleanup_expired(cls) -> Tuple[int, int]:
|
|||
|
|
"""
|
|||
|
|
清理所有过期缓存
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
(内存清理数, 磁盘清理数)
|
|||
|
|
"""
|
|||
|
|
memory_deleted = 0
|
|||
|
|
disk_deleted = 0
|
|||
|
|
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
disk_deleted = cls._disk_cache.cleanup_expired()
|
|||
|
|
|
|||
|
|
return memory_deleted, disk_deleted
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def get_stats(cls) -> Dict:
|
|||
|
|
"""获取缓存统计信息"""
|
|||
|
|
return {
|
|||
|
|
"memory": cls._memory_cache.get_stats() if cls._memory_cache else None,
|
|||
|
|
"disk": cls._disk_cache.get_stats() if cls._disk_cache else None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def get_total_size_mb(cls) -> float:
|
|||
|
|
"""获取总缓存大小(MB)"""
|
|||
|
|
total = 0
|
|||
|
|
|
|||
|
|
if cls._disk_cache:
|
|||
|
|
stats = cls._disk_cache.get_stats()
|
|||
|
|
total += stats.get("total_size_mb", 0)
|
|||
|
|
|
|||
|
|
return round(total, 2)
|
|||
|
|
|
|||
|
|
class NodeCacheAdapter:
|
|||
|
|
def __init__(self, storage_mode: str):
|
|||
|
|
self.storage = storage_mode
|
|||
|
|
|
|||
|
|
def get(self, key: str):
|
|||
|
|
value = CacheManager.get(key, prefer=self.storage)
|
|||
|
|
print(f"NodeCacheAdapter.get<<<<<: key={key}, storage={self.storage}, value={value}")
|
|||
|
|
return value
|
|||
|
|
|
|||
|
|
def set(self, key: str, value: Dict):
|
|||
|
|
print(f"NodeCacheAdapter.set>>>>>: key={key}, storage={self.storage}, value={value}")
|
|||
|
|
CacheManager.set(key, value, storage=self.storage)
|
|||
|
|
return self.get(key)
|
|||
|
|
|
|||
|
|
def delete(self, key: str) -> bool:
|
|||
|
|
return CacheManager.delete(key, storage=self.storage)
|
|||
|
|
|
|||
|
|
def clear(self):
|
|||
|
|
return CacheManager.clear(storage=self.storage)
|