Fix/shared health check polling - #26434
Conversation
The shared health check manager's non-leader branch only waited 2 seconds before falling back to a local health check. Since real health checks against multiple models typically take longer, every non-leader pod would fall through and run redundant checks — defeating the shared coordination. Replace the single 2-second sleep with a polling loop (5s interval, up to lock_ttl) that gives the lock holder realistic time to finish. Adds early exit when the lock disappears without a cache write (crash recovery) and defensive error handling for Redis hiccups during polling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
Low: No security issues foundThis PR changes the health check polling strategy from a single 2-second sleep to a bounded polling loop (up to Status: 0 open Posted by Veria AI · 2026-04-24T15:14:38.100Z |
Greptile SummaryThis PR fixes shared health-check coordination in multi-pod deployments by replacing a single Confidence Score: 5/5Safe to merge; only a P2 style finding present, logic is correct and well-tested. All findings are P2 (style/clarity). The core logic is correct: the redis_cache is None guard prevents the 60-second regression, the polling loop correctly bounds total wait to lock_ttl, and the early-exit on orphaned lock is sound. Four new mock-only tests cover the added paths comprehensively. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/health_check_utils/shared_health_check_manager.py | Replaces single asyncio.sleep(2) with a proper polling loop (up to lock_ttl seconds, 5 s intervals); adds early-exit for crashed lock holders; guards redis_cache is None before entering the loop; handles transient Redis errors during polling gracefully. |
| tests/test_litellm/proxy/test_shared_health_check.py | Adds four new mock-only tests covering: polling fallback exhaustion, early-exit on orphaned lock, Redis error resilience during polling, and no-polling path when redis_cache is None; updates existing tests to reflect 5 s poll interval. All tests are mock-based with no real network calls. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[perform_shared_health_check called] --> B{Cached results\navailable?}
B -- Yes --> C[Return cached results]
B -- No --> D[Try to acquire Redis lock]
D -- Acquired --> E[Run perform_health_check]
E --> F[Cache results in Redis]
F --> G[Release lock]
G --> H[Return results]
D -- Not acquired --> I{redis_cache\nis None?}
I -- Yes --> J[Run local health check immediately]
I -- No --> K[Enter polling loop\nelapsed = 0]
K --> L{elapsed < lock_ttl?}
L -- No --> M[Log warning: exhausted wait]
M --> J
L -- Yes --> N[asyncio.sleep 5s\nelapsed += 5]
N --> O{Cached results\navailable?}
O -- Yes --> C
O -- No --> P{Lock still held?\nasync_get_cache lock_key}
P -- Redis error --> K
P -- Still held --> K
P -- Lock gone\ncurrent_owner is None --> Q[Break: orphaned lock detected]
Q --> M
Reviews (3): Last reviewed commit: "fix: skip polling loop when redis_cache ..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Update two existing tests to match the new 5s polling interval (previously asserted sleep(2)). Add two new tests: - Early exit when lock disappears without cache (crash recovery) - Redis error resilience during polling loop
When Redis is not configured, the polling loop would sleep for the full lock_ttl (60s) with no chance of finding cached results. Add an early return to fall back to local health check immediately. Addresses Greptile review feedback on BerriAI#26434.
|
@noahnistler — could you add a screenshot or short video showing that this change works as expected? It really helps reviewers verify the fix quickly. Thanks! |
@krrish-berri-2 I Added test output screenshot and a scenario coverage table showing all 7 polling paths tested (cache hit, lock acquired, poll succeeds, poll exhausted, orphaned lock/crash recovery, Redis error during polling, no Redis configured). 31/31 tests pass. |
2b4beae
into
BerriAI:shin_agent_oss_staging_05_09_2026
|
🤖 litellm-agent: Squash-merged into staging branch Triage Summary Merge Confidence: 5/5 ✅ READY All checks green. Greptile 5/5, no blocking pattern findings, no CircleCI runs (OSS-typical). |
Squash-merged by litellm-agent from noahnistler's PR.
Relevant issues
Fixes shared health check coordination failing in multi-pod (e.g. ECS) deployments — every pod runs redundant health checks instead of sharing results.
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@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).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Screenshots / Proof of Fix
This fix changes Redis-coordinated polling logic between pods — it cannot be reproduced with a single-instance deployment. Proof is provided via unit tests that simulate the multi-pod coordination scenarios with mocked Redis.
Scenario coverage
test_perform_shared_health_check_with_cacheperform_health_checknever calledtest_perform_shared_health_check_with_lock_acquisitiontest_perform_shared_health_check_lock_failed_then_cachetest_perform_shared_health_check_fallbacklock_ttlexceeded, falls back to local health checktest_perform_shared_health_check_early_exit_orphaned_locktest_perform_shared_health_check_redis_error_during_pollingtest_perform_shared_health_check_no_redis_skips_pollingWhat changed (before → after)
lock_ttl) for cached results, with early exit if the lock disappears (crash recovery), and only falls back to local check if polling is exhaustedType
🐛 Bug Fix
Changes
The SharedHealthCheckManager.perform_shared_health_check() non-leader branch only waited 2 seconds before falling back to a local health check. Since real health checks against multiple models typically take longer than 2s, the cache was almost never ready in time — causing every non-leader pod to run its own redundant health check and defeating the shared coordination.
Fix
Replace the single asyncio.sleep(2) with a polling loop:
Not a breaking change
Same method signature, same return type, same external behavior.