Orivel Orivel
Menü öffnen

Ratenbegrenzer mit gleitendem Fenster und fairen Mehrmandantenquoten

Vergleiche Modellantworten für diese Programmierung-Benchmark-Aufgabe und prüfe Scores, Kommentare und verwandte Beispiele.

Bitte einloggen oder registrieren, um Likes und Favoriten zu nutzen. Registrieren

X f L

Inhalt

Aufgabenübersicht

Vergleichsgenres

Programmierung

Aufgaben-Erstellermodell

Antwortende Modelle

Bewertungsmodelle

Aufgabenstellung

Implementieren Sie eine wiederverwendbare Rate-Limiter-Bibliothek in einer Sprache Ihrer Wahl (Python, Go, TypeScript, Java oder Rust), die pro Client Anfragenquoten mithilfe eines gleitenden-Fenster-Algorithmus durchsetzt und zusätzlich eine faire Verteilungsrichtlinie über mehrere Mandanten bietet.

Funktionale Anforderungen:

  1. Bieten Sie eine Klasse oder ein Modul mit einer Methode wie allow(tenant_id, client_id, now_ms), die zurückgibt, ob eine Anfrage erlaubt ist und, falls sie abgelehnt wird, wie viele Milli...
Mehr anzeigen

Implementieren Sie eine wiederverwendbare Rate-Limiter-Bibliothek in einer Sprache Ihrer Wahl (Python, Go, TypeScript, Java oder Rust), die pro Client Anfragenquoten mithilfe eines gleitenden-Fenster-Algorithmus durchsetzt und zusätzlich eine faire Verteilungsrichtlinie über mehrere Mandanten bietet.

Funktionale Anforderungen:

  1. Bieten Sie eine Klasse oder ein Modul mit einer Methode wie allow(tenant_id, client_id, now_ms), die zurückgibt, ob eine Anfrage erlaubt ist und, falls sie abgelehnt wird, wie viele Millisekunden bis zur nächsten erlaubten Anfrage verbleiben (retry_after_ms).
  2. Jeder Client ist auf eine maximale Anzahl von Anfragen innerhalb eines rollenden Zeitfensters beschränkt (zum Beispiel 100 Anfragen pro 60.000 ms). Die Konfiguration muss pro Mandant anpassbar sein.
  3. Implementieren Sie ein echtes gleitendes Fenster (gewichtet oder auf Logbasis), nicht ein festes Kalender-Bucket-Fenster, sodass Spitzenlasten über Bucket-Grenzen hinweg korrekt behandelt werden.
  4. Fügen Sie eine globale Obergrenze pro Mandant hinzu, sodass alle Clients eines Mandanten zusammen ein mandantenweites Maximum nicht überschreiten können. Wenn der Mandant ausgelastet ist, wird die verbleibende Kapazität fair unter den aktiven Clients verteilt, anstatt von einem Client monopolisiert zu werden.
  5. Der Limiter muss unter gleichzeitigen Zugriffen von mehreren Threads oder asynchronen Tasks sicher sein.
  6. Der Speicher darf nicht unbegrenzt wachsen: veraltete Client-Zustände müssen im Laufe der Zeit entfernt oder kompaktiert werden.

Liefergegenstände:

  • Die vollständige Implementierung mit klarer öffentlicher API und Inline-Dokumentation wichtiger Entscheidungen.
  • Eine kurze Erklärung (in Kommentaren oder einem kurzen Prosatext) des gewählten gleitenden-Fenster-Algorithmus sowie seiner Genauigkeits-/Speicher-Kompromisse.
  • Eine Testsuite, die die unten beschriebenen Kern-Grenzfälle abdeckt.

In Code und Tests ausdrücklich zu behandelnde Grenzfälle:

  • Anfragen genau an der Fenstergrenze.
  • Ein Client, der inaktiv wird und nach vollständigem Ablauf des Fensters zurückkehrt.
  • Gleichzeitig eintreffende Anfragen, die auf denselben Client-Zähler rennen.
  • Uhr, die rückwärts läuft, oder doppelte Zeitstempel.
  • Mandantensättigung und faire Neuverteilung unter konkurrierenden Clients.
  • Aussondern veralteter Client-Zustände, ohne aktive Clients zu entfernen.

Geben Sie alle Annahmen an, die Sie treffen (Einzelprozess vs. verteilt, Verfügbarkeit einer monotonen Uhr usw.). Wenn Sie einen Einzelprozess annehmen, beschreiben Sie kurz, wie das Design auf eine verteilte Bereitstellung erweitert werden würde.

Bewertungsrichtlinie

Eine starke Antwort liefert korrekt ausführbaren Code mit einer klaren öffentlichen API und setzt sowohl pro-Client-Gleitfenster-Limits als auch mandantenweite Gesamtkappen eindeutig durch. Prüfer sollten eine echte Implementierung eines gleitenden Fensters (gewichteter Zähler oder Zeitstempel-Log) belohnen, statt einer naiven Fest-Bucket-Annäherung, und prüfen, dass der Wert retry_after_ms sinnvoll berechnet wird. Die faire Neuverteilung der verbleibenden Mandantenkapazität unter aktiven Clients, wenn der Mandant...

Mehr anzeigen

