rate_limiter.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import time
  2. from collections import defaultdict
  3. from threading import Lock
  4. class SimpleRateLimiter:
  5. """
  6. Simple in-memory rate limiter using sliding window.
  7. For production, consider using Redis-based rate limiting.
  8. """
  9. def __init__(self, max_requests=60, window_seconds=60, max_in_flight=2):
  10. self.max_requests = int(max_requests)
  11. self.window_seconds = int(window_seconds)
  12. self.max_in_flight = int(max_in_flight)
  13. self._requests = defaultdict(list)
  14. self._in_flight = defaultdict(int)
  15. self._lock = Lock()
  16. def is_allowed(self, key):
  17. """
  18. Check if the key is allowed to make a request.
  19. Args:
  20. key: Identifier (IP address, session_id, etc.)
  21. Returns:
  22. bool: True if allowed, False if rate limit exceeded
  23. """
  24. now = time.time()
  25. window_start = now - self.window_seconds
  26. with self._lock:
  27. if self.max_requests <= 0:
  28. return True
  29. # Clean old requests
  30. requests = self._requests[key]
  31. self._requests[key] = [ts for ts in requests if ts > window_start]
  32. # Check limit
  33. if len(self._requests[key]) >= self.max_requests:
  34. return False
  35. # Record this request
  36. self._requests[key].append(now)
  37. return True
  38. def try_acquire(self, key):
  39. """Acquire one reusable in-flight slot for a session/tool key."""
  40. with self._lock:
  41. if self.max_in_flight <= 0:
  42. return True
  43. if self._in_flight[key] >= self.max_in_flight:
  44. return False
  45. self._in_flight[key] += 1
  46. return True
  47. def release(self, key):
  48. """Release a previously acquired slot; extra releases are harmless."""
  49. with self._lock:
  50. if self.max_in_flight <= 0 or key not in self._in_flight:
  51. return
  52. self._in_flight[key] -= 1
  53. if self._in_flight[key] <= 0:
  54. del self._in_flight[key]
  55. def cleanup(self, max_age_seconds=3600):
  56. """
  57. Remove old entries to prevent memory leak.
  58. Call this periodically in a background thread.
  59. """
  60. now = time.time()
  61. cutoff = now - max_age_seconds
  62. with self._lock:
  63. keys_to_delete = []
  64. for key, requests in self._requests.items():
  65. if not requests or requests[-1] < cutoff:
  66. keys_to_delete.append(key)
  67. for key in keys_to_delete:
  68. del self._requests[key]