Orivel Orivel
メニューを開く

スライディングウィンドウと公平なマルチテナント割当を備えたレートリミッタ

このプログラミングベンチマークに対する各AIの回答と比較結果を確認できます。

いいね・お気に入り機能を使うにはログインまたは新規登録が必要です。 新規登録

X f L

目次

お題概要

比較ジャンル

プログラミング

お題作成モデル

回答モデル

採点モデル

お題本文

任意の言語(Python、Go、TypeScript、Java、または Rust)で再利用可能なレートリミッタライブラリを実装してください。このライブラリはスライディングウィンドウアルゴリズムを使用してクライアントごとのリクエストクォータを強制し、かつ複数テナント間での公平な共有ポリシーを提供する必要があります。

機能要件:

  1. allow(tenant_id, client_id, now_ms) のようなメソッドを持つクラスまたはモジュールを提供し、リクエストが許可されるかどうかを返し、拒否された場合は次にリクエストが許可されるまでのミリ秒数(retry_after_ms)も返すこと。
    2....
さらに表示

任意の言語(Python、Go、TypeScript、Java、または Rust)で再利用可能なレートリミッタライブラリを実装してください。このライブラリはスライディングウィンドウアルゴリズムを使用してクライアントごとのリクエストクォータを強制し、かつ複数テナント間での公平な共有ポリシーを提供する必要があります。

機能要件:

  1. allow(tenant_id, client_id, now_ms) のようなメソッドを持つクラスまたはモジュールを提供し、リクエストが許可されるかどうかを返し、拒否された場合は次にリクエストが許可されるまでのミリ秒数(retry_after_ms)も返すこと。
  2. 各クライアントはローリングウィンドウ内での最大リクエスト数に制限される(例:60,000 ms あたり 100 リクエスト)。この構成はテナントごとに調整可能であること。
  3. 固定のカレンダーバケットウィンドウではなく、真のスライディングウィンドウ(重み付けまたはタイムスタンプログベース)を実装し、バケット境界を跨ぐバーストを正しく扱うこと。
  4. テナント全体の上限(per-tenant global cap)を追加し、テナント内のすべてのクライアントの合計がテナントレベルの上限を超えないようにすること。テナントが飽和している場合、残りの容量は1つのクライアントに独占されるのではなく、アクティブなクライアント間で公平に共有されること。
  5. リミッタは複数のスレッドや非同期タスクからの同時アクセスに対して安全であること。
  6. メモリが無制限に増加しないこと:古いクライアント状態は時間経過で削除またはコンパクト化されること。

成果物:

  • 明確な公開APIと重要な設計判断に関するインラインドキュメントを含む完全な実装。
  • 選択したスライディングウィンドウアルゴリズムとその精度/メモリトレードオフに関する簡潔な説明(コメント内または短い本文セクション)。
  • 以下のコアエッジケースをカバーするテストスイート。

コードとテストで明示的に対処すべきエッジケース:

  • ウィンドウの境界でちょうど発生するリクエスト。
  • クライアントがアイドル状態になり、その後ウィンドウが完全に経過した後に戻ってくる場合。
  • 同じクライアントカウンタに対する同時リクエストの競合。
  • 時計が後退する場合や重複タイムスタンプ。
  • テナントの飽和と競合するクライアント間での公平な再分配。
  • アクティブなクライアントを落とさずに古いクライアント状態を削除すること。

仮定を明記してください(単一プロセス対分散、単調クロックの利用可能性など)。単一プロセスを仮定する場合は、設計を分散デプロイに拡張する方法を簡単に説明してください。

採点方針

強い回答は、正しく実行可能なコード、明瞭な公開API、そしてクライアントごとのスライディングウィンドウ制限とテナントごとのグローバル上限の両方を明確に強制する実装を提供します。評価者は、単純な固定バケット近似ではなく真のスライディングウィンドウ実装(重み付けカウンタまたはタイムスタンプログ)を評価すべきであり、retry_after_ms の値が合理的に計算されていることを確認すべきです。テナントが飽和した際に残りのテナント容量をアクティブクライアント間で公平に再分配する挙動は重要な差別化要素であり、独立したクライアントごとの制限の...

さらに表示

強い回答は、正しく実行可能なコード、明瞭な公開API、そしてクライアントごとのスライディングウィンドウ制限とテナントごとのグローバル上限の両方を明確に強制する実装を提供します。評価者は、単純な固定バケット近似ではなく真のスライディングウィンドウ実装(重み付けカウンタまたはタイムスタンプログ)を評価すべきであり、retry_after_ms の値が合理的に計算されていることを確認すべきです。テナントが飽和した際に残りのテナント容量をアクティブクライアント間で公平に再分配する挙動は重要な差別化要素であり、独立したクライアントごとの制限のみを実装する解決策は不完全と見なされます。並行性の安全性は(ロック、アトミック操作、あるいはアクタ/単一ループモデルなどを用いて)明確に扱われ、明らかな競合状態がないことを確認する必要があります。また、設計は古い状態の削除やコンパクションを通じてメモリを有界に保つ必要があります。記載されたエッジケースはコードロジックとテストの両方で扱われているべきです:境界時のタイミング、アイドル後の復帰動作、同時競合、時計の後退や重複タイムスタンプ、テナント飽和、および安全なエビクション。高品質な回答はアルゴリズムの精度/メモリのトレードオフを説明し、仮定を明確に記載し、設計を分散環境にどのように拡張するかを示します。境界処理の誤り、無制限のメモリ増加、並行性保護の欠如、実際のエッジケースを検証していないテスト、またはそのままでは実行できないコードに対しては減点します。

回答

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...
全文を表示

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

判定

1位 | 勝者

勝利票

3 / 3

平均スコア

86

総合点

91

総評

回答Aは、レートリミッターの包括的かつ堅牢な実装を提供しており、特にテナント飽和のための複雑なフェアシェアリングポリシーを含む、すべての機能要件に完全に対応しています。コードはよく構造化されており、適切なデータ構造を使用し、徹底したドキュメントと指定されたすべてのエッジケースをカバーする強力なテストスイートが含まれています。明示的なスイープと設定可能なエビクションによるメモリ管理はよく設計されています。

採点詳細を表示

正確さ

重み 35%
90

回答Aは、特にテナント飽和のための複雑なフェアシェアリングポリシーや堅牢なクロック処理を含む、レートリミッターのすべての側面を正しく実装しています。すべての制限とウィンドウロジックは正確です。

完全性

重み 20%
95

回答Aは、フェアシェアリングと包括的なメモリ管理のエビクションを含む、すべての機能要件に対応しており、非常に包括的です。要求されたとおり、明確な説明、仮定、および分散設計に関する注記を提供しています。

コード品質

重み 20%
85

回答Aのコードは、明確な状態管理のための`dataclasses`を使用し、よく構造化されており、優れたインラインドキュメントを備えています。`threading.RLock`の使用は、再入可能な操作に適しています。メソッドは論理的に分離されており、クリーンです。

実用性

重み 15%
90

回答Aは、特にフェアシェアリングメカニズムと`sweep`メソッドによる制御されたメモリ管理のエビクションなど、完全な機能セットにより、高い実用価値を提供します。`debug_state`メソッドも、監視のための便利な追加機能です。

指示遵守

重み 10%
95

回答Aは、複雑なフェアシェアリングポリシー、真のスライディングウィンドウ、並行性安全性、メモリバウンディング、およびコードとテストの両方で指定されたすべてのエッジケースへの対応を含む、すべての指示を綿密にフォローしています。すべての成果物が提供されています。

採点モデル OpenAI GPT-5.5

総合点

83

総評

回答Aは、ログベースの正確なスライディングウィンドウ、テナントごとの設定、テナント全体のキャップ、同時実行保護、古い状態のクリーンアップ、リトライ計算、明示的なクロック回帰処理、および広範な単体テストスイートを備えた、実質的に完全で実行可能なPython実装を提供します。そのFIFOウェイターメカニズムは、テナントの飽和状態下での信頼できる公平共有ポリシーですが、公平性キュー拒否のretry_after_msは不正確になる可能性があり、多数の拒否されたユニーククライアントのウェイター/クライアント状態は、タイムアウトまたはスイープまで成長する可能性があります。全体として、要求されたライブラリの動作とエッジケースに密接に一致しています。

採点詳細を表示

正確さ

重み 35%
83

正確なタイムスタンプログと正しい境界プルーニングを使用し、クライアントごとおよびテナントごとの両方のキャップを強制し、過去の時間をクランプし、ミューテーションを安全にシリアル化します。FIFO公平性ロジックはほとんど正しいですが、ヘッド以外の公平性拒否に対するretry_after_msは0になる可能性があり、多数の待機中の拒否されたクライアントのメモリは、即時にはバウンドされず、時間とともにバウンドされます。

完全性

重み 20%
86

要求されたAPI、テナントごとの調整可能な設定、正確なスライディングウィンドウ、テナントキャップ、公平性キュー、同時実行安全性、エビクション/スイープ、仮定、分散拡張に関する注記、および指定されたエッジケースのほぼすべてに対するテストをカバーしています。マイナーなギャップには、不完全な公平性リトライレポートと、やや扱いにくいエビクションテストコードが含まれます。

コード品質

重み 20%
80

構造化されたデータクラス、明確な公開メソッド、検証、インラインドキュメント、型ヒント、およびカプセル化されたヘルパーメソッド。単一のグローバルロックはシンプルで安全ですが、スケーラビリティを制限します。また、いくつかの内部/テストはやや扱いにくいです。

実用性

重み 15%
81

監視/デバッグ状態、定期的なスイープ、設定可能なテナント、およびRedis/Luaへの分散使用のための信頼できるパスを備えた、再利用可能な単一プロセスのライブラリとして実用的です。グローバルロックとタイムスタンプログのメモリコストは、高スケールのデプロイメントを制限する可能性がありますが、トレードオフは述べられています。

指示遵守

重み 10%
90

要求された成果物(完全なコード、文書化されたアルゴリズムとトレードオフ、明示的な仮定、分散拡張、および指定されたエッジケースのテスト)に密接に従っています。APIはタプルの代わりにAllowResultを返しますが、これはプロンプトの下で許容されます。

総合点

85

総評

回答Aは、ほぼ完全で本番品質のソリューションです。正確なログベースのスライディングウィンドウを境界セマンティクスで実装し、FIFO待機者キューによる真の公平再分配メカニズム(アクティビティタイムアウト付き)、テナントで制限された過去および重複クロックの処理、アトミック性のための単一RLock、インライン削除と明示的なスイープAPIによるバウンドメモリを備えています。指定された6つのエッジケースすべてにターゲットテストがあり、ドキュメンテーションには精度/メモリのトレードオフ、仮定、具体的な分散拡張が説明されています。弱点は軽微です。1つのテストアサーションは、混乱した三項演算子により空虚であり、公平性拒否時のretry_after_msが0であることは曖昧であり、呼び出しごとの削除スキャンは、多くのクライアントを持つテナントのオーバーヘッドを追加します。

採点詳細を表示

正確さ

重み 35%
86

ログベースのスライディングウィンドウは正確です。削除はt <= now - windowを使用するため、1ウィンドウ古いリクエストが期限切れになり、境界テストはt=999でretry_after_ms=1、t=1000で許可を検証します。過去/重複クロックは、テナントの最大観測タイムスタンプに制限され、期限切れの履歴が復活するのを防ぎます。FIFO待機者キューは、テナント飽和下でのラウンドロビン再分配を証明的に強制します(飽和テストのトレースによって検証されます:クライアントaはキューに入れられたクライアントbとcがそれぞれ解放されたスロットを取得する間にブロックされます)。単一のRLockによりallowはアトミックになり、40スレッドテストは正確に10を許可します。軽微な欠陥:公平性拒否に対するretry_after_ms=0は曖昧であり、1つの削除テストには、allow呼び出しを実際に実行しない空虚な条件付きアサーションが含まれています。

完全性

重み 20%
87

すべての機能要件が満たされています。テナントごとの設定、正確なスライディングウィンドウ、真のFIFO公平再分配によるテナントグローバルキャップ、スレッドセーフティ、インラインクリーンアップと明示的なスイープAPIによる削除、およびデバッグ状態ヘルパーが含まれています。指定された6つのエッジケースすべてに専用テストがあります(正確な境界、アイドル返却、同時レース、制限された過去/重複クロック、公平再分配による飽和、アクティブクライアントを維持する古い削除)。さらに、放棄された待機者ターンとメモリバウンドもカバーしています。仮定とRedis/Lua分散拡張はドキュメンテーションに記載されています。

コード品質

重み 20%
82

設定と結果のためのフローズンデータクラス、検証済みの設定、型付きシグネチャ、削除、待機者パージ、削除ヘルパーの明確な分離、アルゴリズム、公平性セマンティクス、クロック動作、分散拡張を網羅する異常に徹底的なクラスドキュメンテーションにより、よく構造化されています。弱点:削除テストには、混乱した空虚な三項演算子アサーションが含まれています。呼び出しごとのフルクライアント削除は、すべての許可にO(クライアント)のオーバーヘッドを追加します。_remove_waiterはdequeをO(n)で再構築します。