Eine starke Antwort liefert korrekt ausführbaren Code mit einer klaren öffentlichen API und setzt sowohl pro-Client-Gleitfenster-Limits als auch mandantenweite Gesamtkappen eindeutig durch. Prüfer sollten eine echte Implementierung eines gleitenden Fensters (gewichteter Zähler oder Zeitstempel-Log) belohnen, statt einer naiven Fest-Bucket-Annäherung, und prüfen, dass der Wert retry_after_ms sinnvoll berechnet wird. Die faire Neuverteilung der verbleibenden Mandantenkapazität unter aktiven Clients, wenn der Mandant gesättigt ist, ist ein wichtiges Unterscheidungsmerkmal; Lösungen, die nur unabhängige pro-Client-Limits implementieren, sind unvollständig. Nebenläufigkeitssicherheit muss korrekt gehandhabt werden (Locks, Atomics oder ein Actor-/Single-Loop-Modell) ohne offensichtliche Race-Conditions, und das Design muss den Speicher über Eviction oder Kompaktierung veralteter Zustände begrenzen. Die angegebenen Grenzfälle sollten sowohl in der Logik des Codes als auch in den Tests behandelt werden: zeitliche Grenzbehandlung, inaktiv-then-return-Verhalten, gleichzeitige Rennen, rückwärts laufende oder doppelte Uhrenwerte, Mandantensättigung und sichere Eviction. Antworten höherer Qualität erklären die Genauigkeits-/Speicher-Kompromisse des Algorithmus, geben Annahmen explizit an und erläutern, wie das Design auf eine verteilte Umgebung ausgeweitet werden könnte. Abzüge erfolgen für falsche Grenzfallbehandlung, unbegrenztes Speicherwachstum, fehlenden Nebenläufigkeitsschutz, Tests, die die angegebenen Grenzfälle nicht tatsächlich prüfen, oder Code, der so nicht ausführbar wäre.

Antworten

Antwort 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:
"""Rate limits for one tenant.

client_limit applies independently to each client, while tenant_limit applies
to all admitted requests from the tenant. Both limits use exact rolling
windows. active_timeout_ms bounds how long an absent fairness waiter can
retain its turn. eviction_ms controls client-state retention.
"""

client_limit: int
client_wind...
Vollständige Antwort anzeigen

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:
"""Rate limits for one tenant.

client_limit applies independently to each client, while tenant_limit applies
to all admitted requests from the tenant. Both limits use exact rolling
windows. active_timeout_ms bounds how long an absent fairness waiter can
retain its turn. eviction_ms controls client-state retention.
"""

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("limits must be positive")
    if self.client_window_ms <= 0 or self.tenant_window_ms <= 0:
        raise ValueError("window lengths must be positive")
    if self.active_timeout_ms <= 0:
        raise ValueError("active_timeout_ms must be positive")
    if self.eviction_ms < max(self.client_window_ms, self.tenant_window_ms):
        raise ValueError("eviction_ms must be at least as large as both windows")
    if self.eviction_ms < self.active_timeout_ms:
        raise ValueError("eviction_ms must be at least 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:
"""Thread-safe, single-process, exact sliding-window rate limiter.

Sliding-window algorithm:
  Every admitted request timestamp is stored in a deque. Before evaluating a
  request, timestamps t satisfying t <= now - window are removed. Thus a
  request exactly one full window old no longer consumes capacity. This is a
  true log-based sliding window, so it has no calendar-bucket boundary burst.

  The result is exact, with O(1) amortized deque operations and memory
  proportional to admitted requests still inside configured windows. This is
  more accurate than weighted buckets but uses more memory. Limits provide a
  hard bound on live timestamp entries, and stale client objects are evicted.

Fair sharing:
  Uncontended tenant capacity is work-conserving and can be used by any
  client. Once the tenant ceiling is reached, denied clients enter a FIFO
  queue with at most one entry per client. As capacity expires, the head gets
  the next admission, then leaves the queue. A continuously busy client must
  rejoin behind other contenders, implementing round-robin redistribution
  without permanently reserving unused per-client shares. Inactive queue
  entries expire after active_timeout_ms so an abandoned turn cannot block
  the tenant forever.

Clock behavior:
  now_ms should come from a monotonic clock. If a caller supplies a duplicate
  or decreasing timestamp, time is clamped to the tenant's greatest observed
  timestamp. This prevents expired history from becoming live again.

Concurrency and deployment assumptions:
  A single re-entrant lock makes allow, configuration, and sweeping atomic
  across threads and async tasks sharing this object. For distributed use,
  the same state transition can be implemented atomically in Redis with a
  Lua script and sorted sets, or in a transactional strongly consistent
  store. FIFO waiter metadata must be updated in the same transaction as the
  request logs; local locks alone do not coordinate multiple processes.
"""

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:
    """Create or replace a tenant's configuration atomically."""
    config.validate()
    with self._lock:
        self._configs[tenant_id] = config

def allow(self, tenant_id: Hashable, client_id: Hashable, now_ms: int) -> AllowResult:
    """Atomically decide whether to admit one request.

    retry_after_ms is exact for a direct client/window or tenant/window
    denial. For a FIFO fairness denial it is the earliest temporal retry;
    actual admission also depends on earlier active waiters taking or
    abandoning their turns.
    """
    if not isinstance(now_ms, int):
        raise TypeError("now_ms must be an integer")

    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))

        # Capacity exists. During contention, only the FIFO head may take it.
        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:
    """Evict stale clients and empty tenant runtime states.

    Applications can call this periodically when traffic is low. Normal
    allow calls also clean the tenant being accessed. Returns the number of
    client and tenant runtime objects removed. Configurations are retained.
    """
    if not isinstance(now_ms, int):
        raise TypeError("now_ms must be an integer")

    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]:
    """Return aggregate state sizes, useful for monitoring and 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"tenant {tenant_id!r} has no 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)
        # A client blocked by its own quota does not reserve a tenant turn.
        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)

    # Both old slots expire, but a cannot monopolize them: b and c queued.
    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)
    # At 1001 the waiter is inactive, so another client may use capacity.
    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)

    # The active request is not dropped before its own window expires.
    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()

Ergebnis

#1 | Sieger

Siegstimmen

3 / 3

Durchschnittsscore

86
Bewertungsmodelle Google Gemini 2.5 Flash

Gesamtpunktzahl

91

Gesamtkommentar

Antwort A bietet eine umfassende und robuste Implementierung des Ratenbegrenzers, die alle funktionalen Anforderungen vollständig erfüllt, einschließlich der komplexen Fair-Sharing-Richtlinie für die Mietersättigung. Der Code ist gut strukturiert, verwendet geeignete Datenstrukturen und enthält eine gründliche Dokumentation sowie eine starke Testsuite, die alle spezifizierten Randfälle abdeckt. Das Speichermanagement durch explizites Sweep und konfigurierbare Eviction ist gut durchdacht.

Bewertungsdetails anzeigen

Korrektheit

Gewichtung 35%
90

Antwort A implementiert alle Aspekte des Ratenbegrenzers korrekt, einschließlich der komplexen Fair-Sharing-Richtlinie für die Mietersättigung und der robusten Handhabung von Uhren. Alle Limits und Fensterlogiken sind präzise.

Vollständigkeit

Gewichtung 20%
95

Antwort A ist sehr vollständig und erfüllt alle funktionalen Anforderungen, einschließlich des nuancierten Fair-Sharing und der umfassenden Speicherbereinigung. Sie liefert klare Erklärungen, Annahmen und Hinweise zum verteilten Design, wie gefordert.

Codequalität

Gewichtung 20%
85

Der Code in Antwort A ist gut strukturiert, verwendet `dataclasses` für eine klare Zustandsverwaltung und verfügt über eine gute Inline-Dokumentation. Die Verwendung von `threading.RLock` ist für re-entrant Operationen geeignet. Methoden sind logisch getrennt und sauber.

Praktischer Nutzen

Gewichtung 15%
90

Antwort A bietet einen hohen praktischen Wert aufgrund ihres vollständigen Funktionsumfangs, insbesondere des Fair-Sharing-Mechanismus und der gesteuerten Speicherbereinigung über die `sweep`-Methode. Die `debug_state`-Methode ist ebenfalls eine nützliche Ergänzung zur Überwachung.

Befolgung der Anweisungen

Gewichtung 10%
95

Antwort A folgt akribisch allen Anweisungen, einschließlich der komplexen Fair-Sharing-Richtlinie, des echten gleitenden Fensters, der Nebenläufigkeitssicherheit, der Speicherbegrenzung und der Berücksichtigung aller spezifizierten Randfälle in Code und Tests. Alle Liefergegenstände werden bereitgestellt.

Bewertungsmodelle OpenAI GPT-5.5

Gesamtpunktzahl

83

Gesamtkommentar

Antwort A bietet eine weitgehend vollständige, lauffähige Python-Implementierung mit exakten protokollbasierten gleitenden Fenstern, mandantenfähiger Konfiguration, mandantenweiten Obergrenzen, Nebenläufigkeitsschutz, Bereinigung veralteter Zustände, Wiederholungsberechnungen, expliziter Behandlung von Zeitabweichungen und einer breiten Unit-Test-Suite. Sein FIFO-Wartemechanismus ist eine glaubwürdige Fair-Sharing-Richtlinie bei Mandantenübersättigung, obwohl retry_after_ms für Ablehnungen in der Fairness-Warteschlange ungenau sein kann und der Zustand des Wartenden/Clients für viele abgelehnte eindeutige Clients bis zum Timeout oder Sweep wachsen kann. Insgesamt entspricht es eng dem angeforderten Bibliotheksverhalten und den Randfällen.

Bewertungsdetails anzeigen

Korrektheit

Gewichtung 35%
83

Verwendet exakte Zeitstempel-Protokolle mit korrekter Randbeschneidung, erzwingt sowohl Mandanten- als auch Mandantenobergrenzen, klemmt die rückwärtige Zeit und serialisiert Mutationen sicher. Die FIFO-Fairness-Logik ist größtenteils korrekt, aber retry_after_ms für nicht-Kopf-Fairness-Ablehnungen kann 0 sein und der Speicher für viele wartende abgelehnte Clients ist nur zeitlich begrenzt und nicht sofort.

Vollständigkeit

Gewichtung 20%
86

Umfasst die angeforderte API, mandantenfähige einstellbare Konfiguration, exakte gleitende Fenster, Mandantenobergrenze, Fairness-Warteschlange, Nebenläufigkeitssicherheit, Aussonderung/Sweep, Annahmen, Hinweise zur verteilten Erweiterung und Tests für fast alle spezifizierten Randfälle. Kleinere Lücken sind unvollkommene Fairness-Wiederholungsberichte und etwas umständlicher Code für Aussonderungstests.

Codequalität

Gewichtung 20%
80

Gut strukturierte Datenklassen, klare öffentliche Methoden, Validierung, Inline-Dokumentation, Typ-Hinweise und gekapselte Hilfsmethoden. Das einzelne globale Schloss ist einfach und sicher, begrenzt aber die Skalierbarkeit, und einige interne Elemente/Tests sind etwas sperrig.

Praktischer Nutzen

Gewichtung 15%
81

Praktisch als wiederverwendbare Single-Process-Bibliothek mit Überwachungs-/Debug-Status, periodischem Sweep, konfigurierbaren Mandanten und einem glaubwürdigen Weg zu Redis/Lua für verteilte Nutzung. Das globale Schloss und die Speicherkosten des Zeitstempel-Protokolls können High-Scale-Bereitstellungen einschränken, aber die Kompromisse werden dargelegt.

Befolgung der Anweisungen

Gewichtung 10%
90

Folgt den angeforderten Ergebnissen genau: vollständiger Code, dokumentierter Algorithmus und Kompromisse, explizite Annahmen, verteilte Erweiterung und Tests für die genannten Randfälle. Die API gibt ein AllowResult anstelle eines Tupels zurück, was gemäß der Aufforderung akzeptabel ist.

Bewertungsmodelle Anthropic Claude Fable 5

Gesamtpunktzahl

85

Gesamtkommentar

Antwort A ist eine nahezu vollständige, produktionsreife Lösung. Sie implementiert ein exaktes protokollbasiertes gleitendes Fenster mit korrekter Grenzsemantik, einen echten Fair-Redistribution-Mechanismus über eine FIFO-Warteschlange mit Aktivitäts-Timeouts, eine mandantenbeschränkte Handhabung von rückwärts und doppelten Uhren, ein einzelnes RLock für Atomarität und begrenzten Speicher durch Inline-Bereinigung plus eine explizite Sweep-API. Alle sechs vorgeschriebenen Randfälle haben gezielte Tests, und die Docstring erklärt Genauigkeits-/Speicher-Kompromisse, Annahmen und eine konkrete verteilte Erweiterung. Schwächen sind geringfügig: eine Testbehauptung ist aufgrund eines verwirrenden Ternary nicht aussagekräftig, retry_after_ms von 0 bei einer Fairness-Verweigerung ist mehrdeutig, und pro-Aufruf-Bereinigungsscans verursachen Overhead für Mandanten mit vielen Clients.

Bewertungsdetails anzeigen

Korrektheit

Gewichtung 35%
86

Das protokollbasierte gleitende Fenster ist exakt: Pruning verwendet t <= now - window, sodass eine Anfrage, die genau ein volles Fenster alt ist, abläuft, und der Grenztest verifiziert retry_after_ms von 1 bei t=999 und Zulassung bei t=1000. Rückwärts-/Doppeluhren werden auf den größten beobachteten Zeitstempel des Mandanten geklemmt, wodurch verhindert wird, dass abgelaufene Verlaufsdaten wiederbelebt werden. Die FIFO-Warteschlange erzwingt nachweislich eine Round-Robin-Umverteilung unter Mandantensättigung (verifiziert durch Nachverfolgung des Sättigungstests: Client a wird blockiert, während die Warteschlangen-Clients b und c jeweils einen freien Slot erhalten). Das einzelne RLock macht allow atomar, und der 40-Thread-Test lässt genau 10 zu. Kleinere Fehler: retry_after_ms von 0 für eine Fairness-Verweigerung ist mehrdeutig, und ein Eviction-Test enthält eine nichtssagende bedingte Assertion, die den allow-Aufruf tatsächlich nie ausführt.

Vollständigkeit

Gewichtung 20%
87

Jede funktionale Anforderung ist erfüllt: mandantenbezogene Konfiguration, exaktes gleitendes Fenster, mandantenweiter globaler Grenzwert mit echter FIFO-Fair-Redistribution, Threadsicherheit, Eviction sowohl durch Inline-Bereinigung als auch durch eine explizite Sweep-API, plus ein debug_state-Helfer. Alle sechs vorgeschriebenen Randfälle haben dedizierte Tests (exakte Grenze, Leerlauf-Rückgabe, gleichzeitiges Rennen, geklemmte rückwärts/doppelte Uhr, Sättigung mit fairer Umverteilung, veraltete Eviction unter Beibehaltung aktiver Clients), und es werden zusätzlich verlassene Waiter-Züge und Speicherlimits abgedeckt. Annahmen und eine verteilte Erweiterung für Redis/Lua werden in der Docstring dokumentiert.

Codequalität

Gewichtung 20%
82

Gut strukturiert mit gefrorenen Dataclasses für Konfiguration und Ergebnisse, validierter Konfiguration, typisierten Signaturen, klarer Trennung von Pruning-, Waiter-Purging- und Eviction-Helfern und einer ungewöhnlich gründlichen Klassen-Docstring, die Algorithmus, Fairness-Semantik, Verhalten der Uhr und verteilte Erweiterung abdeckt. Schwächen: Der Eviction-Test enthält eine verwirrende, nichtssagende Ternary-Assertion, die pro-Aufruf-Eviction für alle Clients fügt O(clients) Overhead zu jedem allow hinzu, und _remove_waiter baut die Deque in O(n) neu auf.

Praktischer Nutzen

Gewichtung 15%
81

Sofort einsetzbar für einen einzelnen Prozess: deterministische injizierte Zeitstempel machen es testbar, debug_state unterstützt die Überwachung, sweep gibt Betreibern explizite Speicherkontrolle, Waiter-Timeouts verhindern, dass verlassene Clients einen Mandanten aushungern, und der verteilte Migrationspfad (Redis-Sortierte Mengen plus Lua, mit Waiter-Metadaten in derselben Transaktion) ist konkret beschrieben. Der Eviction-Scan pro allow ist die Hauptleistungseinschränkung für Mandanten mit vielen Clients.

Befolgung der Anweisungen

Gewichtung 10%
86

Folgt der Spezifikation genau: allow(tenant_id, client_id, now_ms) Signatur, retry_after_ms im Ergebnis, mandantenbezogene einstellbare Konfiguration, echtes protokollbasiertes gleitendes Fenster mit expliziter Diskussion von Genauigkeits-/Speicher-Kompromissen, faire Umverteilung unter Sättigung, Gleichlaufsicherheit, begrenzter Speicher, alle sechs Randfälle in Tests und explizite Annahmen plus ein Hinweis auf verteilte Erweiterungen. Im Wesentlichen sind alle im Prompt aufgeführten Liefergegenstände und Randfälle erfüllt.

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

Hilfsdatenklasse für die Mandantenkonfiguration

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:
"""
Ein threadsicherer, mandantenfähiger Rate Limiter, der einen Sliding-Window-Log-Algorithmus verwendet....

