fix(rate-limiting): reject key_ttl_seconds shorter than period_seconds

A key_ttl_seconds override below period_seconds expired the bucket key
before its window rolled over, resetting the counter to zero mid-window
and letting tagged traffic exceed the configured limit. Only
period_seconds and above is now accepted.
This commit is contained in:
Deepanshu 2026-08-17 12:12:40 -04:00
parent e83886a9d3
commit f6f65e181b
2 changed files with 17 additions and 1 deletions

View file

@ -172,6 +172,11 @@ class TagRateLimitEntry(BaseModel):
def _validate_key_ttl_seconds(self) -> "TagRateLimitEntry":
if self.key_ttl_seconds is not None and self.key_ttl_seconds <= 0:
raise ValueError("key_ttl_seconds must be a positive integer when set")
if self.key_ttl_seconds is not None and self.key_ttl_seconds < self.period_seconds:
raise ValueError(
"key_ttl_seconds must be at least period_seconds when set -- a shorter TTL expires the "
"counter before its window rolls over, letting tagged traffic reset to zero and exceed the limit"
)
return self
@model_validator(mode="after")

View file

@ -2461,7 +2461,7 @@ def test_ttl_for_concurrency_never_drops_below_the_safety_floor_even_with_a_lowe
"""
below_floor: Final = 10
assert (
_PROXY_TagRateLimiter._ttl_for(_concurrency_limit(period_seconds=60, key_ttl_seconds=below_floor))
_PROXY_TagRateLimiter._ttl_for(_concurrency_limit(period_seconds=5, key_ttl_seconds=below_floor))
== _CONCURRENCY_MIN_SAFETY_TTL_SECONDS
)
@ -2471,6 +2471,17 @@ def test_tag_rate_limit_entry_rejects_non_positive_key_ttl_seconds():
TagRateLimitEntry(name="per_minute", limit=1, period_seconds=60, key_ttl_seconds=0)
def test_tag_rate_limit_entry_rejects_key_ttl_seconds_shorter_than_period_seconds():
"""
Regression test for a real bug: a key_ttl_seconds shorter than
period_seconds expires the bucket key before its window rolls over,
resetting the counter to zero mid-window and letting tagged traffic
exceed the configured limit.
"""
with pytest.raises(ValueError):
TagRateLimitEntry(name="per_minute", limit=1, period_seconds=60, key_ttl_seconds=59)
# ---------------------------------------------------------------------------
# per-tag max_in_memory_cache_size override -- dedicated cache partitions
# ---------------------------------------------------------------------------