fix(dynamic_rate_limiter): make test_priority_reservation deterministic across minute rollover - #32299
Conversation
…table within a request
Greptile SummaryThis PR makes
Confidence Score: 5/5The change is minimal and surgical: a two-line replacement in production code with a well-defaulted parameter, plus a fixture update and new focused unit tests. No user-facing behavior changes. The production change is a textbook dependency-injection seam with a sensible default. The existing tests are made more deterministic, not weakened. The new tests deliberately cover the exact failure scenario described in the PR description. Nothing in the critical request path is structurally altered. No files require special attention. The two still-skipped tests (
|
| Filename | Overview |
|---|---|
| litellm/proxy/hooks/dynamic_rate_limiter.py | Adds injectable time_fn: Callable[[], datetime] (defaulting to get_utc_datetime) to DynamicRateLimiterCache and _PROXY_DynamicRateLimitHandler; replaces two hardcoded get_utc_datetime() calls with self.time_fn() — production behavior is unchanged, tests gain a clock seam |
| tests/local_testing/test_dynamic_rate_limit_handler.py | Fixture now injects a frozen UTC clock into DynamicRateLimitHandler, eliminating the minute-rollover race that caused test_priority_reservation to flake on CI; two skip-decorated tests remain that would now be safe to un-skip |
| tests/test_litellm/proxy/hooks/test_dynamic_rate_limiter.py | New unit-test file covering three scenarios: frozen-clock sadd/get share the same key, minute-rollover between sadd and get returns None, and the handler correctly threads time_fn to its inner cache — no real network calls |
Reviews (3): Last reviewed commit: "fix(dynamic_rate_limiter): inject clock ..." | Re-trigger Greptile
Greptile SummaryThis PR fixes a flaky test (
Confidence Score: 5/5Safe to merge. The change is a minimal, additive dependency injection with a backwards-compatible default that leaves all production call paths identical to before. The production change is a single optional parameter threaded through two constructors; the default value preserves existing behaviour exactly. The test fixture change pins the clock rather than altering any assertion, and the new tests are focused, self-contained, and cover the specific failure mode that caused the flake. No logic in the request path is altered. No files require special attention. The two long-running tests (
|
| Filename | Overview |
|---|---|
| litellm/proxy/hooks/dynamic_rate_limiter.py | Adds optional time_fn injectable clock to DynamicRateLimiterCache and _PROXY_DynamicRateLimitHandler; all get_utc_datetime() call sites replaced with self.time_fn(); production default unchanged. |
| tests/local_testing/test_dynamic_rate_limit_handler.py | Fixture updated to inject a frozen UTC clock, making all write-then-read tests deterministic against minute rollover; no test assertions weakened. |
| tests/test_litellm/proxy/hooks/test_dynamic_rate_limiter.py | New unit tests covering shared clock window, minute-rollover behavior, and handler-to-cache time_fn threading; all use in-memory DualCache with no network calls. |
Reviews (2): Last reviewed commit: "fix(dynamic_rate_limiter): inject clock ..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
those tests also depend on the router's own un-injected minute clock (router.py:8890) plus 15s of real sleeps, so re-enabling them would still flake ~1 in 3 runs |
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
test_priority_reservation[100]failed on CircleCI job 2000264 withassert 90 == 0.DynamicRateLimiterCachekeys the active-project set by the current UTC minute ("%H-%M:{model}"), and the test'sasync_set_cache_saddand the handler's read insidecheck_available_usageeach callget_utc_datetime()independently. When the wall-clock minute rolls over between the two calls, the read targets the next minute's empty key,active_projectscomes backNone, the divide-by-projects step is skipped, and availability isint(100 * 0.9) = 90for every parametrization; only[1]survives that because its expected value happens to also be 90. The siblingtest_multiple_projects_e2ewas skipped back in June 2024 for this exact class of problem ("Unstable on ci/cd due to curr minute changes")Deterministic reproduction of the mechanism on the parent commit, driving the two clock reads across a minute boundary:
Plain rerunning cannot catch it because the race window is sub-millisecond (20 of 20 green before the fix on a quiet clock):
With the fix, the fixture pins the limiter's clock, so the sadd and the read can never straddle a minute boundary, and
tests/test_litellm/proxy/hooks/test_dynamic_rate_limiter.py::test_minute_rollover_between_sadd_and_get_reads_empty_windowpins the rollover mechanism itself deterministicallyType
🐛 Bug Fix
✅ Test
Changes
DynamicRateLimiterCacheand_PROXY_DynamicRateLimitHandlernow take an injectabletime_fn(defaulting toget_utc_datetime, so production behavior is unchanged) instead of hardcoding the clock; this follows the repo preference for dependency injection over monkeypatching. The flakytests/local_testing/test_dynamic_rate_limit_handler.pyfixture injects a frozen clock, which makestest_priority_reservation(and the other tests in that file that share the same write-then-read-within-one-window assumption, e.g.test_update_cache, whose 2s sleep gave it roughly a 1 in 30 chance of straddling a rollover per run) deterministic. New unit tests intests/test_litellm/proxy/hooks/test_dynamic_rate_limiter.pycover the injected-clock key window, the minute-rollover behavior that caused the flake, and the handler threadingtime_fnthrough to its cache