perf: scale workers + per-tablet rate limiting for 20 concurrent users
The default 2-worker gunicorn could only serve 2 concurrent tablet requests, queueing the rest, and the rate limiter saw every tablet as the same Nginx container IP, so 20 users would have collectively burned through the 100 req/min general bucket. - gunicorn: 5 workers x 4 gthread, --forwarded-allow-ips=*, access log - uvicorn: 4 workers, --proxy-headers, --forwarded-allow-ips=* - RateLimitMiddleware: resolve real client IP from X-Forwarded-For -> X-Real-IP -> request.client.host - Bump rate_limit_general 100 -> 300 req/min/IP (per tablet now) - Flask: ProxyFix(x_for=1, x_proto=1, x_host=1) so request.remote_addr is the tablet IP, not the Nginx IP - APIClient: forward X-Forwarded-For + X-Real-IP to FastAPI for both JSON and multipart/files calls; safe no-op outside request context - 12 new tests (7 server + 5 client) covering header precedence, forwarding behavior and ProxyFix install Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from flask import session
|
||||
from flask import has_request_context, request, session
|
||||
|
||||
from config import Config
|
||||
|
||||
@@ -14,13 +14,29 @@ class APIClient:
|
||||
self.base_url = Config.API_SERVER_URL.rstrip("/")
|
||||
self.timeout = 30
|
||||
|
||||
@staticmethod
|
||||
def _real_client_ip() -> str | None:
|
||||
"""Best-effort tablet IP for downstream rate limiting.
|
||||
|
||||
Behind Nginx + ProxyFix, ``request.remote_addr`` already resolves to
|
||||
the tablet IP. Outside a request context (background tasks, tests),
|
||||
return None so we don't forge a bogus header.
|
||||
"""
|
||||
if not has_request_context():
|
||||
return None
|
||||
return request.remote_addr
|
||||
|
||||
@property
|
||||
def _headers(self) -> dict[str, str]:
|
||||
"""Build request headers with API key from session."""
|
||||
"""Build request headers with API key + real client IP from session."""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
api_key = session.get("api_key")
|
||||
if api_key:
|
||||
headers["X-API-Key"] = api_key
|
||||
client_ip = self._real_client_ip()
|
||||
if client_ip:
|
||||
headers["X-Forwarded-For"] = client_ip
|
||||
headers["X-Real-IP"] = client_ip
|
||||
return headers
|
||||
|
||||
def _handle_response(self, response: requests.Response) -> dict[str, Any]:
|
||||
@@ -76,6 +92,10 @@ class APIClient:
|
||||
try:
|
||||
if files:
|
||||
headers = {"X-API-Key": session.get("api_key", "")}
|
||||
client_ip = self._real_client_ip()
|
||||
if client_ip:
|
||||
headers["X-Forwarded-For"] = client_ip
|
||||
headers["X-Real-IP"] = client_ip
|
||||
response = requests.post(
|
||||
f"{self.base_url}{endpoint}",
|
||||
headers=headers,
|
||||
|
||||
Reference in New Issue
Block a user