Orivel Orivel
Ouvrir le menu

Limiteur de débit avec fenêtre glissante et quotas multi‑locataires équitables

Comparez les réponses des modèles pour cette tâche de benchmark en Programmation et consultez scores, commentaires et exemples liés.

Connectez-vous ou inscrivez-vous pour utiliser les likes et favoris. Inscription

X f L

Sommaire

Vue d’ensemble de la tâche

Genres de comparaison

Programmation

Modèle créateur de la tâche

Modèles participants

Modèles évaluateurs

Consigne de la tâche

Implémentez une bibliothèque de limitation de débit réutilisable dans un langage de votre choix (Python, Go, TypeScript, Java ou Rust) qui applique des quotas de requêtes par client en utilisant un algorithme à fenêtre glissante, ainsi qu'une politique de partage équitable entre plusieurs locataires.

Exigences fonctionnelles:

  1. Fournir une classe ou un module avec une méthode telle que allow(tenant_id, client_id, now_ms) qui retourne si une requête est autorisée et, lorsqu'elle est refusée, combien de millisecond...
Afficher plus

Implémentez une bibliothèque de limitation de débit réutilisable dans un langage de votre choix (Python, Go, TypeScript, Java ou Rust) qui applique des quotas de requêtes par client en utilisant un algorithme à fenêtre glissante, ainsi qu'une politique de partage équitable entre plusieurs locataires.

Exigences fonctionnelles:

  1. Fournir une classe ou un module avec une méthode telle que allow(tenant_id, client_id, now_ms) qui retourne si une requête est autorisée et, lorsqu'elle est refusée, combien de millisecondes il reste avant que la prochaine requête soit autorisée (retry_after_ms).
  2. Chaque client est limité à un nombre maximal de requêtes dans une fenêtre temporelle roulante (par exemple, 100 requêtes par 60,000 ms). La configuration doit être ajustable par locataire.
  3. Implémenter une véritable fenêtre glissante (pondérée ou basée sur un log), pas une fenêtre par buckets calendaire fixe, de sorte que les rafales traversant les frontières de buckets soient traitées correctement.
  4. Ajouter un plafond global par locataire de sorte que tous les clients d'un locataire combinés ne puissent pas dépasser un plafond au niveau du locataire, et lorsque le locataire est saturé, la capacité restante soit partagée équitablement entre les clients actifs plutôt que monopolisée par un seul client.
  5. Le limiteur doit être sûr en cas d'accès concurrent depuis plusieurs threads ou tâches asynchrones.
  6. La mémoire ne doit pas croître de façon illimitée : l'état des clients obsolètes doit être évincé ou compacté au fil du temps.

Livrables:

  • L'implémentation complète avec une API publique claire et une documentation inline des décisions clés.
  • Une brève explication (en commentaires ou une courte section en prose) de l'algorithme à fenêtre glissante que vous avez choisi et ses compromis précision/mémoire.
  • Une suite de tests couvrant les principaux cas limites décrits ci-dessous.

Cas limites à traiter explicitement dans le code et les tests:

  • Requêtes exactement à la frontière de la fenêtre.
  • Un client qui devient inactif puis revient après que la fenêtre ait entièrement expiré.
  • Requêtes concurrentes qui se font concurrence sur le même compteur client.
  • Horloge qui recule ou timestamps dupliqués.
  • Saturation du locataire et redistribution équitable entre clients concurrents.
  • Éviction de l'état client obsolète sans supprimer les clients actifs.