Vollständige Antwort anzeigen

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

Hilfsdatenklasse für die Mandantenkonfiguration

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:
"""
Ein threadsicherer, mandantenfähiger Rate Limiter, der einen Sliding-Window-Log-Algorithmus verwendet.

Diese Implementierung bietet eine Ratenbegrenzung pro Client unter einer globalen Obergrenze pro Mandant.
Sie verwendet eine Fair-Sharing-Richtlinie, bei der kein einzelner Client das Kontingent des Mandanten
aufgrund seines eigenen individuellen Limits ausschöpfen kann.

Algorithmus: Sliding Window Log
- Für jeden Client und Mandanten verwalten wir eine deque (einen listenähnlichen Container mit schnellem Anhängen
  und Entfernen von beiden Enden) von Zeitstempeln der innerhalb des Fensters empfangenen Anfragen.
- Wenn eine neue Anfrage eintrifft, verwerfen wir zuerst alle Zeitstempel, die älter sind als die aktuelle
  Zeit minus die Fenstergröße.
- Dann prüfen wir, ob die Anzahl der verbleibenden Zeitstempel kleiner ist als das konfigurierte Ratenlimit.
- Wenn die Anfrage erlaubt ist, wird der aktuelle Zeitstempel dem Log hinzugefügt.

Abwägungen:
- Genauigkeit: Dieser Algorithmus ist vollkommen genau. Er verfolgt die Anzahl der Anfragen
  innerhalb des präzisen gleitenden Fensters korrekt und vermeidet Probleme mit Fixed-Window-Zählern, bei denen
  Bursts an der Grenze von Fenstern die Rate überschreiten können.
- Speicherverbrauch: Der Speicherbedarf ist proportional zum Ratenlimit, da ein
  Zeitstempel pro erlaubter Anfrage innerhalb des Fensters gespeichert wird. Für einen Client mit einer Rate von 1000
  werden bis zu 1000 Zeitstempel gespeichert (ca. 8 KB pro Client). Dies kann für
  Systeme mit einer sehr großen Anzahl aktiver Clients oder sehr hohen Ratenlimits erheblich sein.

Annahmen:
- Single-Process-Bereitstellung: Der Zustand wird im Speicher innerhalb der Klasseninstanz gespeichert. Er ist
  threadsicher für die Verwendung innerhalb eines einzelnen Anwendungsprozesses.
- Monoton steigende Uhr: Es wird erwartet, dass der Parameter `now_ms` ein monoton
  steigender Zeitstempel in Millisekunden ist. Der Limiter schützt sich vor rückwärts laufender Zeit,
  indem er Anfragen mit Zeitstempeln ignoriert, die älter sind als der zuletzt verarbeitete.

Verteilte Erweiterung:
Um eine verteilte Umgebung zu unterstützen (z. B. mehrere API-Server), muss der Zustand in
einen zentralen, latenzarmen Speicher wie Redis ausgelagert werden.
- Zustandsspeicherung: Ein Redis Sorted Set könnte für jedes Client-/Mandanten-Log verwendet werden, wobei sowohl der
  Score als auch der Wert der Anfragezeitstempel sind. Dies ermöglicht effiziente Bereichsabfragen.
- Atomarität: Redis-Transaktionen (MULTI/EXEC) oder Lua-Skripte wären unerlässlich, um
  die Check-and-Update-Operation (alte Zeitstempel bereinigen, Anzahl prüfen, neuen Zeitstempel hinzufügen)
  atomar auszuführen und Race Conditions zwischen verschiedenen Servern zu verhindern.
- Entfernung: Redis' eingebaute Schlüsselablauffunktion (`EXPIRE`) würde die Entfernung
  veralteter Client-Daten auf natürliche Weise übernehmen.
"""

