Your fixed commit message here
This commit is contained in:
222
src/cache.py
Normal file
222
src/cache.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user