WebApp changes
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
mangabaka_rate_limit.py
|
||||
=======================
|
||||
|
||||
Process-wide rate limiter for the MangaBaka API.
|
||||
|
||||
Apply via:
|
||||
|
||||
from MangaBakaRateLimit import apply_to_session
|
||||
apply_to_session(session)
|
||||
|
||||
This mounts a custom ``requests.adapters.HTTPAdapter`` on the given
|
||||
``requests.Session`` for the ``api.mangabaka.dev`` host. Every request
|
||||
going through that adapter is:
|
||||
|
||||
* throttled so that no two requests are dispatched within
|
||||
``_MIN_INTERVAL`` seconds of one another, and
|
||||
* retried on HTTP 429, honouring the ``Retry-After`` header when
|
||||
present, otherwise exponential backoff capped at ``_MAX_BACKOFF``.
|
||||
|
||||
Throttle state is module-global, so even if several sessions exist in
|
||||
the same process they share one budget — important because they all hit
|
||||
the same upstream IP-based limit.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
|
||||
# Tune these if MangaBaka tightens or loosens limits.
|
||||
_MIN_INTERVAL = 1.1 # seconds between consecutive requests
|
||||
_MAX_RETRIES = 6 # retries on 429 before giving up
|
||||
_MAX_BACKOFF = 60.0 # cap on per-attempt backoff sleep
|
||||
|
||||
|
||||
# --- shared throttle state --------------------------------------------------
|
||||
_state_lock = threading.Lock()
|
||||
_last_request_time = 0.0
|
||||
|
||||
|
||||
def _wait_for_slot() -> None:
|
||||
"""Block until the next request slot is available, then reserve it."""
|
||||
global _last_request_time
|
||||
while True:
|
||||
with _state_lock:
|
||||
now = time.monotonic()
|
||||
wait = _MIN_INTERVAL - (now - _last_request_time)
|
||||
if wait <= 0:
|
||||
_last_request_time = now
|
||||
return
|
||||
time.sleep(wait)
|
||||
|
||||
|
||||
class _MangaBakaRateLimitAdapter(HTTPAdapter):
|
||||
def send(self, request, **kwargs):
|
||||
response = None
|
||||
for attempt in range(_MAX_RETRIES + 1):
|
||||
_wait_for_slot()
|
||||
response = super().send(request, **kwargs)
|
||||
if response.status_code != 429:
|
||||
return response
|
||||
|
||||
retry_after = response.headers.get("Retry-After")
|
||||
try:
|
||||
wait = (float(retry_after) if retry_after
|
||||
else min(_MAX_BACKOFF, 2.0 * (2 ** attempt)))
|
||||
except ValueError:
|
||||
wait = min(_MAX_BACKOFF, 2.0 * (2 ** attempt))
|
||||
|
||||
print(f"[MangaBaka] 429 — backing off {wait:.1f}s "
|
||||
f"(attempt {attempt + 1}/{_MAX_RETRIES})",
|
||||
flush=True)
|
||||
response.close()
|
||||
time.sleep(wait)
|
||||
|
||||
# Retries exhausted — let the caller deal with the last 429.
|
||||
return response
|
||||
|
||||
|
||||
def apply_to_session(session) -> None:
|
||||
"""
|
||||
Mount the rate-limit adapter on ``session`` so every MangaBaka call
|
||||
is automatically throttled. Safe to call multiple times (later mounts
|
||||
just replace the earlier adapter for the same prefix).
|
||||
"""
|
||||
adapter = _MangaBakaRateLimitAdapter()
|
||||
session.mount("https://api.mangabaka.dev/", adapter)
|
||||
session.mount("http://api.mangabaka.dev/", adapter)
|
||||
Reference in New Issue
Block a user