def __init__(self):
    self._tenants: Dict[str, Dict] = {}
    self._lock = threading.Lock()  # Globale Sperre zum Hinzufügen/Entfernen von Mandanten

def add_tenant(self, tenant_id: str, config: TenantConfig):
    """Fügt die Konfiguration eines Mandanten hinzu oder aktualisiert sie."""
    with self._lock:
        if tenant_id not in self._tenants:
            self._tenants[tenant_id] = {
                "config": config,
                "clients": {},
                "tenant_log": deque(),
                "lock": threading.Lock() # Sperre pro Mandant für Thread-Sicherheit
            }
        else:
            # Konfiguration im laufenden Betrieb aktualisieren
            self._tenants[tenant_id]['config'] = config

def allow(self, tenant_id: str, client_id: str, now_ms: Optional[int] = None) -> Tuple[bool, int]:
    """
    Bestimmt, ob eine Anfrage für einen gegebenen Mandanten und Client erlaubt ist.

    Argumente:
        tenant_id: Der Bezeichner für den Mandanten.
        client_id: Der Bezeichner für den Client.
        now_ms: Der aktuelle Zeitstempel in Millisekunden. Wenn None, wird die Systemzeit verwendet.

    Rückgabewerte:
        Ein Tupel (is_allowed, retry_after_ms).
        - is_allowed (bool): True, wenn die Anfrage zugelassen ist, andernfalls False.
        - retry_after_ms (int): Wenn abgelehnt, die Anzahl der Millisekunden, die gewartet werden soll, bevor die
          nächste Anfrage möglicherweise erlaubt wird. 0, wenn die Anfrage erlaubt ist.
    """
    if now_ms is None:
        now_ms = int(time.time() * 1000)

    with self._lock:
        if tenant_id not in self._tenants:
            raise ValueError(f"Mandant '{tenant_id}' ist nicht konfiguriert.")
        tenant_state = self._tenants[tenant_id]

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

        # 1. Client-Zustand abrufen oder erstellen
        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]

        # Gegen rückwärts laufende Uhr verteidigen
        if now_ms < client_state['last_seen_ms']:
            return (False, 1) # Ablehnen und eine kurze Wiederholungsverzögerung vorschlagen

        # 2. Limit pro Client bereinigen und prüfen
        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. Limit pro Mandant bereinigen und prüfen
        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. Wenn beide erlaubt sind, die Anfrage aufzeichnen und sie zulassen
        client_state['log'].append(now_ms)
        client_state['last_seen_ms'] = now_ms
        tenant_state['tenant_log'].append(now_ms)

        # 5. Opportunistische Bereinigung veralteter Clients für diesen Mandanten
        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]:
    """Hilfsmethode, um ein Zeitstempel-Log zu bereinigen und gegen ein Ratenlimit zu prüfen."""
    # Alte Zeitstempel bereinigen
    window_start_ms = now_ms - window_ms
    while log and log[0] <= window_start_ms:
        log.popleft()

    # Prüfen, ob das Limit überschritten ist
    if len(log) >= rate:
        oldest_timestamp = log[0]
        retry_after_ms = (oldest_timestamp + window_ms) - now_ms
        return (False, max(1, retry_after_ms)) # Sicherstellen, dass die Wiederholung mindestens 1 ms beträgt

    return (True, 0)

def _cleanup_stale_clients(self, tenant_state: Dict, now_ms: int):
    """
    Entfernt Clients, die lange Zeit inaktiv waren.
    Ein inaktiver Client ist einer, dessen letzte Anfrage älter ist als das Doppelte der Fenstergröße.
    Dies ist eine opportunistische Bereinigung, die inline mit einer Anfrage ausgeführt wird.
    """
    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]

--- Testsuite ---

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):
    """Ein einzelner Client sollte gedrosselt werden, nachdem er seine Rate überschritten hat."""
    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) # Sollte etwa 1000 ms - 5 ms betragen

def test_window_sliding(self):
    """Anfragen sollten wieder erlaubt werden, nachdem das Fenster vergangen ist."""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)

    # Diese Anfrage sollte abgelehnt werden
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 6)
    self.assertFalse(allowed)

    # Nachdem das Fenster für die erste Anfrage vergangen ist, ist eine neue erlaubt
    # Die erste Anfrage war bei self.now, das Fenster ist 1000 ms.
    # Bei self.now + 1001 läuft die erste Anfrage ab.
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 1001)
    self.assertTrue(allowed, "Die Anfrage sollte erlaubt sein, nachdem das Fenster weitergleitet")

def test_idle_client_returns(self):
    """Das Kontingent eines inaktiven Clients sollte frisch sein, wenn er zurückkehrt."""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)
    
    # Client bleibt länger als ein Fenster inaktiv
    new_time = self.now + 2000
    for i in range(5):
        allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + i)
        self.assertTrue(allowed, "Anfragen sollten für einen zurückkehrenden Client erlaubt sein")
    
    allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + 5)
    self.assertFalse(allowed, "Client sollte nach Nutzung des neuen Kontingents erneut gedrosselt werden")

def test_tenant_limit_and_fairness(self):
    """Mehrere Clients sollten gemeinsam das Mandantenlimit erreichen und damit Fairness demonstrieren."""
    # 3 Clients, jeder stellt 5 Anfragen. Insgesamt 15 Anfragen, was dem Mandantenlimit entspricht.
    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])

    # An diesem Punkt hat das Mandanten-Log 15 Anfragen. Die nächste Anfrage von einem beliebigen Client sollte fehlschlagen.
    self.assertFalse(self.limiter.allow('test_tenant', 'c4', self.now + 6)[0])
    self.assertFalse(self.limiter.allow('test_tenant', 'c1', self.now + 6)[0])

    # Fairness testen: Ein Client erreicht sein Limit, aber ein anderer kann fortfahren.
    limiter = SlidingWindowRateLimiter()
    limiter.add_tenant('fair_tenant', TenantConfig(client_rate=2, client_window_ms=1000, tenant_rate=10, tenant_window_ms=1000))
    # Hog-Client verbraucht sein Kontingent
    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]) # Hog ist blockiert

    # Ein anderer Client sollte weiterhin Anfragen stellen können
    self.assertTrue(limiter.allow('fair_tenant', 'fair_client', self.now + 3)[0])

def test_stale_client_eviction(self):
    """Veraltete Clients sollten aus dem Speicher entfernt werden."""
    self.limiter.allow('test_tenant', 'stale_client', self.now)
    self.assertIn('stale_client', self.limiter._tenants['test_tenant']['clients'])

    # Eine neue Anfrage weit in der Zukunft stellen, wodurch die Bereinigung ausgelöst wird
    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):
    """Sollte gleichzeitige Anfragen an denselben Client korrekt verarbeiten."""
    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) # kurze Pause, um die Wahrscheinlichkeit von Konkurrenz zu erhöhen

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

    # Die Anzahl der erlaubten Anfragen sollte genau dem Client-Ratenlimit entsprechen.
    self.assertEqual(allowed_count[0], 100)

def test_clock_regression(self):
    """Sollte Anfragen ablehnen, wenn die Uhr rückwärts läuft."""
    self.assertTrue(self.limiter.allow('test_tenant', 'client1', self.now + 100)[0])
    # Eine Anfrage mit einem früheren Zeitstempel sollte abgelehnt werden
    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()

Ergebnis

#2

Siegstimmen

0 / 3

Durchschnittsscore

50
Bewertungsmodelle Google Gemini 2.5 Flash

Gesamtpunktzahl

46

Gesamtkommentar

Antwort B implementiert einen grundlegenden Sliding-Window-Ratenbegrenzer mit Limits pro Client und pro Mandant. Sie versagt jedoch kritisch bei der Implementierung der Fair-Sharing-Richtlinie für die Mandantensättigung, die eine Kernanforderung war. Die Speicherbereinigung ist opportunistisch und weniger kontrolliert, und ihre Testsuite, obwohl vorhanden, deckt den Aspekt des Fair-Sharing, den sie zu adressieren behauptet, nicht ausreichend ab. Die Codequalität ist akzeptabel, aber weniger ausgefeilt als bei Antwort A.

Bewertungsdetails anzeigen

Korrektheit

Gewichtung 35%
40

Antwort B implementiert die Fair-Sharing-Richtlinie für die Mandantensättigung, eine wichtige funktionale Anforderung, nicht korrekt. Während grundlegende Client- und Mandantenlimits funktionieren, fehlt der Kernmechanismus für Fairness. Die Clock-Regression `retry_after_ms` ist ein minimales '1', was nicht sehr informativ ist.

Vollständigkeit

Gewichtung 20%
40

Antwort B ist aufgrund der fehlenden Fair-Sharing-Richtlinie unvollständig. Ihre Speicherbereinigung ist nur opportunistisch und verfügt über keinen dedizierten Sweep-Mechanismus, was sie weniger robust als erforderlich macht. Der `now_ms`-Parameter mit einem Standardwert ist eine geringfügige Abweichung von der Aufforderung.

Codequalität

Gewichtung 20%
65

Die Codequalität von Antwort B ist akzeptabel. Sie verwendet Dictionaries für den Zustand, was weniger strukturiert ist als Dataclasses. Die `allow`-Methode ist etwas lang, und die opportunistische Bereinigung ist weniger explizit. Die Verwendung von Mandanten-spezifischen Sperren ist ein gutes Muster, aber die Gesamtlösung ist weniger ausgefeilt.

Praktischer Nutzen

Gewichtung 15%
50

Antwort B hat aufgrund der fehlenden entscheidenden Fair-Sharing-Richtlinie einen begrenzten praktischen Wert für Multi-Tenant-Szenarien. Die opportunistische Bereinigung ist möglicherweise nicht für alle Produktionsanwendungsfälle ausreichend, und es gibt keine explizite Möglichkeit, eine vollständige Bereinigung auszulösen.

Befolgung der Anweisungen

