beadboard/.agents/skills/rlm-mem/brain/scripts/cache_system.py

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()
}