Skip to content

fix(connection): convert sync Retry to async Retry in sync_to_async_redis - #817

Merged
vishal-bala merged 2 commits into
redis:mainfrom
Yarmoluk:fix/async-retry-conversion
Nov 6, 2026
Merged

vishal-bala merged 2 commits into
redis:mainfrom
Yarmoluk:fix/async-retry-conversion

Conversation

@Yarmoluk

@Yarmoluk Yarmoluk commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #792, from this review comment.

Motivation

sync_to_async_redis passed the sync client's retry keyword through unchanged. The async connection awaits retry.call_with_retry(...), and a sync redis.retry.Retry returns the coroutine without awaiting it, so the retry policy never ran: a converted client failed on the first command after CLIENT KILL, where a native async client recovers.

Changes

  • _async_connection_kwargs rebuilds a sync Retry as redis.asyncio.retry.Retry with the same backoff object, retry count and supported error set (_async_retry). retry=None and a missing keyword are left alone.
  • Unit tests in tests/unit/test_sync_to_async_redis.py pin the conversion; an integration test in tests/integration/test_connection.py kills the converted client's connection and checks the next command succeeds.

Release Notes

Async paths that convert a caller's sync client (SemanticCache, AsyncSearchIndex, and so on) now keep that client's retry policy, so they reconnect after a dropped connection the way a native async client does.

Reviewer Brief

Start here: redisvl/redis/connection.py, _async_retry. A client converted from sync to async keeps its retry policy only if this rebuilds it, and _async_connection_kwargs calls it for every conversion, including a plain Redis(), which carries a Retry by default.

Where it could fail

  • redisvl/redis/connection.py:383: _async_retry reads Retry._backoff and Retry._supported_errors, private attributes with no public getter in redis-py 6.3 through 8.1; if a future release renames one, every conversion raises AttributeError instead of quietly dropping the policy.
  • redisvl/redis/connection.py:421: a user's Retry subclass is rebuilt as the stock async Retry, so an overridden call_with_retry is not carried over (it could not have run on the async connection before this change either).

Not verified: cluster and Sentinel clients (both are rejected before this code runs); redis-py versions between 6.3.0 and 8.1.0 other than 7.4.1 and 8.0.1.

Beyond CI: test_converted_client_recovers_after_client_kill failed 20 of 20 runs on main and passed 20 of 20 with this change, on both redis-py 6.3.0 and 8.1.0. The unit tests pass on redis-py 6.3.0, 7.4.1, 8.0.1 and 8.1.0, and hardcoding the retry count in _async_retry makes test_sync_retry_is_rebuilt_as_the_async_retry fail. A sync client built with retry_on_timeout=True or retry_on_error=[BusyLoadingError] converts to an async connection whose Retry includes those errors, on 6.3.0 and 8.1.0.

Since the last brief: 57e898e tightens the tests only: a non-default retry count of 5, two redundant tests and assertions dropped, and a clearer CLIENT KILL test docstring.

Reviewed 57e898e · Claude Code (claude-opus-5-5) · lenses: correctness, security, design, tests, docs, Redis behaviour


Note

Medium Risk
Reliability fix on the sync-to-async path used by async cache/index flows; conversion relies on redis-py private Retry fields that could break on a future redis-py release.

Overview
sync_to_async_redis no longer forwards the sync client’s Retry unchanged. It now rebuilds it as redis.asyncio.retry.Retry via _async_retry, preserving backoff, retry count, and supported errors so reconnect-and-retry actually runs on async connections (sync Retry would leave coroutines unawaited and skip the policy).

_async_connection_kwargs applies that conversion when retry is a sync Retry; retry=None or a missing keyword is unchanged. Docs for sync_to_async_redis note the behavior.

Unit tests assert the async Retry matches the sync policy; an integration test **CLIENT KILL**s a converted client and expects the next ping to succeed.

Reviewed by Cursor Bugbot for commit 57e898e. Bugbot is set up for automated code reviews on this repo. Configure here.

…edis

A sync redis.retry.Retry passed the kwargs filter unchanged, and the async
connection awaits retry.call_with_retry, so the sync one returned the
coroutine unawaited and the policy never ran: a converted client failed on
the first command after CLIENT KILL where a native async client recovers.

_async_connection_kwargs now rebuilds a sync Retry as
redis.asyncio.retry.Retry with the same backoff object, retry count and
supported error set. retry=None and a missing keyword are left alone.

Unit tests pin the conversion on redis-py 6.3.0, 7.4.1, 8.0.1 and 8.1.0;
an integration test kills the converted client's connection and checks
the next command succeeds (it fails without the fix on redis-py 8.1.0).
@vishal-bala
vishal-bala self-requested a review November 5, 2026 09:40
@vishal-bala vishal-bala added the auto:patch Increment the patch version when merged label Nov 5, 2026

@vishal-bala vishal-bala left a comment

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.

Thanks for picking this up from the #792 thread! The fix looks right; I checked it on redis-py 6.3 through 8.1. A few small test tweaks and one docstring correction, and then we should be good to go.

Comment thread tests/unit/test_sync_to_async_redis.py Resolved
Comment thread tests/unit/test_sync_to_async_redis.py Resolved
Comment thread tests/unit/test_sync_to_async_redis.py Resolved
Comment thread tests/integration/test_connection.py Resolved
Use a non-default retry count so a converter that ignored the caller's
count would fail, drop two assertions the remaining test already covers,
and correct the integration test's docstring: the immediate ping after
CLIENT KILL fails on main on redis-py 6.3 as well as 8.1, so the test
exercises the policy on every supported version.
@Yarmoluk

Yarmoluk commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Pushed 57e898e:

  • The retry count is now 5 in both places. Hardcoding retries=3 in _async_retry now fails the test.
  • Dropped the redundant default-retry test and the second assert, along with the Connection import only they used. The remaining test's comment notes that a plain Redis() carries a sync Retry.
  • You're right about 6.3.0. I got the same result: test_converted_client_recovers_after_client_kill failed 20 of 20 on main and passed 20 of 20 here, on both redis-py 6.3.0 and 8.1.0. I've reworded the docstring and the Beyond CI line.

@vishal-bala
vishal-bala self-requested a review November 5, 2026 13:14
@vishal-bala vishal-bala added the auto:release Create a release when this PR is merged label Nov 6, 2026
@vishal-bala
vishal-bala merged commit a058124 into redis:main Nov 6, 2026
65 checks passed
@applied-ai-release-bot

Copy link
Copy Markdown

🚀 PR was released in v0.31.0 🚀

@applied-ai-release-bot applied-ai-release-bot Bot added the released This issue/pull request has been released. label Nov 6, 2026
@Yarmoluk
Yarmoluk deleted the fix/async-retry-conversion branch November 6, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto:patch Increment the patch version when merged auto:release Create a release when this PR is merged released This issue/pull request has been released.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants