222 lines
6.8 KiB
Python
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() |