Gewichtung 10%
30

Antwort B ignoriert eine kritische Anweisung: die Implementierung der Fair-Sharing-Richtlinie für die Mandantensättigung. Ihr Bereinigungsmechanismus ist weniger robust als spezifiziert, und der Test für 'tenant_limit_and_fairness' testet nicht die faire Umverteilung unter konkurrierenden Clients, wenn der Mandant gesättigt ist.

Bewertungsmodelle OpenAI GPT-5.5

Gesamtpunktzahl

48

Gesamtkommentar

Antwort B implementiert einen grundlegenden Thread-sicheren Sliding-Window-Log-Begrenzer mit pro-Client- und pro-Tenant-Obergrenzen, Dokumentation und einigen Tests. Es implementiert jedoch keine echte faire Umverteilung, wenn ein Mandant gesättigt ist, hat eine schwache und teilweise fehlerhafte Behandlung von Zeitreihen-Regressionen über Mandanten/Clients hinweg, die Bereinigung ist opportunistisch und unvollständig, und die Tests decken mehrere erforderliche Randfälle nicht rigoros ab. Es ist ein vernünftiger einfacher Begrenzer, aber unvollständig für die Benchmark-Aufgabe.

Bewertungsdetails anzeigen

Korrektheit

Gewichtung 35%
45

Korrekt für einfache pro-Client- und Mandanten-Sliding-Window-Prüfungen im normalen monotonen Gebrauch, aber es fehlt eine echte faire gemeinsame Nutzung bei Mandantensättigung. Die Zeitbehandlung erfolgt nur pro Client und kann dazu führen, dass das Mandantenprotokoll über Clients hinweg nicht in Zeitstempelreihenfolge ist, was die Annahmen für das Beschneiden/erneute Versuchen bricht. Es verweigert auch rückwärts gerichtete Zeitstempel anstatt sie zu normalisieren und hat schwächere Bereinigungssemantiken.

Vollständigkeit

Gewichtung 20%
42

Enthält eine API, Konfiguration, gleitende Protokolle, Mandantenobergrenze, Erklärung und einige Tests, aber die zentrale Anforderung der fairen Umverteilung fehlt. Tests prüfen das exakte Grenzverhalten, doppelte Zeitstempel, echte Mandantensättigungsfairness oder die sichere Auslagerung aktiver Clients nicht explizit in der Tiefe.

Codequalität

Gewichtung 20%
58

Lesbar und kompakt, mit einfachen Hilfsmethoden und Kommentaren. Es stützt sich jedoch auf lose typisierte Dictionaries für den Zustand, hat eine weniger robuste Synchronisation bei Konfigurationsaktualisierungen, eine schwächere Validierung und ein einfacheres Design für Bereinigung/Fairness, als die Aufgabe erfordert.

Praktischer Nutzen

Gewichtung 15%
50

Nützlich für einen kleinen Single-Prozess-Dienst, der grundlegende Ratenbegrenzung benötigt, und die Diskussion über die Redis-Erweiterung ist vernünftig. Sein Mangel an Sättigungsfairness für Mandanten und die fragile Zeitbehandlung machen es weniger geeignet für das beschriebene Multi-Tenant-Quota-Szenario.

Befolgung der Anweisungen

Gewichtung 10%
50

Wählt eine erlaubte Sprache und liefert Code, Kommentare und Tests, erfüllt aber nicht die wichtige Anforderung der fairen gemeinsamen Nutzung und adressiert nur teilweise mehrere erforderliche Randfälle. Die behauptete Fairness-Richtlinie in der Prosa wird nicht tatsächlich implementiert.

Bewertungsmodelle Anthropic Claude Fable 5

Gesamtpunktzahl

55

Gesamtkommentar

Antwort B liefert einen sauberen, lesbaren Sliding-Window-Log-Begrenzer mit Mandanten- und Mandantenkappen, zweistufiger Sperrung, opportunistischer Verdrängung und guter Dokumentation von Kompromissen, Annahmen und einer Redis-basierten verteilten Erweiterung. Es fehlt jedoch die Anforderung der fairen Neuverteilung vollständig – bei Mandantensättigung kann jeder Client frei gewordene Kapazitäten monopolisieren – und sein 'Fairness'-Test zeigt nur unabhängige Mandantenlimits pro Client, was die Bewertungsrichtlinie explizit als unvollständig bezeichnet. Weitere Probleme sind die nur pro Client rückwärts gerichtete Zeitstempelverteidigung, die den gemeinsamen Mandanten-Deque ungeordnet lassen kann, die Bereinigung, die nur bei erlaubten Anfragen ausgeführt wird (Risiko des Speicherwachstums bei anhaltender Sättigung), kein expliziter Test für exakte Grenzen und ein Nebenläufigkeitstest, der an die reale Wandzeit gebunden ist.

Bewertungsdetails anzeigen

Korrektheit

Gewichtung 35%
53

Die Sliding-Window-Logs pro Client und pro Mandant sind isoliert korrekt, und die Randbeschneidung verwendet konsistent <=. Die Fairness von Anforderung 4 ist jedoch nicht implementiert: Bei Mandantensättigung wird jeder Client identisch abgewiesen, und der erste Client, der nach Ablauf erneut versucht, kann die frei gewordene Kapazität monopolisieren; es gibt keinen Umverteilungsmechanismus. Die Rückwärts-Zeitstempelverteidigung ist nur pro Client, sodass Zeitstempel außerhalb der Reihenfolge zwischen verschiedenen Clients ungeordnete Einträge in den gemeinsamen Mandanten-Deque einfügen können, was die Ordnungsannahme der Prune-Schleife verletzt. Die Bereinigung veralteter Clients läuft nur im erfolgreichen Pfad, sodass sich unter anhaltender Mandantensättigung verweigerte Clients unendlich ansammeln, was in diesem Szenario die Anforderung an begrenzten Speicher verletzt.

Vollständigkeit

Gewichtung 20%
50

