AO3 Mirror v4 — initial commit
Cookie-aware proxy pool with CF challenge solving - Tiered proxy pool (fast 50 + main 676) - Per-proxy cf_clearance cookie persistence - CF challenge detection + user-browser solving - Safari + Chrome TLS fingerprint rotation - Async FastAPI backend with LRU cache - Passive daemon with systemd supervision - Stats dashboard + Prometheus metrics
This commit is contained in:
123
cache.py
Normal file
123
cache.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
简单高效的缓存层
|
||||
- 内存 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
|
||||
Reference in New Issue
Block a user