token_bucket.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. # -*- coding: utf-8 -*-
  2. """
  3. Token-Bucket-Algorithmus für Rate-Limiting.
  4. Implementiert den klassischen Token-Bucket-Algorithmus für
  5. gleichmäßige Durchsatzbegrenzung mit Burst-Unterstützung.
  6. """
  7. from __future__ import annotations
  8. import threading
  9. import time
  10. from dataclasses import dataclass, field
  11. from datetime import datetime
  12. from typing import Any
  13. @dataclass
  14. class TokenBucketStats:
  15. """
  16. Statistiken für einen Token-Bucket.
  17. Bietet Einblick in die Nutzung und Auslastung des Buckets.
  18. """
  19. requests_allowed: int = 0
  20. """Anzahl erlaubter Requests."""
  21. requests_denied: int = 0
  22. """Anzahl abgelehnter Requests."""
  23. total_tokens_consumed: float = 0.0
  24. """Gesamtzahl verbrauchter Tokens."""
  25. last_request: datetime | None = None
  26. """Zeitpunkt des letzten Requests."""
  27. @property
  28. def total_requests(self) -> int:
  29. """Gesamtzahl der Requests."""
  30. return self.requests_allowed + self.requests_denied
  31. @property
  32. def allow_rate(self) -> float:
  33. """Erlaubnisrate in Prozent."""
  34. if self.total_requests == 0:
  35. return 100.0
  36. return (self.requests_allowed / self.total_requests) * 100
  37. def record_allow(self, tokens: float = 1.0) -> None:
  38. """Zeichnet einen erlaubten Request auf."""
  39. self.requests_allowed += 1
  40. self.total_tokens_consumed += tokens
  41. self.last_request = datetime.now()
  42. def record_deny(self) -> None:
  43. """Zeichnet einen abgelehnten Request auf."""
  44. self.requests_denied += 1
  45. self.last_request = datetime.now()
  46. def reset(self) -> None:
  47. """Setzt alle Statistiken zurück."""
  48. self.requests_allowed = 0
  49. self.requests_denied = 0
  50. self.total_tokens_consumed = 0.0
  51. self.last_request = None
  52. def to_dict(self) -> dict[str, Any]:
  53. """Konvertiert die Statistiken in ein Dictionary."""
  54. return {
  55. "requests_allowed": self.requests_allowed,
  56. "requests_denied": self.requests_denied,
  57. "total_requests": self.total_requests,
  58. "allow_rate": self.allow_rate,
  59. "total_tokens_consumed": self.total_tokens_consumed,
  60. "last_request": (
  61. self.last_request.isoformat()
  62. if self.last_request else None
  63. ),
  64. }
  65. class TokenBucket:
  66. """
  67. Token-Bucket-Implementierung für Rate-Limiting.
  68. Der Bucket füllt sich kontinuierlich mit Tokens bis zur
  69. maximalen Kapazität. Jeder Request verbraucht Tokens.
  70. Wenn nicht genügend Tokens verfügbar sind, wird der
  71. Request abgelehnt.
  72. Attributes:
  73. capacity: Maximale Anzahl von Tokens im Bucket.
  74. refill_rate: Tokens pro Sekunde, die hinzugefügt werden.
  75. tokens: Aktuelle Anzahl von Tokens.
  76. Example:
  77. bucket = TokenBucket(capacity=10, refill_rate=2.0)
  78. if bucket.consume():
  79. # Request erlaubt
  80. process_request()
  81. else:
  82. # Rate limit erreicht
  83. raise TooManyRequests()
  84. """
  85. def __init__(
  86. self,
  87. capacity: float,
  88. refill_rate: float,
  89. initial_tokens: float | None = None,
  90. ) -> None:
  91. """
  92. Initialisiert den Token-Bucket.
  93. Args:
  94. capacity: Maximale Bucket-Kapazität.
  95. refill_rate: Tokens pro Sekunde.
  96. initial_tokens: Anfängliche Token-Anzahl (Default: capacity).
  97. """
  98. self._capacity = capacity
  99. self._refill_rate = refill_rate
  100. self._tokens = initial_tokens if initial_tokens is not None else capacity
  101. self._last_refill = time.monotonic()
  102. self._lock = threading.Lock()
  103. self._stats = TokenBucketStats()
  104. @property
  105. def capacity(self) -> float:
  106. """Gibt die Bucket-Kapazität zurück."""
  107. return self._capacity
  108. @property
  109. def refill_rate(self) -> float:
  110. """Gibt die Refill-Rate zurück."""
  111. return self._refill_rate
  112. @property
  113. def tokens(self) -> float:
  114. """Gibt die aktuelle Token-Anzahl zurück (nach Refill)."""
  115. with self._lock:
  116. self._refill()
  117. return self._tokens
  118. @property
  119. def stats(self) -> TokenBucketStats:
  120. """Gibt die Statistiken zurück."""
  121. return self._stats
  122. def _refill(self) -> None:
  123. """Füllt den Bucket basierend auf verstrichener Zeit."""
  124. now = time.monotonic()
  125. elapsed = now - self._last_refill
  126. if elapsed > 0:
  127. new_tokens = elapsed * self._refill_rate
  128. self._tokens = min(self._capacity, self._tokens + new_tokens)
  129. self._last_refill = now
  130. def consume(self, tokens: float = 1.0) -> bool:
  131. """
  132. Versucht, Tokens zu verbrauchen.
  133. Args:
  134. tokens: Anzahl zu verbrauchender Tokens.
  135. Returns:
  136. True wenn erfolgreich, False wenn nicht genug Tokens.
  137. """
  138. with self._lock:
  139. self._refill()
  140. if self._tokens >= tokens:
  141. self._tokens -= tokens
  142. self._stats.record_allow(tokens)
  143. return True
  144. self._stats.record_deny()
  145. return False
  146. def try_consume(self, tokens: float = 1.0) -> tuple[bool, float]:
  147. """
  148. Versucht, Tokens zu verbrauchen und gibt Wartezeit zurück.
  149. Args:
  150. tokens: Anzahl zu verbrauchender Tokens.
  151. Returns:
  152. Tuple (erfolgreich, Wartezeit in Sekunden).
  153. """
  154. with self._lock:
  155. self._refill()
  156. if self._tokens >= tokens:
  157. self._tokens -= tokens
  158. self._stats.record_allow(tokens)
  159. return True, 0.0
  160. # Berechne, wie lange auf genug Tokens gewartet werden muss
  161. needed = tokens - self._tokens
  162. wait_time = needed / self._refill_rate
  163. self._stats.record_deny()
  164. return False, wait_time
  165. def wait_and_consume(
  166. self,
  167. tokens: float = 1.0,
  168. max_wait: float | None = None,
  169. ) -> bool:
  170. """
  171. Wartet bis Tokens verfügbar sind und verbraucht sie.
  172. Args:
  173. tokens: Anzahl zu verbrauchender Tokens.
  174. max_wait: Maximale Wartezeit in Sekunden.
  175. Returns:
  176. True wenn erfolgreich, False wenn Timeout.
  177. """
  178. start = time.monotonic()
  179. while True:
  180. success, wait_time = self.try_consume(tokens)
  181. if success:
  182. return True
  183. # Timeout-Prüfung
  184. if max_wait is not None:
  185. elapsed = time.monotonic() - start
  186. remaining = max_wait - elapsed
  187. if remaining <= 0:
  188. return False
  189. wait_time = min(wait_time, remaining)
  190. time.sleep(wait_time)
  191. def reset(self) -> None:
  192. """Setzt den Bucket auf volle Kapazität zurück."""
  193. with self._lock:
  194. self._tokens = self._capacity
  195. self._last_refill = time.monotonic()
  196. def drain(self) -> float:
  197. """
  198. Leert den Bucket und gibt verbleibende Tokens zurück.
  199. Returns:
  200. Anzahl der Tokens, die im Bucket waren.
  201. """
  202. with self._lock:
  203. self._refill()
  204. tokens = self._tokens
  205. self._tokens = 0
  206. return tokens
  207. def __repr__(self) -> str:
  208. """String-Repräsentation."""
  209. return (
  210. f"TokenBucket(capacity={self._capacity}, "
  211. f"refill_rate={self._refill_rate}, "
  212. f"tokens={self.tokens:.2f})"
  213. )
  214. class TokenBucketLimiter:
  215. """
  216. Rate-Limiter basierend auf Token-Buckets.
  217. Verwaltet mehrere Buckets für verschiedene Schlüssel
  218. (z.B. User-IDs, IP-Adressen).
  219. """
  220. def __init__(
  221. self,
  222. capacity: float,
  223. refill_rate: float,
  224. key_ttl: float = 3600.0,
  225. ) -> None:
  226. """
  227. Initialisiert den Limiter.
  228. Args:
  229. capacity: Bucket-Kapazität pro Schlüssel.
  230. refill_rate: Tokens pro Sekunde pro Schlüssel.
  231. key_ttl: Zeit in Sekunden, bevor ungenutzte Buckets gelöscht werden.
  232. """
  233. self._capacity = capacity
  234. self._refill_rate = refill_rate
  235. self._key_ttl = key_ttl
  236. self._buckets: dict[str, tuple[TokenBucket, float]] = {}
  237. self._lock = threading.Lock()
  238. def _get_bucket(self, key: str) -> TokenBucket:
  239. """Holt oder erstellt einen Bucket für einen Schlüssel."""
  240. now = time.monotonic()
  241. with self._lock:
  242. if key in self._buckets:
  243. bucket, _ = self._buckets[key]
  244. self._buckets[key] = (bucket, now)
  245. return bucket
  246. bucket = TokenBucket(
  247. capacity=self._capacity,
  248. refill_rate=self._refill_rate,
  249. )
  250. self._buckets[key] = (bucket, now)
  251. return bucket
  252. def is_allowed(self, key: str, tokens: float = 1.0) -> bool:
  253. """
  254. Prüft, ob ein Request erlaubt ist.
  255. Args:
  256. key: Identifikator für den Rate-Limit-Kontext.
  257. tokens: Anzahl zu verbrauchender Tokens.
  258. Returns:
  259. True wenn erlaubt.
  260. """
  261. bucket = self._get_bucket(key)
  262. return bucket.consume(tokens)
  263. def get_wait_time(self, key: str, tokens: float = 1.0) -> float:
  264. """
  265. Gibt die Wartezeit bis zur Erlaubnis zurück.
  266. Args:
  267. key: Identifikator.
  268. tokens: Anzahl benötigter Tokens.
  269. Returns:
  270. Wartezeit in Sekunden (0 wenn sofort erlaubt).
  271. """
  272. bucket = self._get_bucket(key)
  273. success, wait_time = bucket.try_consume(tokens)
  274. return 0.0 if success else wait_time
  275. def wait_and_proceed(
  276. self,
  277. key: str,
  278. tokens: float = 1.0,
  279. max_wait: float | None = None,
  280. ) -> bool:
  281. """
  282. Wartet und verbraucht dann Tokens.
  283. Args:
  284. key: Identifikator.
  285. tokens: Anzahl zu verbrauchender Tokens.
  286. max_wait: Maximale Wartezeit.
  287. Returns:
  288. True wenn erfolgreich.
  289. """
  290. bucket = self._get_bucket(key)
  291. return bucket.wait_and_consume(tokens, max_wait)
  292. def cleanup(self) -> int:
  293. """
  294. Entfernt abgelaufene Buckets.
  295. Returns:
  296. Anzahl der entfernten Buckets.
  297. """
  298. now = time.monotonic()
  299. expired_keys = []
  300. with self._lock:
  301. for key, (_, last_access) in self._buckets.items():
  302. if now - last_access > self._key_ttl:
  303. expired_keys.append(key)
  304. for key in expired_keys:
  305. del self._buckets[key]
  306. return len(expired_keys)
  307. def reset(self, key: str | None = None) -> None:
  308. """
  309. Setzt einen oder alle Buckets zurück.
  310. Args:
  311. key: Optionaler spezifischer Schlüssel.
  312. """
  313. with self._lock:
  314. if key:
  315. if key in self._buckets:
  316. bucket, last_access = self._buckets[key]
  317. bucket.reset()
  318. else:
  319. for bucket, _ in self._buckets.values():
  320. bucket.reset()
  321. def get_stats(self, key: str) -> TokenBucketStats | None:
  322. """
  323. Gibt Statistiken für einen Schlüssel zurück.
  324. Args:
  325. key: Identifikator.
  326. Returns:
  327. Statistiken oder None.
  328. """
  329. with self._lock:
  330. if key in self._buckets:
  331. bucket, _ = self._buckets[key]
  332. return bucket.stats
  333. return None
  334. @property
  335. def bucket_count(self) -> int:
  336. """Gibt die Anzahl aktiver Buckets zurück."""
  337. with self._lock:
  338. return len(self._buckets)