Resposta 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...
Mostrar resposta completa ▼
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()
Resultado
Votos de vitória
3 / 3
Pontuação média
Pontuação total
Comentário geral
A Resposta A fornece uma implementação abrangente e robusta do limitador de taxa, abordando totalmente todos os requisitos funcionais, incluindo a complexa política de compartilhamento justo para saturação de inquilinos. O código é bem estruturado, usa estruturas de dados apropriadas e inclui documentação completa e um forte conjunto de testes cobrindo todos os casos extremos especificados. Seu gerenciamento de memória por meio de varredura explícita e descarte configurável é bem projetado.
Ver detalhes da avaliação ▼
Correção
Peso 35%A Resposta A implementa corretamente todos os aspectos do limitador de taxa, incluindo a complexa política de compartilhamento justo para saturação de inquilinos e o tratamento robusto de relógios. Todos os limites e a lógica da janela são precisos.
Completude
Peso 20%A Resposta A é altamente completa, abordando todos os requisitos funcionais, incluindo o compartilhamento justo sutil e o descarte abrangente de memória. Ela fornece explicações claras, suposições e notas de design distribuído conforme solicitado.
Qualidade do código
Peso 20%O código na Resposta A é bem estruturado, usa `dataclasses` para gerenciamento claro de estado e tem boa documentação inline. O uso de `threading.RLock` é apropriado para operações reentrantes. Os métodos são logicamente separados e limpos.
Valor prático
Peso 15%A Resposta A oferece alto valor prático devido ao seu conjunto completo de recursos, especialmente o mecanismo de compartilhamento justo e o descarte de memória controlado por meio do método `sweep`. O método `debug_state` também é uma adição útil para monitoramento.
Seguimento de instruções
Peso 10%A Resposta A segue meticulosamente todas as instruções, incluindo a complexa política de compartilhamento justo, janela deslizante verdadeira, segurança de concorrência, limite de memória e tratamento de todos os casos extremos especificados em código e testes. Todos os entregáveis são fornecidos.
Pontuação total
Comentário geral
A Resposta A fornece uma implementação Python substancialmente completa e executável com janelas deslizantes exatas baseadas em logs, configuração por inquilino, limites por inquilino, proteção de concorrência, limpeza de estado obsoleto, cálculos de retentativa, tratamento explícito de regressão de relógio e um amplo conjunto de testes unitários. Seu mecanismo de espera FIFO é uma política de compartilhamento justo crível sob saturação de inquilino, embora retry_after_ms para negações de fila de espera possa ser impreciso e o estado do cliente/espera para muitos clientes únicos negados ainda possa crescer até o tempo limite ou a varredura. No geral, corresponde de perto ao comportamento e aos casos extremos da biblioteca solicitada.
Ver detalhes da avaliação ▼
Correção
Peso 35%Usa logs de timestamp exatos com poda de limite correta, impõe limites por cliente e por inquilino, limita o tempo para trás e serializa mutações com segurança. A lógica de justiça FIFO é em grande parte correta, mas retry_after_ms para negações de justiça não principais pode ser 0 e a memória para muitos clientes negados em espera é limitada apenas ao longo do tempo, em vez de imediatamente.
Completude
Peso 20%Cobre a API solicitada, configuração ajustável por inquilino, janelas deslizantes exatas, limite de inquilino, fila de justiça, segurança de concorrência, evacuação/varredura, suposições, notas de extensão distribuída e testes para quase todos os casos extremos especificados. Lacunas menores incluem relatórios de retentativa de justiça imperfeitos e código de teste de evacuação um tanto desajeitado.
Qualidade do código
Peso 20%Dataclasses bem estruturadas, métodos públicos claros, validação, documentação inline, dicas de tipo e métodos auxiliares encapsulados. O único bloqueio global é simples e seguro, mas limita a escalabilidade, e alguns internos/testes são um tanto desajeitados.
Valor prático
Peso 15%Prático como uma biblioteca reutilizável de processo único com monitoramento/estado de depuração, varredura periódica, inquilinos configuráveis e um caminho crível para Redis/Lua para uso distribuído. O bloqueio global e o custo de memória do log de timestamp podem limitar implantações de alta escala, mas as compensações são declaradas.
Seguimento de instruções
Peso 10%Segue de perto os entregáveis solicitados: código completo, algoritmo e compensações documentados, suposições explícitas, extensão distribuída e testes para os casos extremos nomeados. A API retorna um AllowResult em vez de uma tupla, o que é aceitável sob o prompt.
Pontuação total
Comentário geral
A Resposta A é uma solução quase completa e de qualidade de produção. Ela implementa uma janela deslizante exata baseada em log com semântica de limite correta, um mecanismo genuíno de redistribuição justa através de uma fila de espera FIFO com tempos limite de atividade, tratamento de relógios retroativos e duplicados limitado por inquilino, um único RLock para atomicidade e memória limitada através de descarte interno mais uma API explícita de varredura. Todos os seis casos de borda obrigatórios têm testes direcionados, e a docstring explica os trade-offs de precisão/memória, suposições e uma extensão distribuída concreta. As fraquezas são menores: uma asserção de teste é vazia devido a um ternário confuso, retry_after_ms de 0 em uma negação de justiça é ambíguo, e as varreduras de descarte por chamada adicionam sobrecarga para inquilinos com muitos clientes.
Ver detalhes da avaliação ▼
Correção
Peso 35%A janela deslizante baseada em log é exata: a poda usa t <= agora - janela para que uma solicitação com exatamente uma janela completa de idade expire, e o teste de limite verifica retry_after_ms de 1 em t=999 e admissão em t=1000. Relógios retroativos/duplicados são limitados ao maior carimbo de data/hora observado do inquilino, evitando que o histórico expirado reviva. A fila de espera FIFO comprovadamente impõe redistribuição round-robin sob saturação de inquilino (verificado por rastreamento do teste de saturação: o cliente a é bloqueado enquanto os clientes em fila b e c recebem um slot liberado). O único RLock torna a permissão atômica, e o teste de 40 threads admite exatamente 10. Falhas menores: retry_after_ms de 0 para uma negação de justiça é ambíguo, e um teste de descarte contém uma asserção condicional vazia que nunca exerce a chamada de permissão.
Completude
Peso 20%Todos os requisitos funcionais são abordados: configuração por inquilino, janela deslizante exata, limite global do inquilino com redistribuição justa FIFO genuína, segurança de thread, descarte por limpeza interna e uma API de varredura explícita, mais um auxiliar debug_state. Todos os seis casos de borda obrigatórios têm testes dedicados (limite exato, retorno ocioso, corrida concorrente, relógio retroativo/duplicado limitado, saturação com redistribuição justa, descarte obsoleto preservando clientes ativos), e ele cobre adicionalmente turnos de espera abandonados e limites de memória. Suposições e uma extensão distribuída Redis/Lua são documentadas na docstring.
Qualidade do código
Peso 20%Bem estruturado com dataclasses congeladas para configuração e resultados, configuração validada, assinaturas tipadas, separação clara de funções de poda, purga de espera e auxiliares de descarte, e uma docstring de classe incomumente completa cobrindo algoritmo, semântica de justiça, comportamento do relógio e extensão distribuída. Fraquezas: o teste de descarte contém uma asserção ternária vazia e confusa, o descarte de cliente completo por chamada adiciona sobrecarga O(clientes) a cada permissão, e _remove_waiter reconstrói o deque em O(n).
Valor prático
Peso 15%Implementável como está para um único processo: carimbos de data/hora injetados determinísticos o tornam testável, debug_state suporta monitoramento, sweep dá aos operadores controle explícito de memória, tempos limite de espera evitam que clientes abandonados esgotem um inquilino, e o caminho de migração distribuída (conjuntos ordenados Redis mais Lua, com metadados de espera na mesma transação) é concretamente descrito. A varredura de descarte por chamada é a principal ressalva de desempenho para inquilinos com muitos clientes.
Seguimento de instruções
Peso 10%Segue a especificação de perto: assinatura allow(tenant_id, client_id, now_ms), retry_after_ms no resultado, configuração ajustável por inquilino, janela deslizante baseada em log verdadeira com discussão explícita de trade-off de precisão/memória, redistribuição justa sob saturação, segurança de concorrência, memória limitada, todos os seis casos de borda em testes, e suposições explícitas mais uma nota de extensão distribuída. Essencialmente, todos os entregáveis e casos de borda listados no prompt são satisfeitos.