fix(client): add in-memory rate limit fallback when Valkey is unavailable#372
Merged
TeKrop merged 4 commits intoTeKrop:mainfrom Feb 22, 2026
Merged
Conversation
…able set_global_rate_limit() is decorated with @handle_valkey_error which silently swallows ValkeyError. If Valkey is down when Blizzard returns HTTP 403, the rate limit flag is never stored, and subsequent requests continue hitting Blizzard — potentially causing extended bans. Add an in-memory timestamp (_rate_limited_until) as a fallback that works even when Valkey is unavailable. Since BlizzardClient is a singleton, this effectively rate-limits the entire process. The Valkey check remains for cross-worker coordination; the in-memory check adds defense in depth.
Contributor
Reviewer's guide (collapsed on small PRs)Reviewer's GuideImplements an in-memory per-process fallback rate-limiting mechanism in BlizzardClient to handle cases where Valkey is unavailable, by tracking a monotonic timestamp and checking it before the shared Valkey-based rate limit. Sequence diagram for in-memory fallback rate limiting in BlizzardClientsequenceDiagram
actor Caller
participant BlizzardClient
participant CacheManager
participant Valkey
participant BlizzardAPI
Caller->>BlizzardClient: request_resource()
BlizzardClient->>BlizzardClient: _check_rate_limit()
BlizzardClient->>BlizzardClient: remaining = _rate_limited_until - time.monotonic()
alt in_memory_limit_active
BlizzardClient-->>Caller: HTTP 429 Too Many Requests
else no_in_memory_limit
BlizzardClient->>CacheManager: is_being_rate_limited()
CacheManager->>Valkey: check_global_rate_limit_flag
Valkey-->>CacheManager: flag_or_error
CacheManager-->>BlizzardClient: is_rate_limited
alt valkey_indicates_rate_limit
BlizzardClient->>CacheManager: get_global_rate_limit_remaining_time()
CacheManager->>Valkey: get_retry_after
Valkey-->>CacheManager: retry_after_seconds
CacheManager-->>BlizzardClient: retry_after_seconds
BlizzardClient-->>Caller: HTTP 429 Too Many Requests
else not_rate_limited
BlizzardClient->>BlizzardAPI: call_endpoint()
BlizzardAPI-->>BlizzardClient: HTTP 403 Forbidden
BlizzardClient->>BlizzardClient: _blizzard_forbidden_error()
BlizzardClient->>BlizzardClient: _rate_limited_until = time.monotonic() + settings.blizzard_rate_limit_retry_after
BlizzardClient->>CacheManager: set_global_rate_limit()
CacheManager->>Valkey: write_global_rate_limit_flag
Valkey-->>CacheManager: ok_or_error
BlizzardClient-->>Caller: HTTP 403 Forbidden
end
end
Updated class diagram for BlizzardClient rate limitingclassDiagram
class BlizzardClient {
- CacheManager cache_manager
- float _rate_limited_until
- AsyncClient client
+ __init__()
+ _check_rate_limit() void
+ _blizzard_forbidden_error() HTTPException
}
class CacheManager {
+ is_being_rate_limited() bool
+ get_global_rate_limit_remaining_time() int
+ set_global_rate_limit() void
}
class Singleton {
}
class AsyncClient {
}
class HTTPException {
}
BlizzardClient ..> CacheManager : uses
BlizzardClient ..> AsyncClient : uses
BlizzardClient ..> HTTPException : raises
BlizzardClient ..|> Singleton : metaclass
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `app/adapters/blizzard/client.py:146-148` </location>
<code_context>
- Defense in depth (if nginx check fails or is bypassed)
"""
+ # Check in-memory fallback first (works even when Valkey is down)
+ remaining = self._rate_limited_until - time.monotonic()
+ if remaining > 0:
+ raise self._too_many_requests_response(retry_after=int(remaining) or 1)
+
if await self.cache_manager.is_being_rate_limited():
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider syncing the in-memory fallback with the Valkey remaining TTL to avoid divergence.
Because `_rate_limited_until` is only set in `_blizzard_forbidden_error`, the in-memory check can miss an active rate limit if Valkey reports it (via `is_being_rate_limited()` / `get_global_rate_limit_remaining_time()`) but then becomes unavailable. To keep them in sync, consider updating `_rate_limited_until` whenever Valkey indicates a rate limit is active, e.g. `self._rate_limited_until = time.monotonic() + remaining`.
```suggestion
if await self.cache_manager.is_being_rate_limited():
# Sync in-memory fallback with Valkey's remaining TTL
remaining = await self.cache_manager.get_global_rate_limit_remaining_time()
# Ensure in-memory fallback can still protect when Valkey becomes unavailable
self._rate_limited_until = time.monotonic() + float(remaining)
raise self._too_many_requests_response(
retry_after=int(remaining) or 1
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
When Valkey reports an active rate limit, sync the in-memory _rate_limited_until timestamp with Valkey's remaining TTL. This ensures the in-memory fallback can still protect against Blizzard requests if Valkey becomes unavailable mid-rate-limit window. Addresses review feedback from sourcery-ai.
TeKrop
previously approved these changes
Feb 19, 2026
…th.ceil Two issues fixed: - BlizzardClient singleton's _rate_limited_until persisted across tests, causing 429 responses in unrelated tests after any rate limit trigger - int() truncation turned 4.99s into "4 seconds", mismatching expected "5 seconds" in assertions; math.ceil() preserves correct rounding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Problem
set_global_rate_limit()is decorated with@handle_valkey_error(default_return=None), which silently swallowsValkeyError. If Valkey is unavailable when Blizzard returns HTTP 403:_check_rate_limit()sees no flag → allows requests throughFix
Add an in-memory timestamp (
_rate_limited_until) on theBlizzardClientsingleton as a fallback:time.monotonic() + retry_afterbefore attempting the Valkey write_check_rate_limit(): check the in-memory timestamp first, then fall through to the Valkey checkSince
BlizzardClientuses theSingletonmetaclass, this effectively rate-limits the entire process even when Valkey is down. The Valkey check remains for cross-worker coordination.Impact
Summary by Sourcery
Bug Fixes: