Files
QPP/src/cache.py

222 lines
6.8 KiB
Python

"""
High-performance in-memory caching module with LRU eviction policy.
"""
import time
from typing import Any, Callable, Optional, Dict, List, Tuple
import threading
import functools
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class CacheEntry:
"""Represents a single cache entry with metadata."""
__slots__ = ('value', 'timestamp', 'expires_at', 'hits')
def __init__(self, value: Any, timeout: int):
self.value = value
self.timestamp = time.time()
self.expires_at = self.timestamp + timeout
self.hits = 0
def is_expired(self) -> bool:
"""Check if the cache entry has expired."""
return time.time() >= self.expires_at
def hit(self) -> None:
"""Increment the hit counter."""
self.hits += 1
class FastMemoryCache:
"""
High-performance in-memory cache with LRU eviction policy.
Thread-safe and optimized for frequent reads.
"""
def __init__(self, max_size: int = 1000, default_timeout: int = 300):
"""
Initialize the cache.
Args:
max_size: Maximum number of items to store in cache
default_timeout: Default expiration time in seconds
"""
self.max_size = max_size
self.default_timeout = default_timeout
self._cache: Dict[str, CacheEntry] = {}
self._lock = threading.RLock()
self._hits = 0
self._misses = 0
self._evictions = 0
# Start background cleaner thread
self._cleaner_thread = threading.Thread(target=self._clean_expired, daemon=True)
self._cleaner_thread.start()
def get(self, key: str) -> Optional[Any]:
"""
Get a value from the cache.
Args:
key: Cache key
Returns:
Cached value or None if not found/expired
"""
with self._lock:
entry = self._cache.get(key)
if entry is None:
self._misses += 1
return None
if entry.is_expired():
del self._cache[key]
self._misses += 1
self._evictions += 1
return None
entry.hit()
self._hits += 1
return entry.value
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> None:
"""
Set a value in the cache.
Args:
key: Cache key
value: Value to cache
timeout: Optional timeout in seconds (uses default if None)
"""
if timeout is None:
timeout = self.default_timeout
with self._lock:
# Evict if cache is full (LRU policy)
if len(self._cache) >= self.max_size and key not in self._cache:
self._evict_lru()
self._cache[key] = CacheEntry(value, timeout)
def delete(self, key: str) -> bool:
"""
Delete a key from the cache.
Args:
key: Cache key to delete
Returns:
True if key was deleted, False if not found
"""
with self._lock:
if key in self._cache:
del self._cache[key]
self._evictions += 1
return True
return False
def clear(self) -> None:
"""Clear all items from the cache."""
with self._lock:
self._cache.clear()
self._evictions += len(self._cache)
def _evict_lru(self) -> None:
"""Evict the least recently used item from the cache."""
if not self._cache:
return
# Find the entry with the fewest hits (simplified LRU)
lru_key = min(self._cache.keys(), key=lambda k: self._cache[k].hits)
del self._cache[lru_key]
self._evictions += 1
def _clean_expired(self) -> None:
"""Background thread to clean expired entries."""
while True:
time.sleep(60) # Clean every minute
with self._lock:
expired_keys = [
key for key, entry in self._cache.items()
if entry.is_expired()
]
for key in expired_keys:
del self._cache[key]
self._evictions += 1
if expired_keys:
logger.info(f"Cleaned {len(expired_keys)} expired cache entries")
def get_stats(self) -> Dict[str, Any]:
"""
Get cache statistics.
Returns:
Dictionary with cache statistics
"""
with self._lock:
return {
'size': len(self._cache),
'hits': self._hits,
'misses': self._misses,
'hit_ratio': self._hits / (self._hits + self._misses) if (self._hits + self._misses) > 0 else 0,
'evictions': self._evictions,
'max_size': self.max_size
}
def keys(self) -> List[str]:
"""Get all cache keys."""
with self._lock:
return list(self._cache.keys())
# Global cache instance
cache = FastMemoryCache(max_size=2000, default_timeout=300)
def cached(timeout: Optional[int] = None, unless: Optional[Callable] = None):
"""
Decorator for caching function results.
Args:
timeout: Cache timeout in seconds
unless: Callable that returns True to bypass cache
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Bypass cache if unless condition is met
if unless and unless():
return func(*args, **kwargs)
# Create cache key from function name and arguments
key_parts = [func.__module__, func.__name__]
key_parts.extend(str(arg) for arg in args)
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
key = "|".join(key_parts)
# Try to get from cache
cached_result = cache.get(key)
if cached_result is not None:
logger.info(f"Cache hit for {func.__name__}")
return cached_result
# Call function and cache result
result = func(*args, **kwargs)
cache.set(key, result, timeout)
logger.info(f"Cache miss for {func.__name__}, caching result")
return result
return wrapper
return decorator
def cache_clear() -> None:
"""Clear the entire cache."""
cache.clear()
logger.info("Cache cleared")
def cache_stats() -> Dict[str, Any]:
"""Get cache statistics."""
return cache.get_stats()