Compare commits

...

5 Commits

Author SHA1 Message Date
cf5a06af11 chore: bump version 2026-05-21 21:22:08 +02:00
e07f75432e backup 2026-05-21 21:21:13 +02:00
1696d5c65b chore: bump version 2026-05-21 21:04:51 +02:00
c8b386f47a chore: bump version 2026-05-20 20:00:45 +02:00
3888da352a feat(tmdb): improve rate limiting and retry resilience
- Increase max_retries from 3 to 5 with exponential backoff capped at 30s
- Add per-second rate limiter (~35 req/s) to stay under TMDB's ~40/s limit
- Replace small semaphore (4) with larger one (30) + token-bucket throttle
- Abort retries immediately on DNS/name-resolution failures
- Increase rate-limit fallback wait from default to max(delay*2, 10)s
2026-05-20 20:00:11 +02:00
5 changed files with 89 additions and 53 deletions

View File

@@ -1 +1 @@
v1.1.7 v1.1.11

View File

@@ -191,6 +191,9 @@ start_vpn() {
> /etc/resolv.conf > /etc/resolv.conf
for dns in $(echo "$VPN_DNS" | tr ',' ' '); do for dns in $(echo "$VPN_DNS" | tr ',' ' '); do
echo "nameserver $dns" >> /etc/resolv.conf echo "nameserver $dns" >> /etc/resolv.conf
# Add explicit route to DNS server through wg0 so it's found in main table
# (suppress_prefixlength 0 ignores default routes but allows host routes)
ip -4 route add "$dns" dev "$INTERFACE" 2>/dev/null || true
done done
echo "[vpn] DNS set to: ${VPN_DNS}" echo "[vpn] DNS set to: ${VPN_DNS}"
fi fi
@@ -349,6 +352,8 @@ check_vpn_connectivity() {
echo "[check] FAIL DNS resolution failed" echo "[check] FAIL DNS resolution failed"
echo "[check] resolv.conf: $(tr '\n' ' ' < /etc/resolv.conf)" echo "[check] resolv.conf: $(tr '\n' ' ' < /etc/resolv.conf)"
echo "[check] Check that DNS servers are reachable through wg0" echo "[check] Check that DNS servers are reachable through wg0"
echo "[check] ── End of checks ──"
exit 1
fi fi
echo "[check] ── End of checks ──" echo "[check] ── End of checks ──"

View File

@@ -1,7 +1,8 @@
[Interface] [Interface]
PrivateKey = EPRa2f/v72LvIXAY4yqIRJifsSb+nCcYHqC2rwA94UI= PrivateKey = EPRa2f/v72LvIXAY4yqIRJifsSb+nCcYHqC2rwA94UI=
Address = 100.64.244.78/32 Address = 100.64.244.78/32
DNS = 198.18.0.1,198.18.0.2 #DNS = 198.18.0.1,198.18.0.2
DNS = 8.8.8.8
# Route zum VPN-Server direkt über dein lokales Netz # Route zum VPN-Server direkt über dein lokales Netz
PostUp = ip route add 91.148.236.64 via 192.168.178.1 dev wlp4s0f0 PostUp = ip route add 91.148.236.64 via 192.168.178.1 dev wlp4s0f0

View File

@@ -1,6 +1,6 @@
{ {
"name": "aniworld-web", "name": "aniworld-web",
"version": "1.1.7", "version": "1.1.11",
"description": "Aniworld Anime Download Manager - Web Frontend", "description": "Aniworld Anime Download Manager - Web Frontend",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -12,6 +12,7 @@ Example:
import asyncio import asyncio
import logging import logging
import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -63,6 +64,11 @@ class TMDBClient:
self.max_connections = max_connections self.max_connections = max_connections
self.session: Optional[aiohttp.ClientSession] = None self.session: Optional[aiohttp.ClientSession] = None
self._cache: Dict[str, Any] = {} self._cache: Dict[str, Any] = {}
# TMDB allows ~40 req/s; use 30 concurrent + per-second throttle to stay safe
self._semaphore = asyncio.Semaphore(30)
self._rate_limit_lock = asyncio.Lock()
self._request_timestamps: List[float] = []
self._max_requests_per_second = 35 # Stay under TMDB's ~40/s limit
async def __aenter__(self): async def __aenter__(self):
"""Async context manager entry.""" """Async context manager entry."""
@@ -83,7 +89,7 @@ class TMDBClient:
self, self,
endpoint: str, endpoint: str,
params: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None,
max_retries: int = 3 max_retries: int = 5
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make an async request to TMDB API with retries. """Make an async request to TMDB API with retries.
@@ -110,9 +116,24 @@ class TMDBClient:
logger.debug("Cache hit for %s", endpoint) logger.debug("Cache hit for %s", endpoint)
return self._cache[cache_key] return self._cache[cache_key]
delay = 1 delay = 2
last_error = None last_error = None
# Rate limiting: ensure we don't exceed ~35 requests/second
async with self._rate_limit_lock:
now = time.monotonic()
# Remove timestamps older than 1 second
self._request_timestamps = [
ts for ts in self._request_timestamps if now - ts < 1.0
]
if len(self._request_timestamps) >= self._max_requests_per_second:
sleep_time = 1.0 - (now - self._request_timestamps[0])
if sleep_time > 0:
logger.debug("Rate throttling: waiting %.2fs", sleep_time)
await asyncio.sleep(sleep_time)
self._request_timestamps.append(time.monotonic())
async with self._semaphore:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
# Re-ensure session before each attempt in case it was closed # Re-ensure session before each attempt in case it was closed
@@ -128,8 +149,8 @@ class TMDBClient:
elif resp.status == 404: elif resp.status == 404:
raise TMDBAPIError(f"Resource not found: {endpoint}") raise TMDBAPIError(f"Resource not found: {endpoint}")
elif resp.status == 429: elif resp.status == 429:
# Rate limit - wait longer # Rate limit - wait longer with exponential backoff
retry_after = int(resp.headers.get('Retry-After', delay * 2)) retry_after = int(resp.headers.get('Retry-After', max(delay * 2, 10)))
logger.warning("Rate limited, waiting %ss", retry_after) logger.warning("Rate limited, waiting %ss", retry_after)
await asyncio.sleep(retry_after) await asyncio.sleep(retry_after)
continue continue
@@ -144,7 +165,7 @@ class TMDBClient:
if attempt < max_retries - 1: if attempt < max_retries - 1:
logger.warning("Request timeout (attempt %s), retrying in %ss", attempt + 1, delay) logger.warning("Request timeout (attempt %s), retrying in %ss", attempt + 1, delay)
await asyncio.sleep(delay) await asyncio.sleep(delay)
delay *= 2 delay = min(delay * 2, 30)
else: else:
logger.error("Request timed out after %s attempts", max_retries) logger.error("Request timed out after %s attempts", max_retries)
@@ -156,10 +177,19 @@ class TMDBClient:
self.session = None self.session = None
await self._ensure_session() await self._ensure_session()
# DNS / host-unreachable errors are not transient — abort immediately
error_str = str(e)
if "name resolution" in error_str.lower() or (
isinstance(e, aiohttp.ClientConnectorError) and
"Cannot connect to host" in error_str
):
logger.error("Non-transient connection error, aborting retries: %s", e)
raise TMDBAPIError(f"Request failed after {attempt + 1} attempts: {e}") from e
if attempt < max_retries - 1: if attempt < max_retries - 1:
logger.warning("Request failed (attempt %s): %s, retrying in %ss", attempt + 1, e, delay) logger.warning("Request failed (attempt %s): %s, retrying in %ss", attempt + 1, e, delay)
await asyncio.sleep(delay) await asyncio.sleep(delay)
delay *= 2 delay = min(delay * 2, 30)
else: else:
logger.error("Request failed after %s attempts: %s", max_retries, e) logger.error("Request failed after %s attempts: %s", max_retries, e)