Files
rengine/api/app/core/throttle.py
T
Yogesh OjhaandClaude Sonnet 5 1b44da479f refactor(redis): one connection per process, shared across packages
mcp/, toolbox/ and the api's rate limiter each held their own lazy
Redis client, three copies of the same lru-singleton pattern reaching
into api.core.ratelimit from worker-side code that has no FastAPI app.
shared/redis.py is the one place that builds the async and sync
clients; ratelimit, throttle, mcp/limits and toolbox/store now import
from it. pyproject.toml's isort first-party list gains the packages
that have shipped since it was last written (channels, connectors,
interest, mcp, reports, stages, toolbox).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
2026-09-16 08:03:19 +05:30

46 lines
1.5 KiB
Python

import logging
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from app.config import settings
from app.core.client_ip import client_id
from shared.redis import async_client
logger = logging.getLogger(__name__)
_EXEMPT_PREFIXES = ("/api/v1/events", "/api/v1/media")
class GlobalRateLimitMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
limit = settings.GLOBAL_RATE_LIMIT_PER_MINUTE
path = request.url.path
if (
limit <= 0
or request.method == "OPTIONS"
or any(path.startswith(p) for p in _EXEMPT_PREFIXES)
):
return await call_next(request)
bucket = int(time.time() // 60)
key = f"throttle:{client_id(request)}:{bucket}"
try:
pipe = async_client().pipeline(transaction=True)
pipe.incr(key)
pipe.expire(key, 60, nx=True)
count, _ = await pipe.execute()
except Exception as exc:
logger.warning("global rate limiter unavailable: %s", exc)
return await call_next(request)
if count > limit:
return JSONResponse(
status_code=429,
content={"detail": "Rate limit exceeded. Try again in a minute."},
headers={"Retry-After": str(60 - int(time.time() % 60))},
)
return await call_next(request)