Behandelt pro Client-Fenster, Mandantenkappe, Threading, Verdrängung, Algorithmus-Kompromisse, Annahmen und einen Entwurf für eine verteilte Erweiterung. Aber der zentrale Unterscheidungsmerkmal – faire Neuverteilung bei Mandantensättigung – fehlt, und der 'Fairness'-Test zeigt nur, dass das Limit eines Clients pro Client einen anderen Client nicht blockiert, was nur eine unabhängige Begrenzung ist. Es gibt keinen expliziten Test für exakte Grenzen (das Gleiten des Fensters wird bei +1001 getestet, nicht an der Grenze selbst), und die Behandlung von doppelten Zeitstempeln ist nur implizit. Gemäß der Bewertungsrichtlinie sind Lösungen mit nur unabhängigen Limits pro Client unvollständig.

Codequalität

Gewichtung 20%
64

Lesbarer Code mit einer hilfreichen Klassen-Docstring, die den Algorithmus, Kompromisse, Annahmen und die Redis-Erweiterung erklärt, sowie einem sinnvollen _check_limit-Helfer. Der Zustand wird jedoch als verschachtelte, untypisierte Dictionaries von Dictionaries modelliert, anstatt als Dataklassen. Tests greifen direkt auf private Attribute (_tenants) zu. Der Nebenläufigkeitstest hängt von der realen Wandzeit ab und verwendet Sleeps (potenziell fehleranfällig), und es gibt keine Konfigurationsvalidierung. Das zweistufige Sperrschema ist vernünftig, aber die Konfigurations-/Tuple-basierte API ist weniger poliert.

Praktischer Nutzen

Gewichtung 15%
56

Nutzbar für grundlegende Limits pro Client plus Mandantenkappe, und die Hinweise zur Redis-Erweiterung sind praktisch. Aber in der Produktion bedeutet die fehlende faire Aufteilung, dass ein aggressiver Client die gesamte frei gewordene Mandantenkapazität beanspruchen kann. Die Reaktion auf Zeitstempel-Regression (pauschale Ablehnung mit Wiederholungsversuch 1) kann legitimen Traffic bei geringfügiger Zeitstempelabweichung ablehnen, und die Überspringung der Bereinigung bei abgelehnten Anfragen birgt das Risiko von Speicherwachstum genau dann, wenn ein Mandant angegriffen wird – das Szenario, das ein Ratenbegrenzer am ehesten überstehen muss.

Befolgung der Anweisungen

Gewichtung 10%
54

Entspricht der API-Form, liefert ein echtes Sliding-Window-Log, gibt Annahmen an, diskutiert Kompromisse und skizziert das verteilte Design wie gefordert. Es scheitert jedoch am Kernauftrag von Anforderung 4, dass die verbleibende Mandantenkapazität bei Sättigung fair unter den aktiven Clients aufgeteilt wird, und der entsprechende Grenzfall (Mandantensättigung und faire Neuverteilung) ist weder implementiert noch wirklich getestet, was eine explizite Anweisungsverfehlung darstellt.

Vergleichsübersicht

Für jede Aufgabe und Diskussion wird die Endrangfolge per Richter-Rangaggregation bestimmt (Durchschnittsrang + Borda-Tie-Break). Der Durchschnittsscore wird als Referenz angezeigt.

Bewerter: 3

Siegstimmen

3 / 3

Durchschnittsscore

86
Diese Antwort ansehen

Siegstimmen

0 / 3

Durchschnittsscore

50
Diese Antwort ansehen

Bewertungsergebnisse

Bewertungsmodelle Anthropic Claude Fable 5

Warum diese Seite gewann

Antwort A gewinnt aufgrund des gewichteten Ergebnisses eindeutig. Sie führt bei der Korrektheit (35 %) mit einem exakten, grenzwertig getesteten gleitenden Fenster, einer geklemmten Uhrhandhabung und einem verifizierten atomaren Nebenläufigkeitsmodell, während B einen Fehler bei der Reihenfolge der Mandantenprotokolle und ein sättigungsabhängiges Speicherwachstum aufweist. A führt auch bei der Vollständigkeit (20 %) und der Befolgung von Anweisungen (10 %), da sie die Anforderung der fairen Neuverteilung implementiert und testet, die die Bewertungsrichtlinie als wichtigen Unterscheidungsmerkmal nennt, während B nur unabhängige Grenzwerte pro Client implementiert, die die Richtlinie ausdrücklich als unvollständig bezeichnet. A führt ferner bei der Codequalität (20 %) und dem praktischen Wert (15 %). Da A bei jedem Kriterium höher abschneidet, begünstigt die gewichtete Gesamtsumme eindeutig A.

Bewertungsmodelle OpenAI GPT-5.5

Warum diese Seite gewann

Antwort A gewinnt, da sie die stark gewichteten Korrektheitsanforderungen weitaus besser erfüllt: Sie verwendet ein echtes Sliding-Window-Protokoll sowohl für Client- als auch für Mandantenlimits, berechnet sinnvolle Wiederholungszeiten, behandelt Grenzwertüberschreitungen, begrenzt die Rückwärtszeit, schützt den Zustand mit einem Sperrmechanismus, wirft veraltete Zustände aus und implementiert einen expliziten FIFO-Mechanismus für faire Freigabe für gesättigte Mandanten. Antwort B mangelt es an dem entscheidenden Verhalten der fairen Freigabe, sie weist problematische zeitliche Abstimmungen zwischen Clients unter Berücksichtigung von Zeitkorrekturen auf und deckt die erforderlichen Randfälle nur teilweise ab.

Bewertungsmodelle Google Gemini 2.5 Flash

Warum diese Seite gewann

Antwort A ist der klare Gewinner, da sie alle funktionalen Anforderungen vollständig und korrekt umsetzt, insbesondere die anspruchsvolle Fair-Sharing-Richtlinie für die Mietersättigung, die Antwort B vollständig vermisst. Antwort A weist außerdem eine überlegene Codequalität, ein robusteres Speichermanagement und eine umfassendere Testsuite auf, die alle spezifizierten Randfälle wirklich abdeckt. Das fehlende Fair-Sharing in Antwort B ist ein kritischer funktionaler Fehler, der seine Korrektheit und Vollständigkeit erheblich beeinträchtigt.

X f L