mirror of
https://github.com/HabiRabbu/Musicseerr
synced 2026-04-21 21:47:16 +00:00
154 lines
4.4 KiB
Python
154 lines
4.4 KiB
Python
import asyncio
|
|
import logging
|
|
import sys
|
|
import time
|
|
from typing import Any, Optional
|
|
from abc import ABC, abstractmethod
|
|
from collections import OrderedDict
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CacheInterface(ABC):
|
|
@abstractmethod
|
|
async def get(self, key: str) -> Optional[Any]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def delete(self, key: str) -> None:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def clear(self) -> None:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def clear_prefix(self, prefix: str) -> int:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def cleanup_expired(self) -> int:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def size(self) -> int:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def estimate_memory_bytes(self) -> int:
|
|
pass
|
|
|
|
|
|
class CacheEntry:
|
|
__slots__ = ('value', 'expires_at')
|
|
|
|
def __init__(self, value: Any, ttl_seconds: int):
|
|
self.value = value
|
|
self.expires_at = time.time() + ttl_seconds
|
|
|
|
def is_expired(self) -> bool:
|
|
return time.time() > self.expires_at
|
|
|
|
|
|
class InMemoryCache(CacheInterface):
|
|
def __init__(self, max_entries: int = 10000):
|
|
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
self._lock = asyncio.Lock()
|
|
self._max_entries = max_entries
|
|
self._evictions = 0
|
|
self._hits = 0
|
|
self._misses = 0
|
|
|
|
async def get(self, key: str) -> Optional[Any]:
|
|
async with self._lock:
|
|
entry = self._cache.get(key)
|
|
if entry is None:
|
|
self._misses += 1
|
|
return None
|
|
|
|
if entry.is_expired():
|
|
self._cache.pop(key, None)
|
|
self._misses += 1
|
|
return None
|
|
|
|
self._cache.move_to_end(key)
|
|
self._hits += 1
|
|
return entry.value
|
|
|
|
async def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None:
|
|
async with self._lock:
|
|
if key not in self._cache and len(self._cache) >= self._max_entries:
|
|
oldest_key, _ = self._cache.popitem(last=False)
|
|
self._evictions += 1
|
|
if self._evictions % 100 == 0:
|
|
logger.info(f"Cache LRU evictions: {self._evictions}, current size: {len(self._cache)}")
|
|
|
|
self._cache[key] = CacheEntry(value, ttl_seconds)
|
|
self._cache.move_to_end(key)
|
|
|
|
async def delete(self, key: str) -> None:
|
|
async with self._lock:
|
|
self._cache.pop(key, None)
|
|
|
|
async def clear(self) -> None:
|
|
async with self._lock:
|
|
self._cache.clear()
|
|
|
|
async def clear_prefix(self, prefix: str) -> int:
|
|
async with self._lock:
|
|
keys_to_remove = [k for k in self._cache.keys() if k.startswith(prefix)]
|
|
for key in keys_to_remove:
|
|
self._cache.pop(key, None)
|
|
|
|
if keys_to_remove:
|
|
logger.info(f"Cleared {len(keys_to_remove)} cache entries with prefix '{prefix}'")
|
|
|
|
return len(keys_to_remove)
|
|
|
|
async def cleanup_expired(self) -> int:
|
|
now = time.time()
|
|
|
|
async with self._lock:
|
|
expired_keys = [
|
|
key for key, entry in self._cache.items()
|
|
if now > entry.expires_at
|
|
]
|
|
for key in expired_keys:
|
|
self._cache.pop(key, None)
|
|
|
|
if expired_keys:
|
|
logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
|
|
|
|
return len(expired_keys)
|
|
|
|
def size(self) -> int:
|
|
return len(self._cache)
|
|
|
|
def estimate_memory_bytes(self) -> int:
|
|
total_size = 0
|
|
|
|
total_size += sys.getsizeof(self._cache)
|
|
|
|
for key, entry in self._cache.items():
|
|
total_size += sys.getsizeof(key)
|
|
total_size += sys.getsizeof(entry)
|
|
total_size += sys.getsizeof(entry.value)
|
|
|
|
return total_size
|
|
|
|
def get_stats(self) -> dict[str, Any]:
|
|
total = self._hits + self._misses
|
|
hit_rate = (self._hits / total * 100) if total > 0 else 0.0
|
|
return {
|
|
"size": len(self._cache),
|
|
"max_entries": self._max_entries,
|
|
"hits": self._hits,
|
|
"misses": self._misses,
|
|
"hit_rate_percent": round(hit_rate, 2),
|
|
"evictions": self._evictions,
|
|
"memory_bytes": self.estimate_memory_bytes()
|
|
}
|