from datetime import datetime, UTC from typing import Optional import redis.exceptions import werkzeug.exceptions from limits.storage import RedisStorage from app import metrics from app.log import LOG lock_redis: Optional[RedisStorage] = None rateLimitsEnabled: bool = True def set_redis_concurrent_lock(redis: RedisStorage): global lock_redis lock_redis = redis def set_rate_limit_enabled(enabled: bool): global rateLimitsEnabled rateLimitsEnabled = enabled def check_bucket_limit( lock_name: Optional[str] = None, max_hits: int = 5, bucket_seconds: int = 3600, ): if not rateLimitsEnabled: return # Calculate current bucket time int_time = int(datetime.now(UTC).timestamp()) bucket_id = int_time - (int_time % bucket_seconds) bucket_lock_name = f"bl:{lock_name}:{bucket_id}" if not lock_redis: return try: value = lock_redis.incr(bucket_lock_name, bucket_seconds) if value > max_hits: LOG.i( f"Rate limit hit for {lock_name} (bucket id {bucket_id}) -> {value}/{max_hits}" ) metrics.record_event( "BucketRateLimit", # Keys look like "alias_create_7:": keep the limit name # only, a per user value would create one metric series per user. { "lock_name": (lock_name or "").split(":", 1)[0], "bucket_seconds": bucket_seconds, }, ) raise werkzeug.exceptions.TooManyRequests() except (redis.exceptions.RedisError, AttributeError): LOG.e("Cannot connect to redis")