fix: hand redis-py a zero-retry Retry when retries are disabled - #27
Merged
Merged
Conversation
With retry.enabled=False the factory passed no Retry at all, and redis-py retries on its own when given none: three times since 6.0, ten since 8.0, with jittered backoff. A client built with socket_timeout=0.5 therefore took 10 to 18 seconds to fail against an unreachable Redis. build_redis_retry now always returns a Retry, Retry(NoBackoff(), 0) when retries are off, and build_base_redis_kwargs always carries it, so single and cluster clients, async and sync, get it alike. Rule 5 and the API row on the agents page and the retry section of the configuration guide say so.
This was referenced Sep 7, 2026
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.
With
retry.enabled=False— the default — the factory handed redis-py noRetry, and redis-py fills that in with a policy of its own:Retry(ExponentialWithJitterBackoff(base=1, cap=10), retries=3)since 6.0.0,retries=10since 8.0.0. Rule 5 on the agents page and the retry section of the configuration guide said the opposite, and a client built withsocket_timeout=0.5took 10 to 18 seconds to fail against an unreachable Redis instead of half a second.What changed
build_redis_retryalways returns aRetry: the kit's exponential one whenenabledandmax_attemptsare both set,Retry(NoBackoff(), 0)otherwise.build_base_redis_kwargsalways carries it, so single and cluster clients, async and sync, get it through the one path they already share; the factories are untouched. The return type narrows fromRetry | NonetoRetry— a caller that checked forNonekeeps working, and one that handed the result straight toRedis(retry=...)stops inheriting redis-py's default.Rule 5, the
enabledrow and thebuild_redis_retryrow on the agents page, and the retry section of the configuration guide now say what the code does.What I checked in redis-py
One venv per version, reading the
retrydefault offinspect.signatureand the connection classes off the source.Redis/asyncio.Redisretry=defaultNone; the connection turns it intoRetry(NoBackoff(), 0)cluster_error_retry_attempts=3Retry(ExponentialWithJitterBackoff, retries=3)retries=10retry=on all four client classes,redis.backoff.NoBackoffandRetry(NoBackoff(), 0)in bothredis.retryandredis.asyncio.retryare all there at 4.5.0, so nothing here needs a floor bump. Below 6.0 the explicit object is exactly what "nothing" used to mean for single clients. For cluster clients on 6.0+,retries=0also stops the cluster-level reinitialise-and-retry rounds, which is what "disabled" says. On 4.x/5.x the sync cluster's node connections already defaulted to zero; the async cluster's defaulted toRetry(default_backoff(), 3), so an async cluster on an old redis-py is the one place behaviour moves, from three node-level retries to none.What I rejected
Substituting the zero-retry object only inside
build_base_redis_kwargsand leavingbuild_redis_retryreturningNone. Smaller diff, butbuild_redis_retryis exported from the root, and anyone assembling their own kwargs from it would keep the trap.Jitter on the kit's own backoff.
ExponentialWithJitterBackoffis not in redis-py 4.x, so it is not a one-line swap underredis>=4.5.0, and rule 6 promises the first retry waits exactlybackoff_base. Its own issue if wanted.Tests
Unit: the kwargs handed to the client carry a
Retrywith zero retries andNoBackoffwhen retries are off, for single and cluster, async and sync; the enabled-path tests are unchanged and still pass. Integration: a GET against a pausedredis:7-alpinewithsocket_timeout=0.5and retries off raisesTimeoutErrorin under two seconds, async and sync. All eight fail on master — the integration pair at about nine seconds each — and pass here.The probe from the issue, before and after
retry_probe.py, unmodified, in a fresh venv with this tree installed editable and redis-py 8.1.0.Before, on master:
After, on this branch:
Gate
make check: ruff checkAll checks passed!, ruff format54 files already formatted, mypySuccess: no issues found in 25 source files.make test:138 passed, coverage 99.64% against the 90% floor.uv.lockuntouched.CHANGELOG.mdis release-please's.Follow-up
While measuring, the probe showed that with retries enabled the async factory hands
redis.retry.Retry— the sync class — toredis.asyncio.Redis, whoseexecute_commandawaitscall_with_retry; the sync one runs no loop, somax_attempts=3on an async client fails in 0.50 s whereredis.asyncio.retry.Retrytakes 3.41 s. Not folded in here; it is #26.Closes #25