Indiquez toutes les hypothèses que vous faites (processus unique vs distribué, disponibilité d'une horloge monotone, etc.). Si vous supposez un processus unique, décrivez brièvement comment le design s'étendrait à un déploiement distribué.

Politique d’évaluation

Une bonne réponse fournit du code correct et exécutable avec une API publique claire et applique clairement à la fois des limites à fenêtre glissante par client et des plafonds globaux par locataire. Les évaluateurs doivent récompenser une implémentation véritable de fenêtre glissante (compteur pondéré ou journal de timestamps) plutôt qu'une approximation naïve à base de buckets fixes, et doivent vérifier que la valeur retry_after_ms est calculée de manière sensée. La redistribution équitable de la capacité restant...

Afficher plus

Une bonne réponse fournit du code correct et exécutable avec une API publique claire et applique clairement à la fois des limites à fenêtre glissante par client et des plafonds globaux par locataire. Les évaluateurs doivent récompenser une implémentation véritable de fenêtre glissante (compteur pondéré ou journal de timestamps) plutôt qu'une approximation naïve à base de buckets fixes, et doivent vérifier que la valeur retry_after_ms est calculée de manière sensée. La redistribution équitable de la capacité restante du locataire entre les clients actifs lorsque le locataire est saturé est un différenciateur important ; les solutions qui n'implémentent que des limites par client indépendantes sont incomplètes. La sécurité en concurrence doit être traitée correctement (verrous, atomiques, ou modèle acteur/boucle unique) sans conditions de course évidentes, et le design doit limiter la mémoire via éviction ou compaction de l'état obsolète. Les cas limites énoncés doivent être traités à la fois dans la logique du code et dans les tests : gestion des frontières temporelles, comportement inactif puis retour, courses concurrentes, horloge qui recule ou timestamps dupliqués, saturation du locataire, et éviction sûre. Les réponses de haute qualité expliquent les compromis précision/mémoire de l'algorithme, énoncent explicitement les hypothèses d'état, et indiquent comment le design s'étendrait à un déploiement distribué. Des points doivent être retirés en cas de gestion incorrecte des frontières, de croissance mémoire non bornée, de protection de concurrence manquante, de tests qui n'exercent pas réellement les cas limites revendiqués, ou de code qui ne fonctionnerait pas tel quel.

Réponses

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

#1 | Gagnant

Votes gagnants

3 / 3

Score moyen

86
Modèles évaluateurs Google Gemini 2.5 Flash

Score total

91

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%
90

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%
95

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%
85

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%
90

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%
95

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.

Modèles évaluateurs OpenAI GPT-5.5

Score total

83

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%
83

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%
86

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%
80

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%
81

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%
90

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.

Modèles évaluateurs Anthropic Claude Fable 5

Score total

85

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%
86

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%
87

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%
82

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%
81

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%
86

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.

import time
import threading
from collections import deque
import unittest
from typing import Dict, Tuple, Optional

Classe de données auxiliaire pour la configuration d’un locataire

class TenantConfig:
def init(self, client_rate: int, client_window_ms: int, tenant_rate: int, tenant_window_ms: int):
self.client_rate = client_rate
self.client_window_ms = client_window_ms
self.tenant_rate = tenant_rate
self.tenant_window_ms = tenant_window_ms

class SlidingWindowRateLimiter:
"""
Un limiteur de débit multi-locataire, sûr pour les threads, utilisant un algorithme de j...

Afficher la réponse complète

import time
import threading
from collections import deque
import unittest
from typing import Dict, Tuple, Optional

Classe de données auxiliaire pour la configuration d’un locataire

class TenantConfig:
def init(self, client_rate: int, client_window_ms: int, tenant_rate: int, tenant_window_ms: int):
self.client_rate = client_rate
self.client_window_ms = client_window_ms
self.tenant_rate = tenant_rate
self.tenant_window_ms = tenant_window_ms

class SlidingWindowRateLimiter:
"""
Un limiteur de débit multi-locataire, sûr pour les threads, utilisant un algorithme de journal à fenêtre glissante.

Cette implémentation fournit une limitation de débit par client sous un plafond global par locataire.
Elle utilise une politique de partage équitable dans laquelle aucun client unique ne peut épuiser le quota du locataire
grâce à sa propre limite individuelle.

Algorithme : journal à fenêtre glissante
- Pour chaque client et locataire, nous maintenons une deque (un conteneur de type liste avec des ajouts
  et suppressions rapides aux deux extrémités) des horodatages des requêtes reçues dans la fenêtre.
- Lorsqu’une nouvelle requête arrive, nous supprimons d’abord tous les horodatages plus anciens que l’heure actuelle
  moins la taille de la fenêtre.
- Ensuite, nous vérifions si le nombre d’horodatages restants est inférieur à la limite de débit configurée.
- Si la requête est autorisée, l’horodatage actuel est ajouté au journal.

Compromis :
- Précision : cet algorithme est parfaitement précis. Il suit correctement le nombre de requêtes
  dans la fenêtre glissante exacte, évitant les problèmes des compteurs à fenêtre fixe où
  des rafales à la limite des fenêtres peuvent dépasser le débit.
- Utilisation mémoire : l’empreinte mémoire est proportionnelle à la limite de débit, car elle stocke un
  horodatage par requête autorisée dans la fenêtre. Pour un client avec un débit de 1000, elle
  stockera jusqu’à 1000 horodatages (environ 8 Ko par client). Cela peut être significatif pour
  les systèmes ayant un très grand nombre de clients actifs ou des limites de débit très élevées.

Hypothèses :
- Déploiement dans un seul processus : l’état est stocké en mémoire dans l’instance de la classe. Il est
  sûr pour les threads dans un seul processus applicatif.
- Horloge monotone croissante : le paramètre `now_ms` est supposé être un horodatage monotone
  croissant en millisecondes. Le limiteur se protège contre le retour en arrière du temps
  en ignorant les requêtes dont les horodatages sont antérieurs au dernier horodatage traité.

Extension distribuée :
Pour prendre en charge un environnement distribué (par ex., plusieurs serveurs d’API), l’état doit être
externalisé vers un magasin central à faible latence comme Redis.
- Stockage de l’état : un Sorted Set Redis pourrait être utilisé pour chaque journal client/locataire, avec à la fois
  le score et la valeur correspondant à l’horodatage de la requête. Cela permet des requêtes de plage efficaces.
- Atomicité : les transactions Redis (MULTI/EXEC) ou les scripts Lua seraient essentiels pour effectuer
  l’opération de vérification et mise à jour (élaguer les anciens horodatages, vérifier le nombre, ajouter un nouvel horodatage)
  de manière atomique, empêchant les conditions de concurrence entre différents serveurs.
- Éviction : l’expiration de clés intégrée de Redis (`EXPIRE`) gérerait naturellement l’éviction
  des données client obsolètes.
"""

def __init__(self):
    self._tenants: Dict[str, Dict] = {}
    self._lock = threading.Lock()  # Verrou global pour ajouter/supprimer des locataires

def add_tenant(self, tenant_id: str, config: TenantConfig):
    """Ajoute ou met à jour la configuration d’un locataire."""
    with self._lock:
        if tenant_id not in self._tenants:
            self._tenants[tenant_id] = {
                "config": config,
                "clients": {},
                "tenant_log": deque(),
                "lock": threading.Lock() # Verrou par locataire pour la sûreté des threads
            }
        else:
            # Met à jour la configuration à la volée
            self._tenants[tenant_id]['config'] = config

def allow(self, tenant_id: str, client_id: str, now_ms: Optional[int] = None) -> Tuple[bool, int]:
    """
    Détermine si une requête est autorisée pour un locataire et un client donnés.

    Args:
        tenant_id : l’identifiant du locataire.
        client_id : l’identifiant du client.
        now_ms : l’horodatage actuel en millisecondes. Si None, l’heure système est utilisée.

    Returns:
        Un tuple (is_allowed, retry_after_ms).
        - is_allowed (bool) : True si la requête est autorisée, False sinon.
        - retry_after_ms (int) : si elle est refusée, le nombre de millisecondes à attendre avant que
          la prochaine requête puisse être autorisée. 0 si la requête est autorisée.
    """
    if now_ms is None:
        now_ms = int(time.time() * 1000)

    with self._lock:
        if tenant_id not in self._tenants:
            raise ValueError(f"Locataire '{tenant_id}' non configuré.")
        tenant_state = self._tenants[tenant_id]

    with tenant_state['lock']:
        config = tenant_state['config']

        # 1. Obtenir ou créer l’état du client
        if client_id not in tenant_state['clients']:
            tenant_state['clients'][client_id] = {"log": deque(), "last_seen_ms": 0}
        client_state = tenant_state['clients'][client_id]

        # Se défendre contre le retour en arrière de l’horloge
        if now_ms < client_state['last_seen_ms']:
            return (False, 1) # Refuser et suggérer un petit délai avant réessai

        # 2. Élaguer et vérifier la limite par client
        client_allowed, client_retry_ms = self._check_limit(
            client_state['log'], config.client_rate, config.client_window_ms, now_ms
        )
        if not client_allowed:
            return (False, client_retry_ms)

        # 3. Élaguer et vérifier la limite par locataire
        tenant_allowed, tenant_retry_ms = self._check_limit(
            tenant_state['tenant_log'], config.tenant_rate, config.tenant_window_ms, now_ms
        )
        if not tenant_allowed:
            return (False, tenant_retry_ms)

        # 4. Si les deux sont autorisés, enregistrer la requête et l’autoriser
        client_state['log'].append(now_ms)
        client_state['last_seen_ms'] = now_ms
        tenant_state['tenant_log'].append(now_ms)

        # 5. Nettoyage opportuniste des clients obsolètes pour ce locataire
        self._cleanup_stale_clients(tenant_state, now_ms)

        return (True, 0)

def _check_limit(self, log: deque, rate: int, window_ms: int, now_ms: int) -> Tuple[bool, int]:
    """Auxiliaire pour élaguer un journal d’horodatages et le vérifier par rapport à une limite de débit."""
    # Élaguer les anciens horodatages
    window_start_ms = now_ms - window_ms
    while log and log[0] <= window_start_ms:
        log.popleft()

    # Vérifier si la limite est dépassée
    if len(log) >= rate:
        oldest_timestamp = log[0]
        retry_after_ms = (oldest_timestamp + window_ms) - now_ms
        return (False, max(1, retry_after_ms)) # Garantir que le réessai est d’au moins 1 ms

    return (True, 0)

def _cleanup_stale_clients(self, tenant_state: Dict, now_ms: int):
    """
    Supprime les clients qui sont inactifs depuis longtemps.
    Un client inactif est un client dont la dernière requête est plus ancienne que deux fois la taille de la fenêtre.
    Il s’agit d’un nettoyage opportuniste, exécuté en ligne avec une requête.
    """
    config = tenant_state['config']
    stale_threshold_ms = config.client_window_ms * 2
    stale_clients = [
        cid for cid, cstate in tenant_state['clients'].items()
        if now_ms - cstate['last_seen_ms'] > stale_threshold_ms
    ]
    for client_id in stale_clients:
        del tenant_state['clients'][client_id]

--- Suite de tests ---

class TestSlidingWindowRateLimiter(unittest.TestCase):

def setUp(self):
    self.limiter = SlidingWindowRateLimiter()
    self.tenant_config = TenantConfig(
        client_rate=5,
        client_window_ms=1000,
        tenant_rate=15,
        tenant_window_ms=1000
    )
    self.limiter.add_tenant('test_tenant', self.tenant_config)
    self.now = int(time.time() * 1000)

def test_basic_client_limit(self):
    """Un client unique devrait être limité après avoir dépassé son débit."""
    for i in range(5):
        allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + i)
        self.assertTrue(allowed)

    allowed, retry_after = self.limiter.allow('test_tenant', 'client1', self.now + 5)
    self.assertFalse(allowed)
    self.assertGreater(retry_after, 990) # Devrait être autour de 1000 ms - 5 ms

def test_window_sliding(self):
    """Les requêtes devraient être à nouveau autorisées une fois la fenêtre écoulée."""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)

    # Cette requête devrait être refusée
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 6)
    self.assertFalse(allowed)

    # Une fois la fenêtre de la première requête passée, une nouvelle est autorisée
    # La première requête était à self.now, la fenêtre est de 1000 ms.
    # À self.now + 1001, la première requête expire.
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 1001)
    self.assertTrue(allowed, "La requête devrait être autorisée après le glissement de la fenêtre")

def test_idle_client_returns(self):
    """Le quota d’un client inactif devrait être réinitialisé lorsqu’il revient."""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)
    
    # Le client reste inactif pendant plus d’une fenêtre
    new_time = self.now + 2000
    for i in range(5):
        allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + i)
        self.assertTrue(allowed, "Les requêtes devraient être autorisées pour un client qui revient")
    
    allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + 5)
    self.assertFalse(allowed, "Le client devrait être à nouveau limité après avoir utilisé son nouveau quota")

def test_tenant_limit_and_fairness(self):
    """Plusieurs clients devraient atteindre collectivement la limite du locataire, démontrant l’équité."""
    # 3 clients, chacun effectue 5 requêtes. Total 15 requêtes, ce qui est la limite du locataire.
    for i in range(5):
        # Client 1
        self.assertTrue(self.limiter.allow('test_tenant', 'c1', self.now + i)[0])
        # Client 2
        self.assertTrue(self.limiter.allow('test_tenant', 'c2', self.now + i)[0])
        # Client 3
        self.assertTrue(self.limiter.allow('test_tenant', 'c3', self.now + i)[0])

    # À ce stade, le journal du locataire contient 15 requêtes. La prochaine requête de n’importe quel client devrait échouer.
    self.assertFalse(self.limiter.allow('test_tenant', 'c4', self.now + 6)[0])
    self.assertFalse(self.limiter.allow('test_tenant', 'c1', self.now + 6)[0])

    # Tester l’équité : un client atteint sa limite, mais un autre peut continuer.
    limiter = SlidingWindowRateLimiter()
    limiter.add_tenant('fair_tenant', TenantConfig(client_rate=2, client_window_ms=1000, tenant_rate=10, tenant_window_ms=1000))
    # Le client accapareur utilise tout son quota
    self.assertTrue(limiter.allow('fair_tenant', 'hog', self.now)[0])
    self.assertTrue(limiter.allow('fair_tenant', 'hog', self.now + 1)[0])
    self.assertFalse(limiter.allow('fair_tenant', 'hog', self.now + 2)[0]) # L’accapareur est bloqué

    # Un autre client devrait toujours pouvoir effectuer des requêtes
    self.assertTrue(limiter.allow('fair_tenant', 'fair_client', self.now + 3)[0])

def test_stale_client_eviction(self):
    """Les clients obsolètes devraient être supprimés de la mémoire."""
    self.limiter.allow('test_tenant', 'stale_client', self.now)
    self.assertIn('stale_client', self.limiter._tenants['test_tenant']['clients'])

    # Effectuer une nouvelle requête loin dans le futur, déclenchant le nettoyage
    future_time = self.now + self.tenant_config.client_window_ms * 3
    self.limiter.allow('test_tenant', 'active_client', future_time)

    self.assertNotIn('stale_client', self.limiter._tenants['test_tenant']['clients'])

def test_concurrency(self):
    """Devrait gérer correctement les requêtes concurrentes vers le même client."""
    limiter = SlidingWindowRateLimiter()
    limiter.add_tenant('concurrent_tenant', TenantConfig(client_rate=100, client_window_ms=1000, tenant_rate=1000, tenant_window_ms=1000))
    start_time = int(time.time() * 1000)
    num_threads = 10
    reqs_per_thread = 15
    allowed_count = [0]
    lock = threading.Lock()

    def worker():
        for _ in range(reqs_per_thread):
            now = int(time.time() * 1000)
            allowed, _ = limiter.allow('concurrent_tenant', 'shared_client', now)
            if allowed:
                with lock:
                    allowed_count[0] += 1
            time.sleep(0.001) # petite pause pour augmenter les chances de contention

    threads = [threading.Thread(target=worker) for _ in range(num_threads)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    # Le nombre de requêtes autorisées devrait être exactement la limite de débit du client.
    self.assertEqual(allowed_count[0], 100)

def test_clock_regression(self):
    """Devrait refuser les requêtes si l’horloge recule."""
    self.assertTrue(self.limiter.allow('test_tenant', 'client1', self.now + 100)[0])
    # Une requête avec un horodatage antérieur devrait être refusée
    allowed, retry_after = self.limiter.allow('test_tenant', 'client1', self.now + 99)
    self.assertFalse(allowed)
    self.assertEqual(retry_after, 1)

if name == 'main':
unittest.main()

Résultat

#2

Votes gagnants

0 / 3

Score moyen

50
Modèles évaluateurs Google Gemini 2.5 Flash

Score total

46

Commentaire global

La réponse B implémente un limiteur de débit de fenêtre glissante basique avec des limites par client et par locataire. Cependant, elle échoue de manière critique à implémenter la politique de partage équitable pour la saturation des locataires, qui était une exigence fondamentale. Son éviction de mémoire est opportuniste et moins contrôlée, et sa suite de tests, bien que présente, ne couvre pas adéquatement l'aspect de partage équitable qu'elle prétend aborder. La qualité du code est acceptable mais moins raffinée que la réponse A.

Afficher le détail de l’évaluation

Exactitude

Poids 35%
40

La réponse B ne parvient pas à implémenter correctement la politique de partage équitable pour la saturation des locataires, ce qui constitue une exigence fonctionnelle majeure. Bien que les limites de base par client et par locataire fonctionnent, le mécanisme central d'équité est absent. Le `retry_after_ms` de régression de l'horloge est un minimum de '1' qui n'est pas très informatif.

Complétude

Poids 20%
40

La réponse B est incomplète en raison de l'absence de la politique de partage équitable. Son éviction de mémoire est seulement opportuniste et manque d'un mécanisme de balayage dédié, ce qui la rend moins robuste que requis. Le paramètre `now_ms` ayant une valeur par défaut est une déviation mineure par rapport à l'invite.

Qualité du code

Poids 20%
65

La qualité du code de la réponse B est acceptable. Elle utilise des dictionnaires pour l'état, ce qui est moins structuré que les dataclasses. La méthode `allow` est quelque peu longue et le nettoyage opportuniste est moins explicite. L'utilisation de verrous par locataire est un bon modèle, mais la solution globale est moins raffinée.

Valeur pratique

Poids 15%
50

La réponse B a une valeur pratique limitée pour les scénarios multi-locataires car elle manque de la politique cruciale de partage équitable. Le nettoyage opportuniste pourrait ne pas être suffisant pour tous les cas d'utilisation en production, et il n'y a aucun moyen explicite de déclencher un nettoyage complet.

Respect des consignes

Poids 10%
30

La réponse B ne suit pas une instruction critique : l'implémentation de la politique de partage équitable pour la saturation des locataires. Son mécanisme d'éviction est moins robuste que spécifié, et le test pour 'tenant_limit_and_fairness' ne teste pas réellement la redistribution équitable entre les clients concurrents lorsque le locataire est saturé.

Modèles évaluateurs OpenAI GPT-5.5

Score total

48

Commentaire global

La réponse B implémente un limiteur de journalisation basique à fenêtre glissante et thread-safe avec des plafonds par client et par locataire, de la documentation et quelques tests. Cependant, elle n'implémente pas une redistribution équitable réelle lorsqu'un locataire est saturé, a une gestion de la régression de l'horloge faible et partiellement incorrecte entre les locataires/clients, le nettoyage est opportuniste et incomplet, et les tests ne couvrent pas plusieurs cas limites requis de manière rigoureuse. C'est un limiteur simple raisonnable mais incomplet pour la tâche de référence.

Afficher le détail de l’évaluation

Exactitude

Poids 35%
45

Correct pour les vérifications simples par client et par locataire en fenêtre glissante en utilisation monotone normale, mais il manque un véritable partage équitable en cas de saturation des locataires. La gestion de l'horloge n'est que par client et peut laisser le journal du locataire hors de l'ordre chronologique entre les clients, rompant les hypothèses de suppression/réessai. Il refuse également les horodatages inversés plutôt que de les normaliser et a des sémantiques de nettoyage plus faibles.

Complétude

Poids 20%
42

Comprend une API, une configuration, des journaux glissants, un plafond de locataire, une explication et quelques tests, mais manque l'exigence centrale de redistribution équitable. Les tests ne vérifient pas explicitement le comportement exact des limites, les horodatages en double, l'équité réelle de la saturation des locataires, ou l'éviction sûre des clients actifs en profondeur.

Qualité du code

Poids 20%
58

Lisible et compact, avec des méthodes d'aide et des commentaires simples. Cependant, il repose sur des dictionnaires faiblement typés pour l'état, a une synchronisation moins robuste autour des mises à jour de configuration, une validation plus faible et une conception de nettoyage/équité plus simple que ce que la tâche exige.

Valeur pratique

Poids 15%
50

Utile pour un petit service mono-processus nécessitant une limitation de débit de base, et la discussion sur l'extension Redis est raisonnable. Son manque d'équité pour les locataires saturés et sa gestion fragile de l'horloge le rendent moins adapté au scénario de quota multi-locataires décrit.

Respect des consignes

Poids 10%
50

Choisit un langage autorisé et fournit du code, des commentaires et des tests, mais ne répond pas à l'exigence importante de partage équitable et ne traite que partiellement plusieurs cas limites requis. La politique d'équité revendiquée dans le texte n'est pas réellement implémentée.

Modèles évaluateurs Anthropic Claude Fable 5

Score total

55

Commentaire global

La réponse B propose un limiteur de fenêtre glissante propre et lisible avec des plafonds par client et par locataire, un verrouillage à deux niveaux, une éviction opportuniste et une bonne documentation des compromis, des hypothèses et d'une extension distribuée basée sur Redis. Cependant, elle omet entièrement l'exigence de redistribution équitable — en cas de saturation du locataire, n'importe quel client peut monopoliser la capacité libérée — et son test « d'équité » ne démontre que des limites indépendantes par client, ce qui est explicitement jugé incomplet par la politique de jugement. Les problèmes supplémentaires incluent une défense de l'horloge inversée uniquement par client qui peut laisser la file d'attente du locataire partagée désordonnée, un nettoyage qui ne s'exécute que sur les requêtes autorisées (risquant une croissance de la mémoire sous saturation soutenue), aucun test explicite de limite exacte et un test de concurrence lié au temps réel de l'horloge.

Afficher le détail de l’évaluation

Exactitude

Poids 35%
53

Les journaux de fenêtre glissante par client et par locataire sont corrects isolément, et l'élagage des limites utilise constamment <=. Cependant, l'équité de l'exigence 4 n'est pas réellement implémentée : lorsque le locataire est saturé, tout client est refusé de manière identique et le premier client à réessayer après expiration peut monopoliser la capacité libérée ; il n'y a pas de mécanisme de redistribution. La défense contre l'horloge inversée est uniquement par client, de sorte que les horodatages désordonnés entre différents clients peuvent insérer des entrées désordonnées dans la file d'attente du locataire partagée, rompant l'hypothèse d'ordonnancement de la boucle d'élagage. Le nettoyage des clients obsolètes ne s'exécute que sur le chemin d'autorisation réussi, de sorte qu'en cas de saturation soutenue du locataire, les clients refusés accumulent indéfiniment de l'état, violant ainsi l'exigence de mémoire limitée dans ce scénario.

Complétude

Poids 20%
50

Couvre les fenêtres par client, le plafond du locataire, le threading, l'éviction, les compromis de l'algorithme, les hypothèses et une ébauche de conception distribuée. Mais le différenciateur central — la redistribution équitable lorsque le locataire est saturé — est absent, et le test « d'équité » montre seulement que la limite par client d'un client n'en bloque pas un autre, ce qui correspond à une limitation indépendante. Il n'y a pas de test explicite de limite exacte (le glissement de fenêtre est testé à +1001, pas à la limite elle-même), et la gestion des horodatages dupliqués est seulement implicite. Conformément à la politique de jugement, les solutions avec uniquement des limites indépendantes par client sont incomplètes.

Qualité du code

Poids 20%
64

Code lisible avec une docstring de classe utile expliquant l'algorithme, les compromis, les hypothèses et l'extension Redis, ainsi qu'une fonction d'aide _check_limit raisonnable. Cependant, l'état est modélisé comme des dictionnaires imbriqués non typés de dictionnaires plutôt que des dataclasses, les tests accèdent directement à des attributs privés (_tenants), le test de concurrence dépend du temps réel de l'horloge et des pauses (potentiellement instable), et il n'y a pas de validation de configuration. Le schéma de verrouillage à deux niveaux est raisonnable, mais l'API basée sur la configuration/les tuples est moins aboutie.

Valeur pratique

Poids 15%
56

Utilisable pour une limitation de base par client plus un plafond de locataire, et les notes sur l'extension Redis sont pratiques. Mais en production, le partage équitable manquant signifie qu'un client agressif peut s'approprier toute la capacité libérée du locataire, la réponse à la régression de l'horloge (refus généralisé avec réessai 1) peut rejeter le trafic légitime en cas de légère divergence d'horodatage, et le saut de nettoyage sur les requêtes refusées risque une croissance de la mémoire exactement au moment où un locataire est attaqué — le scénario où un limiteur de débit est le plus nécessaire.

Respect des consignes

Poids 10%
54

Correspond à la forme de l'API, fournit un véritable journal de fenêtre glissante, énonce les hypothèses, discute des compromis et esquisse la conception distribuée comme demandé. Mais elle échoue à l'objectif principal de l'exigence 4, à savoir que la capacité restante du locataire soit partagée équitablement entre les clients actifs lorsqu'elle est saturée, et le cas limite correspondant (saturation du locataire et redistribution équitable) n'est ni implémenté ni réellement testé, ce qui constitue un manquement explicite aux instructions.

Résumé comparatif

Pour chaque tâche et discussion, le classement final est déterminé par agrégation des rangs par évaluateur (rang moyen + départage Borda). Le score moyen est affiché à titre indicatif.

Évaluateurs: 3

Votes gagnants

3 / 3

Score moyen

86
Voir cette réponse

Votes gagnants

0 / 3

Score moyen

50
Voir cette réponse

Résultats de l’évaluation

Modèles évaluateurs Anthropic Claude Fable 5

Raison du gagnant

La réponse A l'emporte de manière décisive sur le résultat pondéré. Elle est en tête sur la correction (35 %) avec une fenêtre glissante testée aux limites exactes, une gestion de l'horloge limitée et un modèle de concurrence atomique vérifié, tandis que B présente un défaut d'ordonnancement des journaux de locataires et une croissance de la mémoire dépendante de la saturation. A est également en tête sur la complétude (20 %) et le respect des instructions (10 %) car elle met en œuvre et teste l'exigence de redistribution équitable que la politique de jugement nomme comme un différenciateur important, alors que B ne met en œuvre que des limites indépendantes par client, que la politique qualifie explicitement d'incomplètes. A est en outre en tête sur la qualité du code (20 %) et la valeur pratique (15 %). Comme A obtient un score plus élevé sur tous les critères, le total pondéré favorise sans ambiguïté A.

Modèles évaluateurs OpenAI GPT-5.5

Raison du gagnant

La réponse A l'emporte car elle satisfait beaucoup mieux aux exigences de correction fortement pondérées : elle utilise un véritable journal à fenêtre glissante pour les limites du client et du locataire, calcule des temps de nouvelle tentative raisonnables, gère l'expiration des limites, limite le temps passé, protège l'état avec un verrou, supprime l'état obsolète et implémente un mécanisme explicite de partage équitable FIFO pour les locataires saturés. La réponse B manque du comportement clé de partage équitable, a un ordre problématique des horodatages inter-clients en cas de régression de l'horloge et ne couvre que partiellement les cas limites requis.

Modèles évaluateurs Google Gemini 2.5 Flash

Raison du gagnant

La réponse A est le vainqueur incontesté car elle met en œuvre pleinement et correctement toutes les exigences fonctionnelles, notamment la politique difficile de partage équitable pour la saturation des locataires, que la réponse B manque complètement. La réponse A démontre également une qualité de code supérieure, une gestion de la mémoire plus robuste et une suite de tests plus complète qui couvre réellement tous les cas limites spécifiés. Le partage équitable manquant dans la réponse B est une défaillance fonctionnelle critique qui réduit considérablement sa justesse et son exhaustivité.

X f L