""" 简单高效的缓存层 - 内存 LRU 缓存,每个 worker 独立 - 短 TTL 避免内容过时 - 针对不同路径设置不同 TTL """ import hashlib import threading import time from collections import OrderedDict from typing import Optional class LRUCache: """Thread-safe LRU cache with TTL support.""" def __init__(self, capacity: int = 2000, default_ttl: int = 30): self.capacity = capacity self.default_ttl = default_ttl self._cache: OrderedDict[str, tuple[float, bytes, dict, int]] = OrderedDict() # (expiry_time, body, headers, status) self._lock = threading.RLock() def _make_key(self, url: str, headers: Optional[dict] = None) -> str: """Generate cache key from URL and key headers.""" # Use URL + relevant headers accept = "" if headers: accept = headers.get("Accept-Encoding", "") raw = f"{url}|{accept}" return hashlib.md5(raw.encode()).hexdigest() def get(self, url: str, headers: Optional[dict] = None) -> Optional[tuple[bytes, dict, int]]: """Get cached response. Returns (body, headers, status) or None.""" key = self._make_key(url, headers) with self._lock: if key not in self._cache: return None expiry, body, resp_headers, status = self._cache[key] if time.time() > expiry: del self._cache[key] return None # Move to end (most recently used) self._cache.move_to_end(key) return (body, resp_headers, status) def set(self, url: str, body: bytes, headers: dict, status: int, ttl: Optional[int] = None, request_headers: Optional[dict] = None): """Store response in cache.""" key = self._make_key(url, request_headers) t = ttl if ttl is not None else self.default_ttl expiry = time.time() + t with self._lock: self._cache[key] = (expiry, body, headers, status) self._cache.move_to_end(key) if len(self._cache) > self.capacity: self._cache.popitem(last=False) def invalidate(self, url: str, headers: Optional[dict] = None): """Remove a specific URL from cache.""" key = self._make_key(url, headers) with self._lock: self._cache.pop(key, None) def clear(self): with self._lock: self._cache.clear() @property def size(self) -> int: with self._lock: return len(self._cache) def get_stats(self) -> dict: with self._lock: return { "size": len(self._cache), "capacity": self.capacity, "usage_pct": round(len(self._cache) / self.capacity * 100, 1) if self.capacity else 0, } # TTL 策略:不同路径不同缓存时间 PATH_TTL = { "/": 30, # 首页 30s "/works": 60, # 作品列表 60s "/chapters": 120, # 章节内容 120s "/series": 60, # 系列 60s "/collections": 60, "/tags": 60, "/users": 30, "/pseuds": 30, "/bookmarks": 60, "/skins": 300, # CSS 皮肤缓存 5 分钟 "/stylesheets": 300, "/images": 600, # 图片缓存 10 分钟 "/media": 600, "/javascripts": 300, "/api": 15, # API 响应 15s "/external_links": 30, # Default: 30s } def get_ttl_for_path(path: str) -> int: """Determine cache TTL based on URL path.""" for prefix, ttl in PATH_TTL.items(): if path.startswith(prefix): return ttl return 30 # default # 全局缓存实例 _cache: Optional[LRUCache] = None def get_cache() -> LRUCache: global _cache if _cache is None: _cache = LRUCache(capacity=5000, default_ttl=30) return _cache