178 lines
5.1 KiB
Python
178 lines
5.1 KiB
Python
import hashlib
|
|
import logging
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
from aiohttp import web
|
|
|
|
from app.library.Services import Services
|
|
|
|
from .Scheduler import Scheduler
|
|
from .Singleton import ThreadSafe
|
|
|
|
LOG = logging.getLogger("cache")
|
|
|
|
|
|
class Cache(metaclass=ThreadSafe):
|
|
def __init__(self) -> None:
|
|
"""
|
|
Initialize the Cache.
|
|
"""
|
|
self._cache: dict[str, tuple[Any, float | None]] = {}
|
|
self._lock = threading.Lock()
|
|
|
|
@staticmethod
|
|
def get_instance() -> "Cache":
|
|
"""
|
|
Get the singleton instance of Cache.
|
|
|
|
Returns:
|
|
Cache: The singleton instance of Cache.
|
|
|
|
"""
|
|
return Cache()
|
|
|
|
def attach(self, _: web.Application) -> None:
|
|
Services.get_instance().add("cache", self)
|
|
Scheduler.get_instance().add(
|
|
timer="* * * * *",
|
|
func=self.cleanup,
|
|
id=f"{__class__.__name__}.{__class__.cleanup.__name__}",
|
|
)
|
|
|
|
def set(self, key: str, value: Any, ttl: float | None = None) -> None:
|
|
"""
|
|
Synchronously set a value in the cache with an optional time-to-live.
|
|
If ttl is None, the entry never expires.
|
|
"""
|
|
expire_at = None if ttl is None else time.time() + ttl
|
|
with self._lock:
|
|
self._cache[key] = (value, expire_at)
|
|
|
|
def get(self, key: str, default: Any | None = None) -> Any | None:
|
|
"""
|
|
Synchronously retrieve a value from the cache if it exists and hasn't expired.
|
|
"""
|
|
with self._lock:
|
|
entry = self._cache.get(key)
|
|
if entry is None:
|
|
return default
|
|
|
|
value, expire_at = entry
|
|
if expire_at is not None and time.time() >= expire_at:
|
|
del self._cache[key]
|
|
return default
|
|
return value
|
|
|
|
def ttl(self, key: str) -> float | None:
|
|
"""
|
|
Synchronously retrieve the time-to-live of a key in the cache.
|
|
"""
|
|
with self._lock:
|
|
entry = self._cache.get(key)
|
|
if entry is None:
|
|
return None
|
|
|
|
_, expire_at = entry
|
|
if expire_at is None:
|
|
return None
|
|
return expire_at - time.time()
|
|
|
|
def has(self, key: str) -> bool:
|
|
"""
|
|
Synchronously check if a key exists in the cache and hasn't expired.
|
|
"""
|
|
with self._lock:
|
|
entry = self._cache.get(key)
|
|
if entry is None:
|
|
return False
|
|
|
|
_, expire_at = entry
|
|
if expire_at is not None and time.time() >= expire_at:
|
|
del self._cache[key]
|
|
return False
|
|
return True
|
|
|
|
def delete(self, key: str) -> None:
|
|
"""
|
|
Synchronously remove a key from the cache if it exists.
|
|
"""
|
|
with self._lock:
|
|
self._cache.pop(key, None)
|
|
|
|
def clear(self) -> None:
|
|
"""
|
|
Synchronously clear all items from the cache.
|
|
"""
|
|
with self._lock:
|
|
self._cache.clear()
|
|
|
|
def hash(self, key: str) -> str:
|
|
"""
|
|
Generate a SHA-256 hash for the given input string.
|
|
|
|
:param key (str): The string to hash.
|
|
:return: A hexadecimal SHA-256 hash of the input string.
|
|
"""
|
|
return hashlib.sha256(key.encode("utf-8")).hexdigest()
|
|
|
|
# Asynchronous counterparts for non-blocking interfaces
|
|
async def aset(self, key: str, value: Any, ttl: float | None = None) -> None:
|
|
"""
|
|
Asynchronously set a value in the cache.
|
|
"""
|
|
self.set(key, value, ttl)
|
|
|
|
async def aget(self, key: str, default: Any | None = None) -> Any | None:
|
|
"""
|
|
Asynchronously retrieve a value from the cache.
|
|
"""
|
|
return self.get(key, default)
|
|
|
|
async def attl(self, key: str) -> float | None:
|
|
"""
|
|
Asynchronously retrieve the time-to-live of a key in the cache.
|
|
"""
|
|
return self.ttl(key)
|
|
|
|
async def ahas(self, key: str) -> bool:
|
|
"""
|
|
Asynchronously check if a key exists in the cache.
|
|
"""
|
|
return self.has(key)
|
|
|
|
async def adelete(self, key: str) -> None:
|
|
"""
|
|
Asynchronously remove a key from the cache.
|
|
"""
|
|
self.delete(key)
|
|
|
|
async def aclear(self) -> None:
|
|
"""
|
|
Asynchronously clear all items from the cache.
|
|
"""
|
|
self.clear()
|
|
|
|
async def ahash(self, key: str) -> str:
|
|
"""
|
|
Asynchronously generate a SHA-256 hash for the given input string.
|
|
"""
|
|
return self.hash(key=key)
|
|
|
|
async def cleanup(self) -> None:
|
|
"""
|
|
Remove all expired entries from the cache.
|
|
Called periodically by the scheduler.
|
|
"""
|
|
with self._lock:
|
|
now = time.time()
|
|
expired_keys: list[str] = [
|
|
key for key, (_, expire_at) in self._cache.items() if expire_at is not None and now >= expire_at
|
|
]
|
|
|
|
for key in expired_keys:
|
|
del self._cache[key]
|
|
|
|
if expired_keys:
|
|
LOG.debug(f"Cleaned up {len(expired_keys)} expired cache entries.")
|