fix(stress): unblock Locust runs from login rate-limit self-DoS
Locust spawns N virtual users (default 1000), all from 127.0.0.1 as admin. /auth/login is rate-limited 10/5min per-IP AND per-username, so the 11th on_start() got 429 and a RuntimeError. A @task(2) login in the task weights turned the whole run into a 429 factory even after ramp-up. And _login_with_retry treated 429 as non-retryable, so there was no graceful degradation path. Three changes, one root cause: - decnet/web/limiter.py: read DECNET_LIMITER_ENABLED (default true). When false, slowapi's Limiter(enabled=False) makes @limiter.limit a no-op. Default ships unchanged; nobody should ever release with this off. - tests/stress/conftest.py: set DECNET_LIMITER_ENABLED=false in the uvicorn subprocess env. Stress tests measure throughput, not rate limiting. - tests/stress/locustfile.py: drop the @task(2) login — it added zero coverage (every user already logs in at on_start) and only generated contention. Teach _login_with_retry to honour 429 + Retry-After so a Locust pointed at a limiter-enabled server degrades gracefully instead of crashing on_start.
This commit is contained in:
@@ -20,6 +20,7 @@ we introduce a verified-proxy config.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Awaitable, Callable
|
||||
|
||||
from fastapi import Request
|
||||
@@ -27,6 +28,19 @@ from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
|
||||
def _limiter_enabled() -> bool:
|
||||
"""``DECNET_LIMITER_ENABLED=false`` disables the limiter process-wide.
|
||||
|
||||
Intended for stress / load testing, where a single Locust host
|
||||
represents thousands of virtual users but shares one source IP and
|
||||
one admin username — the real-world limits (10/5min per IP, per
|
||||
user) would otherwise cap every run at 10 successful logins. The
|
||||
default is ``true``; nobody should ever ship a release with this
|
||||
off.
|
||||
"""
|
||||
return os.environ.get("DECNET_LIMITER_ENABLED", "true").lower() != "false"
|
||||
|
||||
|
||||
# Single process-wide limiter. Importing modules pull this instance to
|
||||
# apply `@limiter.limit(...)` decorators on their routes. Default
|
||||
# headers off: FastAPI response_model handlers return dicts, not
|
||||
@@ -36,6 +50,7 @@ from slowapi.util import get_remote_address
|
||||
limiter: Limiter = Limiter(
|
||||
key_func=get_remote_address,
|
||||
storage_uri="memory://",
|
||||
enabled=_limiter_enabled(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user