Skip to content

fix(messaging): Azure receive() could never decode a body; Redis resilience for managed cache - #18

Merged
abhi-bhat-lyzr merged 4 commits into
LYZR-OSS:mainfrom
parshva-lyzr:fix/redis-connection-resilience
Jul 29, 2026
Merged

fix(messaging): Azure receive() could never decode a body; Redis resilience for managed cache#18
abhi-bhat-lyzr merged 4 commits into
LYZR-OSS:mainfrom
parshva-lyzr:fix/redis-connection-resilience

Conversation

@parshva-lyzr

@parshva-lyzr parshva-lyzr commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Azure Service Bus receive() has never worked

receive() built its Message with bytes(m), but ServiceBusReceivedMessage does not implement __bytes__ — its .body is a single-use generator of byte chunks. Every receive against a live broker raised:

TypeError: cannot convert 'ServiceBusReceivedMessage' object to bytes

Sending was fine, so the break only shows up the moment anything tries to consume. Present in 0.2.6 and 0.2.7 alike.

Why CI never caught it: the fake in tests/test_messaging_azure.py defined __bytes__, which the real SDK type lacks. bytes(m) passed against the fake and failed against the broker. The fake now mirrors the SDK — .body yields chunks and is single-use — so restoring bytes(m) fails 4 tests with the production error.

Verified against namespace lyzr-studio-dev-we-sbus: type(m.body) is generator, b"".join(m.body) yields the payload, and a full send → receive → complete round trip now succeeds on a session-enabled queue.

Redis resilience for managed cache

Three earlier commits in this branch:

  • Redis clients were built with no retry policy or health checks.
  • ReadOnlyError was reaching the caller. It subclasses ResponseError, not ConnectionError, so Azure's failover reply (-READONLY You can't write against a read only replica.) skipped the retry path. Retrying reconnects and re-resolves DNS to the promoted primary.
  • from_credentials / from_access_key accept username and ssl_cert_reqs. ssl_cert_reqs defaults to "required"; redis-py silently resolves None to CERT_NONE, which disables verification without saying so.

Verification

267 passed, 3 skipped. The one failure, test_azure_credentials.py::test_crypto_backend_uses_the_shared_chain, is pre-existing and unrelated (No module named 'azure.keyvault').

Version bumped to 0.2.8.

Every Redis factory built `redis.asyncio.Redis` with library defaults:
`health_check_interval=0` and `Retry(NoBackoff(), 0)` — no health checks and
zero retries. Managed Redis (Azure Cache for Redis, ElastiCache) reaps idle
connections and drops every connection during maintenance, failover, and
scale, so the first command after the server hangs up raised
`ConnectionError: Connection closed by server.` at the caller. This took down
lyzr-memory in production: every authenticated request 500'd.

Adds `resilient_client_kwargs()` — retry with exponential backoff (0.1/0.2/0.4s)
on ConnectionError and TimeoutError, a 30s health-check interval, TCP
keepalive, 5s socket timeouts, and a 100-connection ceiling — and applies it in
every factory on all three backends. Each factory takes **client_kwargs so a
call site can override any of it.

Tests assert the settings reach both the pool kwargs and the Connection object
built from them, that overrides win, and — by signature — that no factory can
be added without the passthrough.
…he caller

Azure Cache for Redis promotes the replica on failover and during maintenance.
A pooled connection can still be pointed at the node that just became a replica,
which answers writes with `-READONLY You can't write against a read only
replica.` redis-py raises that as ReadOnlyError, a ResponseError subclass — not
a ConnectionError — so the retry_on_error list added in the previous commit did
not cover it and the error surfaced to the caller.

Retrying disconnects and reconnects, which re-resolves DNS to the newly
promoted primary. Removing ReadOnlyError from the list fails 6 tests.
The live Azure deployment is Azure Managed Redis
(<name>.<region>.redis.azure.net:10000), which is TLS-only and ACL-based.
from_credentials defaulted ssl=False and had no way to pass a certificate
policy, so a caller could only produce a plaintext client. Connecting in
plaintext to a TLS-only port gets the connection closed by the server and
surfaces as `ConnectionError: Connection closed by server.` on every command —
verified against the live cache: plaintext fails, TLS pings.

- from_credentials: add ssl_cert_reqs.
- from_access_key: add username and ssl_cert_reqs, and document that the two
  Azure Redis products differ in host, port and auth (Cache for Redis is
  password-only on 6380; Managed Redis is ACL-based on 10000).

ssl_cert_reqs defaults to "required", not None: redis-py silently resolves None
to CERT_NONE, so a None default would turn off certificate verification for
every TLS caller without saying so.
receive() built its Message with bytes(m), but ServiceBusReceivedMessage does
not implement __bytes__ — its .body is a single-use generator of chunks. Every
receive against a live broker raised:

    TypeError: cannot convert 'ServiceBusReceivedMessage' object to bytes

so the Azure consume path had never worked. Verified against Service Bus
namespace lyzr-studio-dev-we-sbus: type(m.body) is 'generator' and
b"".join(m.body) yields the payload.

The fake in tests/test_messaging_azure.py defined __bytes__, which the real
type lacks, so bytes(m) passed in CI and failed on first contact with a broker.
The fake now mirrors the SDK: .body yields chunks and is single-use. Restoring
bytes(m) with the corrected fake fails 4 tests with the production error.

Bumps to 0.2.8.
@sonarqubecloud

Copy link
Copy Markdown

@abhi-bhat-lyzr
abhi-bhat-lyzr merged commit ee3f8c6 into LYZR-OSS:main Jul 29, 2026
2 checks passed
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.

3 participants