Réponse A: OpenAI GPT-5.6
from future import annotations
import threading
import unittest
from collections import deque
from dataclasses import dataclass, field
from typing import Deque, Dict, Hashable, Optional
@dataclass(frozen=True)
class TenantConfig:
"""Limites de débit pour un locataire.
client_limit s'applique indépendamment à chaque client, tandis que tenant_limit s'applique
à toutes les requêtes admises du locataire. Les deux limites utilisent des fenêtres glissantes exactes.
active_timeout_ms limite la durée pendant laquelle un attenteur d'équité absent peut conserver son tour.
eviction_ms contrôle l...
Afficher la réponse complète ▼
from future import annotations
import threading
import unittest
from collections import deque
from dataclasses import dataclass, field
from typing import Deque, Dict, Hashable, Optional
@dataclass(frozen=True)
class TenantConfig:
"""Limites de débit pour un locataire.
client_limit s'applique indépendamment à chaque client, tandis que tenant_limit s'applique
à toutes les requêtes admises du locataire. Les deux limites utilisent des fenêtres glissantes exactes.
active_timeout_ms limite la durée pendant laquelle un attenteur d'équité absent peut conserver son tour.
eviction_ms contrôle la rétention de l'état du client.
"""
client_limit: int
client_window_ms: int
tenant_limit: int
tenant_window_ms: int
active_timeout_ms: int = 60_000
eviction_ms: int = 120_000
def validate(self) -> None:
if self.client_limit <= 0 or self.tenant_limit <= 0:
raise ValueError("les limites doivent être positives")
if self.client_window_ms <= 0 or self.tenant_window_ms <= 0:
raise ValueError("les longueurs de fenêtre doivent être positives")
if self.active_timeout_ms <= 0:
raise ValueError("active_timeout_ms doit être positif")
if self.eviction_ms < max(self.client_window_ms, self.tenant_window_ms):
raise ValueError("eviction_ms doit être au moins aussi grand que les deux fenêtres")
if self.eviction_ms < self.active_timeout_ms:
raise ValueError("eviction_ms doit être au moins active_timeout_ms")
@dataclass(frozen=True)
class AllowResult:
allowed: bool
retry_after_ms: int = 0
@dataclass
class _ClientState:
admitted: Deque[int] = field(default_factory=deque)
last_seen_ms: int = 0
@dataclass
class _TenantState:
admitted: Deque[int] = field(default_factory=deque)
clients: Dict[Hashable, _ClientState] = field(default_factory=dict)
waiters: Deque[Hashable] = field(default_factory=deque)
waiter_set: set[Hashable] = field(default_factory=set)
last_now_ms: Optional[int] = None
last_seen_ms: int = 0
class SlidingWindowRateLimiter:
"""Limiteur de débit à fenêtre glissante, thread-safe, mono-processus.
Algorithme de fenêtre glissante :
Chaque horodatage de requête admise est stocké dans une deque. Avant d'évaluer une
requête, les horodatages t satisfaisant t <= now - window sont supprimés. Ainsi une
requête vieille d'une fenêtre complète ne consomme plus de capacité. Il s'agit d'une
véritable fenêtre glissante basée sur les journaux, elle n'a donc pas de rafale de limite de compartiment calendaire.
Le résultat est exact, avec des opérations de deque amorties en O(1) et une mémoire
proportionnelle aux requêtes admises encore dans les fenêtres configurées. C'est
plus précis que les compartiments pondérés mais utilise plus de mémoire. Les limites fournissent une
limite stricte aux entrées d'horodatage actives, et les objets clients obsolètes sont évincés.
Partage équitable :
La capacité de locataire non contestée est économe en travail et peut être utilisée par n'importe quel
client. Une fois le plafond du locataire atteint, les clients refusés entrent dans une file FIFO
avec au plus une entrée par client. Au fur et à mesure que la capacité expire, la tête obtient
l'admission suivante, puis quitte la file. Un client continuellement occupé doit
se réinscrire derrière d'autres prétendants, implémentant une redistribution round-robin
sans réserver de manière permanente des parts inutilisées par client. Les entrées de file inactives
expirent après active_timeout_ms afin qu'un tour abandonné ne puisse pas bloquer
le locataire indéfiniment.
Comportement de l'horloge :
now_ms doit provenir d'une horloge monotone. Si un appelant fournit un horodatage dupliqué
ou décroissant, le temps est limité à l'horodatage le plus élevé observé par le locataire.
Cela empêche l'historique expiré de redevenir actif.
Concurrence et hypothèses de déploiement :
Un seul verrou réentrant rend l'autorisation, la configuration et le nettoyage atomiques
entre les threads et les tâches asynchrones partageant cet objet. Pour une utilisation distribuée,
la même transition d'état peut être implémentée atomiquement dans Redis avec un
script Lua et des ensembles triés, ou dans un magasin fortement cohérent transactionnel.
Les métadonnées de l'attenteur FIFO doivent être mises à jour dans la même transaction que les
journaux de requêtes ; les verrous locaux seuls ne coordonnent pas plusieurs processus.
"""
def __init__(self, default_config: Optional[TenantConfig] = None) -> None:
if default_config is not None:
default_config.validate()
self._default_config = default_config
self._configs: Dict[Hashable, TenantConfig] = {}
self._tenants: Dict[Hashable, _TenantState] = {}
self._lock = threading.RLock()
def configure_tenant(self, tenant_id: Hashable, config: TenantConfig) -> None:
"""Crée ou remplace la configuration d'un locataire de manière atomique."""
config.validate()
with self._lock:
self._configs[tenant_id] = config
def allow(self, tenant_id: Hashable, client_id: Hashable, now_ms: int) -> AllowResult:
"""Décide de manière atomique s'il faut admettre une requête.
retry_after_ms est exact pour un refus direct client/fenêtre ou locataire/fenêtre.
Pour un refus d'équité FIFO, il s'agit du premier délai de nouvelle tentative temporel ;
l'admission réelle dépend également des attenteurs actifs précédents qui prennent ou
abandonnent leurs tours.
"""
if not isinstance(now_ms, int):
raise TypeError("now_ms doit être un entier")
with self._lock:
cfg = self._config_for(tenant_id)
tenant = self._tenants.get(tenant_id)
if tenant is None:
tenant = _TenantState(last_now_ms=now_ms, last_seen_ms=now_ms)
self._tenants[tenant_id] = tenant
now = self._normalize_time(tenant, now_ms)
tenant.last_seen_ms = now
self._prune(tenant.admitted, now, cfg.tenant_window_ms)
client = tenant.clients.get(client_id)
if client is None:
client = _ClientState(last_seen_ms=now)
tenant.clients[client_id] = client
client.last_seen_ms = now
self._prune(client.admitted, now, cfg.client_window_ms)
self._purge_waiters(tenant, cfg, now)
self._evict_clients(tenant, cfg, now, preserve=client_id)
if len(client.admitted) >= cfg.client_limit:
self._remove_waiter(tenant, client_id)
retry = client.admitted[0] + cfg.client_window_ms - now
return AllowResult(False, max(0, retry))
if len(tenant.admitted) >= cfg.tenant_limit:
self._enqueue_waiter(tenant, client_id)
retry = tenant.admitted[0] + cfg.tenant_window_ms - now
return AllowResult(False, max(0, retry))
# La capacité existe. En cas de contention, seule la tête FIFO peut l'utiliser.
if tenant.waiters and tenant.waiters[0] != client_id:
self._enqueue_waiter(tenant, client_id)
return AllowResult(False, 0)
if tenant.waiters and tenant.waiters[0] == client_id:
tenant.waiters.popleft()
tenant.waiter_set.remove(client_id)
client.admitted.append(now)
tenant.admitted.append(now)
return AllowResult(True, 0)
def sweep(self, now_ms: int) -> int:
"""Évince les clients obsolètes et nettoie les états d'exécution des locataires.
Les applications peuvent appeler cela périodiquement lorsque le trafic est faible. Les appels
normaux à allow nettoient également le locataire accédé. Retourne le nombre d'objets clients
et locataires d'exécution supprimés. Les configurations sont conservées.
"""
if not isinstance(now_ms, int):
raise TypeError("now_ms doit être un entier")
removed = 0
with self._lock:
for tenant_id in list(self._tenants):
cfg = self._config_for(tenant_id)
tenant = self._tenants[tenant_id]
now = self._normalize_time(tenant, now_ms)
self._prune(tenant.admitted, now, cfg.tenant_window_ms)
self._purge_waiters(tenant, cfg, now)
before = len(tenant.clients)
self._evict_clients(tenant, cfg, now, preserve=None)
removed += before - len(tenant.clients)
if (not tenant.clients and not tenant.admitted and not tenant.waiters
and now - tenant.last_seen_ms >= cfg.eviction_ms):
del self._tenants[tenant_id]
removed += 1
return removed
def debug_state(self, tenant_id: Hashable) -> dict[str, int]:
"""Retourne les tailles d'état agrégées, utiles pour la surveillance et les tests."""
with self._lock:
tenant = self._tenants.get(tenant_id)
if tenant is None:
return {"clients": 0, "tenant_events": 0, "waiters": 0,
"client_events": 0}
return {
"clients": len(tenant.clients),
"tenant_events": len(tenant.admitted),
"waiters": len(tenant.waiters),
"client_events": sum(len(c.admitted) for c in tenant.clients.values()),
}
def _config_for(self, tenant_id: Hashable) -> TenantConfig:
cfg = self._configs.get(tenant_id, self._default_config)
if cfg is None:
raise KeyError(f"le locataire {tenant_id!r} n'a pas de configuration")
return cfg
@staticmethod
def _normalize_time(tenant: _TenantState, supplied: int) -> int:
if tenant.last_now_ms is None or supplied >= tenant.last_now_ms:
tenant.last_now_ms = supplied
return supplied
return tenant.last_now_ms
@staticmethod
def _prune(events: Deque[int], now: int, window_ms: int) -> None:
boundary = now - window_ms
while events and events[0] <= boundary:
events.popleft()
@staticmethod
def _enqueue_waiter(tenant: _TenantState, client_id: Hashable) -> None:
if client_id not in tenant.waiter_set:
tenant.waiters.append(client_id)
tenant.waiter_set.add(client_id)
@staticmethod
def _remove_waiter(tenant: _TenantState, client_id: Hashable) -> None:
if client_id not in tenant.waiter_set:
return
tenant.waiters = deque(x for x in tenant.waiters if x != client_id)
tenant.waiter_set.remove(client_id)
def _purge_waiters(self, tenant: _TenantState, cfg: TenantConfig, now: int) -> None:
kept: Deque[Hashable] = deque()
kept_set: set[Hashable] = set()
for client_id in tenant.waiters:
client = tenant.clients.get(client_id)
if client is None or now - client.last_seen_ms >= cfg.active_timeout_ms:
continue
self._prune(client.admitted, now, cfg.client_window_ms)
# Un client bloqué par son propre quota ne réserve pas un tour de locataire.
if len(client.admitted) >= cfg.client_limit:
continue
kept.append(client_id)
kept_set.add(client_id)
tenant.waiters = kept
tenant.waiter_set = kept_set
@staticmethod
def _evict_clients(tenant: _TenantState, cfg: TenantConfig, now: int,
preserve: Optional[Hashable]) -> None:
for client_id in list(tenant.clients):
if preserve is not None and client_id == preserve:
continue
client = tenant.clients[client_id]
boundary = now - cfg.client_window_ms
while client.admitted and client.admitted[0] <= boundary:
client.admitted.popleft()
if (not client.admitted and client_id not in tenant.waiter_set
and now - client.last_seen_ms >= cfg.eviction_ms):
del tenant.clients[client_id]
class SlidingWindowRateLimiterTests(unittest.TestCase):
def make_limiter(self, **overrides: int) -> SlidingWindowRateLimiter:
values = dict(client_limit=3, client_window_ms=1000,
tenant_limit=20, tenant_window_ms=1000,
active_timeout_ms=1000, eviction_ms=2000)
values.update(overrides)
return SlidingWindowRateLimiter(TenantConfig(**values))
def test_exact_window_boundary_is_expired(self) -> None:
limiter = self.make_limiter(client_limit=1)
self.assertTrue(limiter.allow("t", "c", 0).allowed)
denied = limiter.allow("t", "c", 999)
self.assertFalse(denied.allowed)
self.assertEqual(denied.retry_after_ms, 1)
self.assertTrue(limiter.allow("t", "c", 1000).allowed)
def test_idle_client_returns_after_full_window(self) -> None:
limiter = self.make_limiter(client_limit=2)
self.assertTrue(limiter.allow("t", "c", 10).allowed)
self.assertTrue(limiter.allow("t", "c", 11).allowed)
self.assertFalse(limiter.allow("t", "c", 12).allowed)
self.assertTrue(limiter.allow("t", "c", 1011).allowed)
def test_duplicate_and_backwards_clock_are_clamped(self) -> None:
limiter = self.make_limiter(client_limit=2)
self.assertTrue(limiter.allow("t", "c", 100).allowed)
self.assertTrue(limiter.allow("t", "c", 100).allowed)
denied = limiter.allow("t", "c", 50)
self.assertFalse(denied.allowed)
self.assertEqual(denied.retry_after_ms, 1000)
self.assertTrue(limiter.allow("t", "c", 1100).allowed)
def test_concurrent_requests_are_atomic(self) -> None:
limiter = self.make_limiter(client_limit=10, tenant_limit=100)
barrier = threading.Barrier(40)
results: list[bool] = []
results_lock = threading.Lock()
def worker() -> None:
barrier.wait()
value = limiter.allow("t", "same-client", 500).allowed
with results_lock:
results.append(value)
threads = [threading.Thread(target=worker) for _ in range(40)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
self.assertEqual(sum(results), 10)
self.assertEqual(len(results), 40)
def test_tenant_cap_combines_all_clients(self) -> None:
limiter = self.make_limiter(client_limit=10, tenant_limit=2)
self.assertTrue(limiter.allow("t", "a", 0).allowed)
self.assertTrue(limiter.allow("t", "b", 1).allowed)
denied = limiter.allow("t", "c", 2)
self.assertFalse(denied.allowed)
self.assertEqual(denied.retry_after_ms, 998)
def test_saturated_tenant_uses_fifo_fair_redistribution(self) -> None:
limiter = self.make_limiter(client_limit=10, tenant_limit=2)
self.assertTrue(limiter.allow("t", "a", 0).allowed)
self.assertTrue(limiter.allow("t", "a", 1).allowed)
self.assertFalse(limiter.allow("t", "b", 2).allowed)
self.assertFalse(limiter.allow("t", "c", 3).allowed)
# Les deux anciens slots expirent, mais a ne peut pas les monopoliser : b et c sont en file d'attente.
self.assertFalse(limiter.allow("t", "a", 1001).allowed)
self.assertTrue(limiter.allow("t", "b", 1001).allowed)
self.assertFalse(limiter.allow("t", "b", 1001).allowed)
self.assertTrue(limiter.allow("t", "c", 1001).allowed)
def test_abandoned_fairness_turn_eventually_expires(self) -> None:
limiter = self.make_limiter(client_limit=10, tenant_limit=1,
active_timeout_ms=1000, eviction_ms=2000)
self.assertTrue(limiter.allow("t", "a", 0).allowed)
self.assertFalse(limiter.allow("t", "gone", 1).allowed)
# À 1001, l'attenteur est inactif, donc un autre client peut utiliser la capacité.
self.assertTrue(limiter.allow("t", "b", 1001).allowed)
def test_eviction_removes_stale_but_not_active_clients(self) -> None:
limiter = self.make_limiter(client_window_ms=100, tenant_window_ms=100,
active_timeout_ms=100, eviction_ms=200)
self.assertTrue(limiter.allow("t", "stale", 0).allowed)
self.assertTrue(limiter.allow("t", "active", 150).allowed)
limiter.sweep(201)
state = limiter.debug_state("t")
self.assertEqual(state["clients"], 1)
self.assertEqual(state["client_events"], 1)
# La requête active n'est pas abandonnée avant l'expiration de sa propre fenêtre.
self.assertFalse(limiter.allow("t", "active", 201).allowed
if limiter._default_config.client_limit == 1 else False)
self.assertEqual(limiter.debug_state("t")["clients"], 1)
limiter.sweep(351)
self.assertEqual(limiter.debug_state("t")["clients"], 0)
def test_configuration_is_per_tenant(self) -> None:
limiter = SlidingWindowRateLimiter()
limiter.configure_tenant("small", TenantConfig(1, 100, 1, 100, 100, 100))
limiter.configure_tenant("large", TenantConfig(2, 100, 3, 100, 100, 100))
self.assertTrue(limiter.allow("small", "c", 0).allowed)
self.assertFalse(limiter.allow("small", "c", 1).allowed)
self.assertTrue(limiter.allow("large", "c", 0).allowed)
self.assertTrue(limiter.allow("large", "c", 1).allowed)
def test_memory_is_bounded_by_live_limits_and_sweep(self) -> None:
limiter = self.make_limiter(client_limit=5, tenant_limit=7,
client_window_ms=100, tenant_window_ms=100,
active_timeout_ms=100, eviction_ms=100)
for index in range(100):
limiter.allow("t", f"c{index}", 0)
state = limiter.debug_state("t")
self.assertLessEqual(state["tenant_events"], 7)
self.assertLessEqual(state["client_events"], 7)
limiter.sweep(100)
self.assertEqual(limiter.debug_state("t")["clients"], 0)
if name == "main":
unittest.main()
Résultat
Votes gagnants
3 / 3
Score moyen
Score total
Commentaire global
La réponse A fournit une implémentation complète et robuste du limiteur de débit, répondant pleinement à toutes les exigences fonctionnelles, y compris la politique complexe de partage équitable pour la saturation des locataires. Le code est bien structuré, utilise des structures de données appropriées et comprend une documentation complète et une suite de tests solide couvrant tous les cas limites spécifiés. Sa gestion de la mémoire via un balayage explicite et une éviction configurable est bien conçue.
Afficher le détail de l’évaluation ▼
Exactitude
Poids 35%La réponse A implémente correctement tous les aspects du limiteur de débit, y compris la politique complexe de partage équitable pour la saturation des locataires et la gestion robuste de l'horloge. Toutes les limites et la logique de fenêtre sont précises.
Complétude
Poids 20%La réponse A est très complète, répondant à toutes les exigences fonctionnelles, y compris le partage équitable nuancé et l'éviction complète de la mémoire. Elle fournit des explications claires, des hypothèses et des notes de conception distribuée comme demandé.
Qualité du code
Poids 20%Le code de la réponse A est bien structuré, utilise des `dataclasses` pour une gestion claire de l'état et possède une bonne documentation en ligne. L'utilisation de `threading.RLock` est appropriée pour les opérations réentrantes. Les méthodes sont logiquement séparées et propres.
Valeur pratique
Poids 15%La réponse A offre une grande valeur pratique grâce à son ensemble complet de fonctionnalités, en particulier le mécanisme de partage équitable et l'éviction contrôlée de la mémoire via la méthode `sweep`. La méthode `debug_state` est également un ajout utile pour la surveillance.
Respect des consignes
Poids 10%La réponse A suit méticuleusement toutes les instructions, y compris la politique complexe de partage équitable, la vraie fenêtre glissante, la sécurité de la concurrence, la limitation de la mémoire et la gestion de tous les cas limites spécifiés dans le code et les tests. Tous les livrables sont fournis.
Score total
Commentaire global
La réponse A fournit une implémentation Python substantiellement complète et exécutable avec des fenêtres glissantes exactes basées sur les journaux, une configuration par locataire, des plafonds par locataire, une protection de la concurrence, un nettoyage de l'état obsolète, des calculs de nouvelle tentative, une gestion explicite du décalage de l'horloge et une large suite de tests unitaires. Son mécanisme d'attente FIFO est une politique de partage équitable crédible en cas de saturation des locataires, bien que retry_after_ms pour les rejets de la file d'attente d'équité puisse être imprécis et que l'état de l'attente/client pour de nombreux clients uniques rejetés puisse encore croître jusqu'à l'expiration ou le balayage. Dans l'ensemble, il correspond étroitement au comportement et aux cas limites de la bibliothèque demandée.
Afficher le détail de l’évaluation ▼
Exactitude
Poids 35%Utilise des journaux d'horodatage exacts avec un élagage correct des limites, applique les plafonds par client et par locataire, borne le temps passé et sérialise les mutations en toute sécurité. La logique d'équité FIFO est globalement correcte, mais retry_after_ms pour les rejets d'équité non principaux peut être de 0 et la mémoire pour de nombreux clients rejetés en attente n'est limitée que dans le temps plutôt qu'immédiatement.
Complétude
Poids 20%Couvre l'API demandée, la configuration réglable par locataire, les fenêtres glissantes exactes, le plafond de locataire, la file d'attente d'équité, la sécurité de la concurrence, l'éviction/balayage, les hypothèses, les notes d'extension distribuée et les tests pour presque tous les cas limites spécifiés. Les lacunes mineures incluent un rapport de nouvelle tentative d'équité imparfait et un code de test d'éviction quelque peu maladroit.
Qualité du code
Poids 20%Dataclasses bien structurées, méthodes publiques claires, validation, documentation en ligne, annotations de type et méthodes d'aide encapsulées. Le verrou global unique est simple et sûr mais limite la scalabilité, et certains éléments internes/tests sont quelque peu maladroits.
Valeur pratique
Poids 15%Pratique en tant que bibliothèque réutilisable pour un seul processus avec état de surveillance/débogage, balayage périodique, locataires configurables et une voie crédible vers Redis/Lua pour une utilisation distribuée. Le verrou global et le coût de la mémoire du journal d'horodatage peuvent limiter les déploiements à grande échelle, mais les compromis sont énoncés.
Respect des consignes
Poids 10%Suit de près les livrables demandés : code complet, algorithme et compromis documentés, hypothèses explicites, extension distribuée et tests pour les cas limites nommés. L'API renvoie un AllowResult au lieu d'un tuple, ce qui est acceptable selon l'invite.
Score total
Commentaire global
La réponse A est une solution quasi complète et de qualité production. Elle implémente une fenêtre glissante exacte basée sur un journal avec une sémantique de limites correcte, un véritable mécanisme de redistribution équitable via une file d'attente FIFO avec des délais d'expiration d'activité, la gestion des horloges en arrière ou dupliquées limitée par le locataire, un seul RLock pour l'atomicité, et une mémoire limitée grâce à l'éviction en ligne plus une API de balayage explicite. Les six cas limites imposés ont des tests ciblés, et la docstring explique les compromis précision/mémoire, les hypothèses et une extension distribuée concrète. Les faiblesses sont mineures : une assertion de test est vide en raison d'un ternaire confus, retry_after_ms de 0 lors d'un refus d'équité est ambigu, et les analyses d'éviction par appel ajoutent une surcharge pour les locataires ayant de nombreux clients.
Afficher le détail de l’évaluation ▼
Exactitude
Poids 35%La fenêtre glissante basée sur un journal est exacte : la suppression utilise t <= maintenant - fenêtre, donc une requête vieille d'une fenêtre complète expire, et le test de limite vérifie retry_after_ms de 1 à t=999 et l'admission à t=1000. Les horloges en arrière/dupliquées sont limitées au plus grand horodatage observé par le locataire, empêchant la revive de l'historique expiré. La file d'attente FIFO prouve l'application d'une redistribution round-robin sous saturation du locataire (vérifié par le traçage du test de saturation : le client a est bloqué tandis que les clients en file d'attente b et c reçoivent chacun un créneau libéré). Le seul RLock rend l'autorisation atomique, et le test à 40 threads admet exactement 10. Défauts mineurs : retry_after_ms de 0 pour un refus d'équité est ambigu, et un test d'éviction contient une assertion conditionnelle vide qui n'exerce jamais réellement l'appel d'autorisation.
Complétude
Poids 20%Toutes les exigences fonctionnelles sont satisfaites : configuration par locataire, fenêtre glissante exacte, limite globale par locataire avec redistribution équitable FIFO réelle, sécurité des threads, éviction via nettoyage en ligne et une API de balayage explicite, plus une aide debug_state. Les six cas limites imposés ont des tests dédiés (limite exacte, retour inactif, course concurrente, horloge en arrière/dupliquée limitée, saturation avec redistribution équitable, éviction obsolète préservant les clients actifs), et il couvre en outre les tours d'attente abandonnés et les limites de mémoire. Les hypothèses et une extension distribuée Redis/Lua sont documentées dans la docstring.
Qualité du code
Poids 20%Bien structuré avec des dataclasses figées pour la configuration et les résultats, une configuration validée, des signatures typées, une séparation claire des fonctions de suppression, de purge des attentes et d'aide à l'éviction, et une docstring de classe exceptionnellement complète couvrant l'algorithme, la sémantique d'équité, le comportement de l'horloge et l'extension distribuée. Faiblesses : le test d'éviction contient une assertion ternaire vide et confuse, l'éviction complète par client à chaque appel ajoute une surcharge O(clients) à chaque autorisation, et _remove_waiter reconstruit la deque en O(n).
Valeur pratique
Poids 15%Déployable tel quel pour un processus unique : les horodatages injectés déterministes le rendent testable, debug_state prend en charge la surveillance, sweep donne aux opérateurs un contrôle explicite de la mémoire, les délais d'attente des attentes empêchent les clients abandonnés de priver un locataire, et le chemin de migration distribuée (ensembles triés Redis plus Lua, avec les métadonnées d'attente dans la même transaction) est concrètement décrit. L'analyse d'éviction par autorisation est le principal inconvénient de performance pour les locataires ayant de nombreux clients.
Respect des consignes
Poids 10%Suit de près les spécifications : signature allow(tenant_id, client_id, now_ms), retry_after_ms dans le résultat, configuration réglable par locataire, véritable fenêtre glissante basée sur un journal avec discussion explicite du compromis précision/mémoire, redistribution équitable sous saturation, sécurité de la concurrence, mémoire limitée, les six cas limites dans les tests, et hypothèses explicites plus une note sur l'extension distribuée. Essentiellement, chaque livrable et cas limite listé dans l'invite est satisfait.