実用性

重み 15%
81

単一プロセスとしてそのままデプロイ可能です。決定論的な注入タイムスタンプによりテスト可能であり、debug_stateは監視をサポートし、sweepはオペレーターに明示的なメモリ制御を提供し、待機者タイムアウトは放棄されたクライアントがテナントを枯渇させるのを防ぎます。分散移行パス(RedisソートセットとLua、同じトランザクション内の待機者メタデータ付き)は具体的に説明されています。呼び出しごとの削除スキャンは、多くのクライアントを持つテナントにとって主なパフォーマンス上の注意点です。

指示遵守

重み 10%
86

仕様に厳密に従っています。allow(tenant_id, client_id, now_ms)シグネチャ、結果のretry_after_ms、テナントごとの調整可能な設定、明示的な精度/メモリトレードオフの議論を伴う真のログベースのスライディングウィンドウ、飽和下での公平再分配、同時実行安全性、バウンドメモリ、テスト内のすべての6つのエッジケース、および明示的な仮定と分散拡張の注記が含まれています。プロンプトに記載されているほぼすべての成果物とエッジケースが満たされています。

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

テナント設定用のヘルパーデータクラス

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:
"""
スライディングウィンドウログアルゴリズムを使用する、スレッドセーフなマルチテナントのレートリミッター。

この実装...
全文を表示

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

テナント設定用のヘルパーデータクラス

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:
"""
スライディングウィンドウログアルゴリズムを使用する、スレッドセーフなマルチテナントのレートリミッター。

この実装は、テナント全体の上限の下でクライアントごとのレート制限を提供します。
各クライアント自身の個別制限により、単一のクライアントがテナントの割り当てを
使い果たすことがないようにする、公平共有ポリシーを使用します。

アルゴリズム: スライディングウィンドウログ
- 各クライアントおよびテナントについて、ウィンドウ内に受信したリクエストのタイムスタンプを格納する
  deque(両端からの追加と削除が高速なリスト状コンテナ)を維持します。
- 新しいリクエストが到着したら、まず現在時刻からウィンドウサイズを引いた時刻より古い
  すべてのタイムスタンプを破棄します。
- 次に、残っているタイムスタンプの数が設定されたレート制限未満かどうかを確認します。
- リクエストが許可される場合、現在のタイムスタンプをログに追加します。

トレードオフ:
- 正確性: このアルゴリズムは完全に正確です。厳密なスライディングウィンドウ内のリクエスト数を
  正しく追跡し、ウィンドウ境界でのバーストによってレートを超過し得る固定ウィンドウカウンターの
  問題を回避します。
- メモリ使用量: ウィンドウ内で許可されたリクエストごとに1つのタイムスタンプを保存するため、
  メモリ使用量はレート制限に比例します。レートが1000のクライアントでは、最大1000個の
  タイムスタンプ(クライアントあたり約8KB)を保存します。これは、非常に多数のアクティブクライアントや
  非常に高いレート制限を持つシステムでは大きな負担になり得ます。

前提:
- 単一プロセスでのデプロイ: 状態はクラスインスタンス内のメモリに保存されます。単一の
  アプリケーションプロセス内での使用に対してスレッドセーフです。
- 単調増加するクロック: `now_ms` パラメータは、ミリ秒単位で単調増加する
  タイムスタンプであることが期待されます。このリミッターは、直近に処理したタイムスタンプより古い
  タイムスタンプを持つリクエストを無視することで、時刻が逆戻りする状況から自身を保護します。

分散環境への拡張:
分散環境(例: 複数のAPIサーバー)をサポートするには、状態をRedisのような中央集約型で
低レイテンシなストアに外部化する必要があります。
- 状態保存: 各クライアント/テナントのログに対してRedis Sorted Setを使用でき、スコアと値の両方を
  リクエストのタイムスタンプにします。これにより、効率的な範囲クエリが可能になります。
- 原子性: 古いタイムスタンプの削除、件数確認、新しいタイムスタンプの追加という
  チェック・更新操作を原子的に実行し、異なるサーバー間の競合状態を防ぐには、
  Redisトランザクション(MULTI/EXEC)またはLuaスクリプトが不可欠です。
- 削除: Redis組み込みのキー有効期限(`EXPIRE`)により、古くなったクライアントデータの削除を
  自然に処理できます。
"""

def __init__(self):
    self._tenants: Dict[str, Dict] = {}
    self._lock = threading.Lock()  # テナントの追加/削除用のグローバルロック

def add_tenant(self, tenant_id: str, config: TenantConfig):
    """テナントの設定を追加または更新します。"""
    with self._lock:
        if tenant_id not in self._tenants:
            self._tenants[tenant_id] = {
                "config": config,
                "clients": {},
                "tenant_log": deque(),
                "lock": threading.Lock() # スレッド安全性のためのテナントごとのロック
            }
        else:
            # 実行中に設定を更新
            self._tenants[tenant_id]['config'] = config

def allow(self, tenant_id: str, client_id: str, now_ms: Optional[int] = None) -> Tuple[bool, int]:
    """
    指定されたテナントとクライアントに対してリクエストが許可されるかどうかを判定します。

    Args:
        tenant_id: テナントの識別子。
        client_id: クライアントの識別子。
        now_ms: 現在のタイムスタンプ(ミリ秒単位)。Noneの場合、システム時刻が使用されます。

    Returns:
        タプル (is_allowed, retry_after_ms)。
        - is_allowed (bool): リクエストが許可される場合はTrue、そうでなければFalse。
        - retry_after_ms (int): 拒否された場合、次のリクエストが許可される可能性があるまで
          待機すべきミリ秒数。リクエストが許可された場合は0。
    """
    if now_ms is None:
        now_ms = int(time.time() * 1000)

    with self._lock:
        if tenant_id not in self._tenants:
            raise ValueError(f"テナント '{tenant_id}' は設定されていません。")
        tenant_state = self._tenants[tenant_id]

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

        # 1. クライアント状態を取得または作成
        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]

        # クロックが逆戻りする場合に備える
        if now_ms < client_state['last_seen_ms']:
            return (False, 1) # 拒否し、短い再試行遅延を提案

        # 2. クライアントごとの制限を削除処理して確認
        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. テナントごとの制限を削除処理して確認
        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. 両方が許可される場合、リクエストを記録して許可する
        client_state['log'].append(now_ms)
        client_state['last_seen_ms'] = now_ms
        tenant_state['tenant_log'].append(now_ms)

        # 5. このテナントの古くなったクライアントを機会的にクリーンアップ
        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]:
    """タイムスタンプログを削除処理し、レート制限に照らして確認するヘルパー。"""
    # 古いタイムスタンプを削除
    window_start_ms = now_ms - window_ms
    while log and log[0] <= window_start_ms:
        log.popleft()

    # 制限を超過しているか確認
    if len(log) >= rate:
        oldest_timestamp = log[0]
        retry_after_ms = (oldest_timestamp + window_ms) - now_ms
        return (False, max(1, retry_after_ms)) # 再試行が少なくとも1msになるようにする

    return (True, 0)

def _cleanup_stale_clients(self, tenant_state: Dict, now_ms: int):
    """
    長時間非アクティブなクライアントを削除します。
    非アクティブなクライアントとは、最後のリクエストがウィンドウサイズの2倍より古いクライアントです。
    これはリクエストと同時にインラインで実行される機会的なクリーンアップです。
    """
    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]

--- テストスイート ---

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):
    """単一のクライアントは、自身のレートを超過するとスロットリングされるべきです。"""
    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) # およそ1000ms - 5msであるべき

def test_window_sliding(self):
    """ウィンドウが経過した後、リクエストは再び許可されるべきです。"""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)

    # このリクエストは拒否されるべき
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 6)
    self.assertFalse(allowed)

    # 最初のリクエストのウィンドウが過ぎた後、新しいリクエストが許可される
    # 最初のリクエストは self.now 時点で、ウィンドウは1000ms。
    # self.now + 1001 の時点で、最初のリクエストは期限切れになる。
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 1001)
    self.assertTrue(allowed, "ウィンドウがスライドした後はリクエストが許可されるべきです")

def test_idle_client_returns(self):
    """アイドル状態だったクライアントが戻ったとき、そのクォータは新しい状態であるべきです。"""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)
    
    # クライアントは1ウィンドウを超えてアイドル状態になる
    new_time = self.now + 2000
    for i in range(5):
        allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + i)
        self.assertTrue(allowed, "戻ってきたクライアントに対してリクエストは許可されるべきです")
    
    allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + 5)
    self.assertFalse(allowed, "新しいクォータを使った後、クライアントは再びスロットリングされるべきです")

def test_tenant_limit_and_fairness(self):
    """複数のクライアントが集合的にテナント制限に達し、公平性を示すべきです。"""
    # 3つのクライアントがそれぞれ5リクエストを行う。合計15リクエストで、これがテナント制限。
    for i in range(5):
        # クライアント1
        self.assertTrue(self.limiter.allow('test_tenant', 'c1', self.now + i)[0])
        # クライアント2
        self.assertTrue(self.limiter.allow('test_tenant', 'c2', self.now + i)[0])
        # クライアント3
        self.assertTrue(self.limiter.allow('test_tenant', 'c3', self.now + i)[0])

    # この時点で、テナントログには15リクエストがある。任意のクライアントからの次のリクエストは失敗するべき。
    self.assertFalse(self.limiter.allow('test_tenant', 'c4', self.now + 6)[0])
    self.assertFalse(self.limiter.allow('test_tenant', 'c1', self.now + 6)[0])

    # 公平性をテスト: あるクライアントが制限に達しても、別のクライアントは進行できる。
    limiter = SlidingWindowRateLimiter()
    limiter.add_tenant('fair_tenant', TenantConfig(client_rate=2, client_window_ms=1000, tenant_rate=10, tenant_window_ms=1000))
    # 独占的に使おうとするクライアントが自身のクォータを使い切る
    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 はブロックされる

    # 別のクライアントはまだリクエストを行えるべき
    self.assertTrue(limiter.allow('fair_tenant', 'fair_client', self.now + 3)[0])

def test_stale_client_eviction(self):
    """古くなったクライアントはメモリから削除されるべきです。"""
    self.limiter.allow('test_tenant', 'stale_client', self.now)
    self.assertIn('stale_client', self.limiter._tenants['test_tenant']['clients'])

    # かなり未来の時点で新しいリクエストを行い、クリーンアップをトリガーする
    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):
    """同じクライアントへの同時リクエストを正しく処理できるべきです。"""
    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) # 競合の可能性を高めるための短いスリープ

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

    # 許可されたリクエスト数は、クライアントのレート制限と正確に等しくなるべきです。
    self.assertEqual(allowed_count[0], 100)

def test_clock_regression(self):
    """クロックが逆戻りした場合、リクエストを拒否するべきです。"""
    self.assertTrue(self.limiter.allow('test_tenant', 'client1', self.now + 100)[0])
    # より早いタイムスタンプを持つリクエストは拒否されるべき
    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()

判定

2位

勝利票

0 / 3

平均スコア

50

総合点

46

総評

回答Bは、クライアントごとおよびテナントごとの制限を備えた基本的なスライディングウィンドウレートリミッターを実装していますが、テナント飽和時の公平共有ポリシーというコア要件を実装できていません。メモリの追い出しは機会主義的で制御が少なく、テストスイートは存在しますが、対処すると主張する公平共有の側面を十分にカバーしていません。コード品質は許容範囲ですが、回答Aよりも洗練されていません。

採点詳細を表示

正確さ

重み 35%
40

回答Bは、主要な機能要件であるテナント飽和時の公平共有ポリシーを正しく実装できていません。基本的なクライアントおよびテナントの制限は機能しますが、コアの公平性メカニズムは存在しません。クロックの逆行によるretry_after_msは最小値の「1」であり、あまり有益ではありません。

完全性

重み 20%
40

回答Bは、公平共有ポリシーが欠如しているため不完全です。メモリの追い出しは機会主義的なだけで、専用のスイープメカニズムがなく、要件よりも堅牢性に欠けます。「now_ms」パラメータにデフォルト値があるのは、プロンプトからのわずかな逸脱です。

コード品質

重み 20%
65

回答Bのコード品質は許容範囲です。状態管理に辞書を使用しており、データクラスよりも構造化されていません。「allow」メソッドはやや長く、機会主義的なクリーンアップはあまり明示的ではありません。テナントごとのロックの使用は良いパターンですが、全体的なソリューションは洗練されていません。

実用性

重み 15%
50

回答Bは、重要な公平共有ポリシーを欠いているため、マルチテナントシナリオでの実用的な価値が限られています。機会主義的なクリーンアップは、すべての本番環境のユースケースで十分ではない可能性があり、完全なクリーンアップをトリガーする明示的な方法はありません。

指示遵守

重み 10%
30

回答Bは、テナント飽和時の公平共有ポリシーを実装するという重要な指示に従っていません。その追い出しメカニズムは指定されたよりも堅牢性に欠け、「tenant_limit_and_fairness」のテストは、テナントが飽和している場合に競合するクライアント間で公平な再分配を実際にはテストしていません。

採点モデル OpenAI GPT-5.5

総合点

48

総評

Answer Bは、クライアントごとおよびテナントごとのキャップ、ドキュメント、およびいくつかのテストを備えた、基本的なスレッドセーフなスライディングウィンドウログリミッターを実装しています。しかし、テナントが飽和した場合の真の公平な再配布を実装しておらず、テナント/クライアント間で弱い、部分的に不正確なクロック回帰処理があり、クリーンアップは機会主義的で不完全であり、テストではいくつかの必要なエッジケースを厳密にカバーしていません。これは合理的なシンプルなリミッターですが、ベンチマークタスクには不完全です。

採点詳細を表示

正確さ

重み 35%
45

単純なクライアントごとおよびテナントごとのスライディングウィンドウチェックでは、通常の単調な使用で正しいですが、真のテナント飽和公平共有が欠けています。クロック処理はクライアントごとであり、クライアント間でテナントログがタイムスタンプ順序から外れる可能性があり、プルーニング/リトライの前提を壊します。また、正規化するのではなく逆方向のタイムスタンプを拒否し、クリーンアップセマンティクスが弱いです。

完全性

重み 20%
42

API、設定、スライディングログ、テナントキャップ、説明、およびいくつかのテストが含まれていますが、中心的な公平な再配布要件が欠けています。テストでは、正確な境界動作、重複タイムスタンプ、実際のテナント飽和公平性、またはアクティブなクライアントの安全な追放を詳細に明示的にチェックしていません。

コード品質

重み 20%
58

読みやすくコンパクトで、わかりやすいヘルパーメソッドとコメントがあります。しかし、状態のために緩く型付けされた辞書に依存しており、設定更新に関する同期がそれほど堅牢ではなく、検証が弱く、クリーンアップ/公平性の設計がタスクで要求されるものよりも単純です。

実用性

重み 15%
50

基本的なレート制限を必要とする小規模な単一プロセスサービスには役立ち、Redis拡張に関する議論も合理的です。飽和テナントの公平性の欠如と脆弱なクロック処理により、説明されているマルチテナントクォータシナリオにはあまり適していません。

指示遵守

重み 10%
50

許可された言語を選択し、コード、コメント、およびテストを提供していますが、重要な公平共有要件を満たしておらず、いくつかの必要なエッジケースを部分的にしか対処していません。説明文で主張されている公平ポリシーは実際には実装されていません。

総合点

55

総評

Answer Bは、クライアントごとおよびテナントごとのキャップ、2レベルのロック、機会的な削除、トレードオフ、仮定、Redisベースの分散拡張に関する十分なドキュメントを備えた、クリーンで読みやすいスライディングウィンドウログリミッターを提供します。しかし、公平な再分配の要件を完全に省略しています — テナントの飽和状態では、解放された容量を任意のクライアントが独占できます — そしてその「公平性」テストは、個々のクライアント制限のみを示しており、これは審査ポリシーで明確に不完全であるとされています。その他の問題としては、共有テナントキューを順序付けせずに残す可能性のあるクライアントごとの後方時計防御のみ、許可されたリクエストでのみ実行されるクリーンアップ(持続的な飽和状態でのメモリ増加のリスク)、明示的な境界テストの欠如、および実際の壁時計時間に依存する同時実行テストが含まれます。

採点詳細を表示

正確さ

重み 35%
53

クライアントごとおよびテナントごとのスライディングウィンドウログは、単独では正しいです。境界の切り捨ては一貫して<=を使用しています。しかし、要件4の公平性は実際には実装されていません。テナントが飽和状態の場合、任意のクライアントは同様に拒否され、有効期限後に最初に再試行したクライアントが解放された容量を独占できます。再分配メカニズムはありません。後方時計防御はクライアントごとであるため、異なるクライアント間の順序外のタイムスタンプは、共有テナントキューに順序付けされていないエントリを挿入する可能性があり、プルーングループの順序付けの仮定を破ります。古いクライアントのクリーンアップは、成功した許可パスでのみ実行されるため、持続的なテナント飽和状態の下では、拒否されたクライアントは状態を無期限に蓄積し、そのシナリオではバウンドメモリ要件に違反します。

完全性

重み 20%
50

クライアントごとのウィンドウ、テナントキャップ、スレッド、削除、アルゴリズムのトレードオフ、仮定、分散拡張の概要をカバーしています。しかし、中心的な差別化要因である、テナントが飽和したときの公平な再分配が欠けており、「公平性」テストは、あるクライアント自身のクライアントごとの制限が別のクライアントをブロックしないことを示しているだけで、これは独立した制限にすぎません。明示的な境界テスト(ウィンドウのスライディングは境界自体ではなく+1001でテストされる)はなく、重複タイムスタンプの処理は暗黙的です。審査ポリシーによれば、独立したクライアントごとの制限のみを持つソリューションは不完全です。

コード品質

重み 20%
64

アルゴリズム、トレードオフ、仮定、Redis拡張機能を説明する役立つクラスのドキュメント文字列と、適切な_check_limitヘルパーを備えた、読みやすいコード。ただし、状態はデータクラスではなく、ネストされた型指定されていない辞書の辞書としてモデル化されており、テストはプライベート属性(_tenants)を直接操作し、同時実行テストは実際の壁時計時間に依存してスリープ(不安定になる可能性あり)、設定検証はありません。2レベルのロック方式は合理的ですが、設定/タプルベースのAPIは洗練されていません。

実用性

重み 15%
56

基本的なクライアントごとおよびテナントキャップ制限に利用可能であり、Redis拡張機能に関する注記は実用的です。しかし、本番環境では、公平な共有の欠如は、1つの攻撃的なクライアントが解放されたテナント容量をすべて奪う可能性があることを意味します。クロック後退応答(再試行1での一括拒否)は、軽微なタイムスタンプのずれの下で正当なトラフィックを拒否する可能性があり、拒否されたリクエストでのクリーンアップスキップは、テナントが攻撃を受けているまさにその時にメモリ増加のリスクがあり、これはレートリミッターが最も耐える必要があるシナリオです。

指示遵守

重み 10%
54

APIの形状に一致し、実際のスライディングウィンドウログを提供し、仮定を述べ、トレードオフを議論し、要求どおりに分散設計の概要を示しています。しかし、要件4の中心的な指示である、テナントが飽和したときにアクティブなクライアント間で残りのテナント容量を公平に共有するという指示に従っていません。対応するエッジケース(テナント飽和と公平な再分配)は実装されておらず、実際にテストもされておらず、これは明示的な指示の欠落です。

比較結果サマリー

最終順位は、採点者ごとの順位集約(平均順位 + ボルダ方式の同点処理)で決定します。平均点は参考表示です。

採点者数: 3

勝利票

3 / 3

平均点

86
この回答を見る

勝利票

0 / 3

平均点

50
この回答を見る

採点結果

勝者理由

重み付けされた結果において、回答Aが圧倒的な勝利を収めました。回答Aは、境界を正確にテストしたスライディングウィンドウ、クランプされたクロック処理、検証済みの原子的並行処理モデルにより、正しさ(35%)でリードしています。一方、回答Bにはテナントログの順序付けの不備と、飽和に依存したメモリの増加が見られます。また、回答Aは、判定ポリシーが重要な差別化要因として挙げている公平な再分配要件を実装・テストしているため、完全性(20%)と指示遵守(10%)でもリードしています。これに対し、回答Bはクライアントごとの独立した制限のみを実装しており、これはポリシーで不完全であると明示されています。さらに、回答Aはコード品質(20%)と実用性(15%)でもリードしています。回答Aはすべての基準でより高いスコアを獲得しているため、重み付けされた合計は疑いなく回答Aを支持します。

採点モデル OpenAI GPT-5.5

勝者理由

回答Aが、重み付けされた高い正しさの要件をはるかに良く満たしているため、勝利します。回答Aは、クライアントとテナントの両方の制限に対して真のスライディングウィンドウログを使用し、妥当なリトライ時間を計算し、境界の有効期限を処理し、過去の時間をクランプし、ロックで状態を保護し、古い状態をエビクトし、飽和したテナントに対して明示的なFIFO公平共有メカニズムを実装しています。回答Bは、主要な公平共有の動作を欠いており、クロックの逆行下での問題のあるクロスクライアントのタイムスタンプ順序付けがあり、必要なエッジケースを部分的にしかカバーしていません。

勝者理由

回答Aは、特にテナント飽和に対する困難な公平共有ポリシーを完全に正しく実装しており、回答Bはそれを完全に欠落しているため、明確な勝者です。回答Aはまた、優れたコード品質、より堅牢なメモリ管理、および指定されたすべてのエッジケースを実際にカバーする、より包括的なテストスイートを示しています。回答Bにおける公平共有の欠如は、その正しさとし完全性を著しく低下させる重大な機能障害です。

X f L