Redis: deliver rotating StreamingCredentialProvider tokens to long-lived BRPOP and pub/sub connections - #2563
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances Kombu’s Redis transport to properly apply rotated authentication tokens from redis.credentials.StreamingCredentialProvider to the transport’s two long-lived connections (the BRPOP consumer connection and the pub/sub LISTEN connection), preventing broker-forced disconnects when credentials expire.
Changes:
- Adds periodic, event-loop-driven re-authentication checks (
maybe_reauth) and a newreauth_check_intervaltransport option (default: 10s). - Flushes pending re-auth tokens onto the BRPOP connection at safe idle points (including
_brpop_read’sfinallyblock), and refreshes the RESP2 pub/sub connection by reconnecting when needed. - Adds extensive unit tests and new documentation describing streaming credential support and configuration.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
kombu/transport/redis.py |
Implements token flushing/reconnect logic and registers a new event-loop timer controlled by reauth_check_interval. |
t/unit/transport/test_redis.py |
Adds unit tests covering token detection, protocol gating (RESP2/RESP3), BRPOP/LISTEN behavior, and timer registration updates. |
docs/reference/kombu.transport.redis.rst |
Documents streaming credential behavior and the new reauth_check_interval option. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2563 +/- ##
==========================================
+ Coverage 82.81% 82.89% +0.08%
==========================================
Files 79 79
Lines 10333 10394 +61
Branches 1187 1195 +8
==========================================
+ Hits 8557 8616 +59
- Misses 1575 1577 +2
Partials 201 201 ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
docs/reference/kombu.transport.redis.rst:84
- This second snippet repeats the unsupported placement of
credential_providerin transport options, so copying the complete example still does not activate streaming credentials. Keep onlyreauth_check_intervalinbroker_transport_optionsand reference the corrected credential-provider setup above.
app.conf.broker_transport_options = {
"credential_provider": my_streaming_credential_provider,
"reauth_check_interval": 10,
}
docs/reference/kombu.transport.redis.rst:51
- This example does not configure the credential provider.
broker_transport_optionsbecomesConnection.transport_options, while the Redis channel obtains credentials fromConnection.client.credential_provider(seekombu/connection.py:189-198andkombu/transport/redis.py:1422-1440);credential_provideris also absent fromChannel.from_transport_options. Consequently, users following this example get passwordless/default Redis connections and the documented re-authentication never activates. Show a supported route such as passingcredential_provider=tokombu.Connection, or a broker URLcredential_providerquery parameter for an importable provider.
This issue also appears on line 81 of the same file.
app.conf.broker_transport_options = {
"credential_provider": my_streaming_credential_provider,
}
kombu/transport/redis.py:1727
- The newly documented configurable interval is not exercised: all updated registration tests use empty transport options and assert only the default
10. Add a test supplying a non-defaultreauth_check_intervaland asserting that value is passed tocall_repeatedly, analogous totest_configurable_health_check, so a typo or ignored option cannot regress unnoticed.
reauth_check_interval = connection.client.transport_options.get(
'reauth_check_interval',
DEFAULT_REAUTH_CHECK_INTERVAL
)
cycle._reauth_tref = loop.call_repeatedly(
reauth_check_interval,
cycle.maybe_reauth
Fixes #2509. Builds on #2507.
Summary
When a
redis.credentials.StreamingCredentialProvider(e.g. the Microsoft Entra IDprovider from
redis-entraid, or an AWS ElastiCache IAM provider) is used with theRedis transport, the rotated re-auth tokens it emits never reach the two connections
the transport holds for the entire lifetime of the worker: the BRPOP connection and
the pub/sub LISTEN connection. As a result the broker eventually severs those
connections when the original credentials expire (AWS ElastiCache with IAM auth enforces
a hard 12-hour connection limit), causing unacked messages to be redelivered, in-flight
tasks to be interrupted, and brief worker unavailability during the forced reconnect.
This PR makes the transport deliver those tokens itself, at safe points on the event loop.
Root cause
redis-py's streaming re-auth (
RegisterReAuthForPooledConnections) handles in-usepooled connections by calling
Connection.set_re_auth_token(token), which only storesthe token; the actual
AUTHcommand is deferred until the connection is released back tothe pool (
AfterConnectionReleasedEvent→ReAuthConnectionListener→Connection.re_auth()).The transport, however, pins two connections that are never released:
client.connectioninMultiChannelPoller._client_registered, andsubclient.subscribe()runs.Both live in
ConnectionPool._in_use_connectionsfor the worker's lifetime, sore_auth()is never triggered and the stored token is never flushed. #2507 correctlymade sure the re-auth listener gets registered (by passing
credential_providerthroughto
redis.Redis(...)); this PR makes sure the stored tokens actually get applied.What this PR does
A periodic, event-loop-driven flush of any pending token onto the two long-lived
connections. Because it runs on the single event-loop thread, it can never race the
socket reads:
Connection.re_auth(), but onlywhen the connection is live and no blocking pop is in flight (
not _in_poll), so theAUTHreply can't interleave with the pop's reply. It is flushed both from the periodictimer and from
_brpop_read'sfinallyblock (the guaranteed-idle moment rightafter a reply is consumed), so rotated tokens are applied promptly even under heavy
consumption. Crucially, the flush is skipped when the socket is already down: sending
AUTHon a disconnected connection would transparently reconnect a fresh socketbehind the poller's back (which
_register_BRPOPwould then never re-register, silentlystalling the channel). Instead the socket is left down and
_register_BRPOPreconnectsand re-registers it, with
on_connectre-authenticating via the currentcredentials. On an
AUTHfailure the socket is dropped and the stale token cleared, sothe reconnect's fresh
on_connectauth stands and the same (possibly expired) tokenisn't re-sent in place on every tick.
AUTH, soit is transparently reconnected (and re-subscribed) via the existing poller machinery,
which re-authenticates on
on_connectwith the current credentials. Fanout delivery isbest-effort, so the sub-second reconnect is far less disruptive than a broker-forced
disconnect. Under RESP3, redis-py re-authenticates pub/sub connections itself
(
RegisterReAuthForPubSub), so the transport leaves those untouched to avoiddouble-
AUTHand background-thread races.Wiring:
MultiChannelPoller.maybe_reauth()iterates channels and swallowsconnection_errors(like the existing
maybe_restore_messages/maybe_check_subclient_healthtimers, so atransient error can't tear down the event loop).
_reauth_treftimer is registered inregister_with_event_loop, with the samestale-timer cancellation as the existing timers.
reauth_check_interval(seconds, default10) controls the cadence.Design notes
_re_auth_token(the attribute redis-py itself populates for in-use pooled connections),so there is no coupling to any specific provider and no new import surface.
place on a live socket; whenever a socket must be dropped, reconnection and
re-registration are left to the existing
_register_BRPOP/_register_LISTENpaths, sothe poller's bookkeeping (
_chan_to_sock/_fd_to_chan) stays consistent.StreamingCredentialProviderisconfigured the token is never set, so every check is a cheap early return. Behaviour for
existing users is unchanged.
a sensible default.
Tests
28 new unit tests in
t/unit/transport/test_redis.pycovering:redis.Connectionto guard againstredis-py renaming the stored-token attribute),
ConnectionError/ResponseError/ disconnect-error paths, and that a rejected/failedAUTHboth drops the socket and clears the stale token,an unregistered socket (the socket is left down for
_register_BRPOP),missing-method tolerance),
_brpop_readfinallyhook, andThe 10 existing
register_with_event_looptests were updated for the new timer.Docs
docs/reference/kombu.transport.redis.rst.reauth_check_intervaldocumented in the transport module docstring.I tagged the docs
versionadded:: 5.7.0— happy to adjust to whatever the next release will be.🤖 Heavily assisted by Claude Code