241 lines
No EOL
6.9 KiB
Python
241 lines
No EOL
6.9 KiB
Python
"""
|
|
RLM-MEM - Cache System (D5.1)
|
|
Simple in-memory caching for frequently accessed data.
|
|
"""
|
|
|
|
import time
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
from dataclasses import dataclass
|
|
from threading import Lock
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class CacheEntry:
|
|
"""Single cache entry."""
|
|
value: Any
|
|
timestamp: float
|
|
ttl: int # Time to live in seconds
|
|
|
|
|
|
class MemoryCache:
|
|
"""Thread-safe in-memory cache with TTL support."""
|
|
|
|
def __init__(self, default_ttl: int = 300):
|
|
"""
|
|
Initialize memory cache.
|
|
|
|
Args:
|
|
default_ttl: Default time-to-live in seconds (5 minutes)
|
|
"""
|
|
self._cache: Dict[str, CacheEntry] = {}
|
|
self._default_ttl = default_ttl
|
|
self._lock = Lock()
|
|
self._hits = 0
|
|
self._misses = 0
|
|
self._evictions = 0
|
|
self._lookups = 0
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
"""
|
|
Get value from cache if not expired.
|
|
|
|
Args:
|
|
key: Cache key
|
|
|
|
Returns:
|
|
Cached value or None if not found/expired
|
|
"""
|
|
with self._lock:
|
|
self._lookups += 1
|
|
entry = self._cache.get(key)
|
|
if entry is None:
|
|
self._misses += 1
|
|
logger.debug("Memory cache miss for %s", key)
|
|
return None
|
|
|
|
# Check if expired
|
|
if time.time() - entry.timestamp > entry.ttl:
|
|
del self._cache[key]
|
|
self._misses += 1
|
|
self._evictions += 1
|
|
logger.debug("Memory cache evicted expired entry for %s", key)
|
|
return None
|
|
|
|
self._hits += 1
|
|
logger.debug("Memory cache hit for %s", key)
|
|
return entry.value
|
|
|
|
def set(self, key: str, value: Any, ttl: int = None):
|
|
"""
|
|
Store value in cache.
|
|
|
|
Args:
|
|
key: Cache key
|
|
value: Value to cache
|
|
ttl: Time-to-live in seconds (uses default if None)
|
|
"""
|
|
if ttl is None:
|
|
ttl = self._default_ttl
|
|
|
|
with self._lock:
|
|
self._cache[key] = CacheEntry(
|
|
value=value,
|
|
timestamp=time.time(),
|
|
ttl=ttl
|
|
)
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""
|
|
Delete key from cache.
|
|
|
|
Args:
|
|
key: Cache key
|
|
|
|
Returns:
|
|
True if key was present and deleted
|
|
"""
|
|
with self._lock:
|
|
if key in self._cache:
|
|
del self._cache[key]
|
|
return True
|
|
return False
|
|
|
|
def clear(self):
|
|
"""Clear all cache entries."""
|
|
with self._lock:
|
|
self._cache.clear()
|
|
|
|
def cleanup(self):
|
|
"""Remove all expired entries."""
|
|
with self._lock:
|
|
now = time.time()
|
|
expired = [
|
|
key for key, entry in self._cache.items()
|
|
if now - entry.timestamp > entry.ttl
|
|
]
|
|
for key in expired:
|
|
del self._cache[key]
|
|
self._evictions += len(expired)
|
|
if expired:
|
|
logger.debug("Memory cache cleanup evicted %d entries", len(expired))
|
|
return len(expired)
|
|
|
|
def stats(self) -> Dict[str, Any]:
|
|
"""Get cache statistics."""
|
|
with self._lock:
|
|
hit_rate = (self._hits / self._lookups) if self._lookups else 0.0
|
|
return {
|
|
"size": len(self._cache),
|
|
"default_ttl": self._default_ttl,
|
|
"lookups": self._lookups,
|
|
"hits": self._hits,
|
|
"misses": self._misses,
|
|
"evictions": self._evictions,
|
|
"hit_rate": round(hit_rate, 4)
|
|
}
|
|
|
|
|
|
class CacheManager:
|
|
"""
|
|
Manages in-memory cache.
|
|
(Disk cache tier removed per ADR 0002)
|
|
"""
|
|
|
|
def __init__(self, cache_dir: str = None, default_ttl: int = 300):
|
|
"""
|
|
Initialize cache manager.
|
|
|
|
Args:
|
|
cache_dir: Ignored (legacy compatibility)
|
|
default_ttl: Default time-to-live in seconds
|
|
"""
|
|
self.memory = MemoryCache(default_ttl)
|
|
self._lock = Lock()
|
|
self._metrics: Dict[str, int] = {
|
|
"get_calls": 0,
|
|
"memory_hits": 0,
|
|
"misses": 0,
|
|
"set_calls": 0,
|
|
"delete_calls": 0,
|
|
"clear_calls": 0,
|
|
}
|
|
|
|
def get(self, key: str, use_disk: bool = False) -> Optional[Any]:
|
|
"""
|
|
Get from memory cache.
|
|
|
|
Args:
|
|
key: Cache key
|
|
use_disk: Ignored (legacy compatibility)
|
|
|
|
Returns:
|
|
Cached value or None
|
|
"""
|
|
with self._lock:
|
|
self._metrics["get_calls"] += 1
|
|
|
|
value = self.memory.get(key)
|
|
if value is not None:
|
|
with self._lock:
|
|
self._metrics["memory_hits"] += 1
|
|
return value
|
|
|
|
with self._lock:
|
|
self._metrics["misses"] += 1
|
|
return None
|
|
|
|
def set(self, key: str, value: Any, ttl: int = None, use_disk: bool = False):
|
|
"""
|
|
Store in cache.
|
|
|
|
Args:
|
|
key: Cache key
|
|
value: Value to cache
|
|
ttl: Time-to-live
|
|
use_disk: Ignored
|
|
"""
|
|
with self._lock:
|
|
self._metrics["set_calls"] += 1
|
|
self.memory.set(key, value, ttl)
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""Delete from cache."""
|
|
with self._lock:
|
|
self._metrics["delete_calls"] += 1
|
|
return self.memory.delete(key)
|
|
|
|
def clear(self):
|
|
"""Clear all caches."""
|
|
with self._lock:
|
|
self._metrics["clear_calls"] += 1
|
|
self.memory.clear()
|
|
|
|
def telemetry(self) -> Dict[str, Any]:
|
|
"""Return manager-level telemetry with derived rates."""
|
|
with self._lock:
|
|
metrics = dict(self._metrics)
|
|
total_gets = metrics["get_calls"]
|
|
metrics["memory_hit_rate"] = round(
|
|
(metrics["memory_hits"] / total_gets), 4
|
|
) if total_gets else 0.0
|
|
metrics["miss_rate"] = round(
|
|
(metrics["misses"] / total_gets), 4
|
|
) if total_gets else 0.0
|
|
return metrics
|
|
|
|
def cleanup(self) -> Dict[str, int]:
|
|
"""Cleanup expired entries from cache."""
|
|
mem_removed = self.memory.cleanup()
|
|
return {"memory": mem_removed}
|
|
|
|
def stats(self) -> Dict[str, Any]:
|
|
"""Get combined cache statistics."""
|
|
return {
|
|
"memory": self.memory.stats(),
|
|
"manager": self.telemetry()
|
|
} |