Skip to content

lmi: support TLS Redis (rediss://) in GlobalRateLimiter#452

Merged
jabra merged 7 commits intomainfrom
fix/rate-limiter-tls-redis
Apr 24, 2026
Merged

lmi: support TLS Redis (rediss://) in GlobalRateLimiter#452
jabra merged 7 commits intomainfrom
fix/rate-limiter-tls-redis

Conversation

@jabra
Copy link
Copy Markdown
Contributor

@jabra jabra commented Apr 23, 2026

Summary

GlobalRateLimiter previously hardcoded async+redis:// for the storage client (rate_limiter.py:155) and opened the get_rate_limit_keys SCAN client without any ssl= kwarg. Against a TLS-only Redis — e.g. AWS ElastiCache with in-transit encryption enabled — the TCP handshake succeeds but the server then hangs waiting for a ClientHello that never arrives. Symptom in the wild: get_rate_limit_keys() hangs during agent startup, blocking _choose_model_based_on_limits and every downstream rate-limited call.

This PR makes both Redis code paths scheme-aware and consistent:

  • Two shared helpers — _wants_tls(redis_url) and _strip_scheme(redis_url) — decide TLS from an explicit rediss:// / redis:// scheme, or (for bare host:port URLs) from a REDIS_USE_TLS env var parsed as a whitelist (1|true|yes|on) rather than bool() (which would treat "0"/"false" as true).
  • The storage property now builds async+rediss://{host}:{port} when TLS is required, instead of unconditionally prefixing async+redis://.
  • get_rate_limit_keys() uses the same helpers and passes ssl=<tls> to the direct coredis.Redis(...) client.

Behavior matrix

REDIS_URL REDIS_USE_TLS Storage URL Scan client ssl
host:6379 unset async+redis://host:6379 False
host:6379 true async+rediss://host:6379 True
host:6379 0 / false async+redis://host:6379 False
redis://host:6379 any async+redis://host:6379 False
rediss://host:6379 any async+rediss://host:6379 True

The bare-host + no-env-var row is the GCP Memorystore path — unchanged.

Test plan

  • uv run --directory packages/lmi pytest tests/test_rate_limiter.py → 38 passed
  • uv run mypy packages/lmi/src/lmi/rate_limiter.py → clean
  • pre-commit run --files packages/lmi/src/lmi/rate_limiter.py packages/lmi/tests/test_rate_limiter.py → all hooks pass (ruff, mypy, codespell, detect-secrets, blacken-docs)
  • New parametrized coverage for the scan client AND the storage URL across plain, redis://, rediss://, and both REDIS_USE_TLS directions (including the "0" regression case that bool() would have mishandled)
  • Validated end-to-end in a downstream service: a TLS-only ElastiCache endpoint that previously hung during get_rate_limit_keys now completes startup and proceeds to model selection.

Follow-up

A downstream service (paperqa-server) currently carries a local override of get_rate_limit_keys that added ssl=True to the scan client. Once this lands and is released, that override can be dropped.

Previously GlobalRateLimiter hardcoded `async+redis://` for the storage
client and opened the SCAN client without `ssl=`. Against a TLS-only
Redis (e.g. AWS ElastiCache with in-transit encryption enabled), the
TCP handshake succeeds but the server hangs waiting for a ClientHello
that never arrives -- typically observed as `get_rate_limit_keys`
hanging during startup.

Now a shared pair of helpers (`_wants_tls`, `_strip_scheme`) drives both
code paths: `rediss://` -> TLS, `redis://` -> plaintext, and a bare
`host:port` can be promoted to TLS via `REDIS_USE_TLS` (parsed as a
whitelist of `1|true|yes|on`, not `bool()`).

Tests parametrize the scan-client kwargs and the storage URL across
plain, `redis://`, `rediss://`, and both env-override directions to
guard against scheme/transport drift.
Copilot AI review requested due to automatic review settings April 23, 2026 09:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds TLS-aware Redis handling to GlobalRateLimiter so it can work reliably with TLS-only Redis endpoints (e.g., AWS ElastiCache in-transit encryption) without hanging during startup key scans.

Changes:

  • Introduces helpers to infer TLS from rediss:// / redis:// schemes or REDIS_USE_TLS for bare host:port URLs.
  • Builds RedisStorage URLs with async+rediss:// when TLS is required.
  • Passes ssl=<tls> to the direct coredis.Redis(...) scan client and adds parametrized tests for both code paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/lmi/src/lmi/rate_limiter.py Adds scheme/env-based TLS detection and applies it consistently to storage URL construction and the SCAN client.
packages/lmi/tests/test_rate_limiter.py Adds parametrized coverage for TLS selection and expected storage URL scheme across URL formats and env var values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/lmi/src/lmi/rate_limiter.py Outdated
Comment thread packages/lmi/src/lmi/rate_limiter.py Outdated
Copy link
Copy Markdown
Collaborator

@jamesbraza jamesbraza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, though I think you clobbered a few things worth keeping around

Comment thread packages/lmi/src/lmi/rate_limiter.py Outdated
Comment thread packages/lmi/src/lmi/rate_limiter.py Outdated
Comment thread packages/lmi/src/lmi/rate_limiter.py
Comment thread packages/lmi/src/lmi/rate_limiter.py
Comment thread packages/lmi/tests/test_rate_limiter.py Outdated
Copy link
Copy Markdown
Collaborator

@jamesbraza jamesbraza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

pytest.param(
lambda: "rediss://10.58.188.212:6379",
{"host": "10.58.188.212", "port": 6379, "password": None, "ssl": True},
id="rediss-scheme",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe say ssl-scheme

) -> None:
with patch("lmi.rate_limiter.RedisStorage") as mock_redis_storage:
limiter = GlobalRateLimiter(redis_url=redis_url)
_ = limiter.storage
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we doing this? Maybe put an inline comment

ypicard and others added 2 commits April 23, 2026 16:15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
self.redis_url = redis_url
self._rate_config = RATE_CONFIG if rate_config is None else rate_config
self._redis_url = redis_url or os.environ.get("REDIS_URL")
self._use_in_memory = use_in_memory or not self._redis_url
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamesbraza This defaults to in memory when redis isn't configured.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sounds great.

Can you document that in the Args section of the docstring? Or feel free to skip.

Copy link
Copy Markdown
Collaborator

@jamesbraza jamesbraza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.redis_url = redis_url
self._rate_config = RATE_CONFIG if rate_config is None else rate_config
self._redis_url = redis_url or os.environ.get("REDIS_URL")
self._use_in_memory = use_in_memory or not self._redis_url
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sounds great.

Can you document that in the Args section of the docstring? Or feel free to skip.

Document that use_in_memory falls back to in-memory storage when no
Redis URL is configured, and surface the redis://, rediss://, and
bare host:port URL forms on `redis_url`.
@jabra jabra merged commit 4424777 into main Apr 24, 2026
7 checks passed
@jabra jabra deleted the fix/rate-limiter-tls-redis branch April 24, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants