Version: redis-py 8.0.1, code unchanged in 8.1.0. No server involved, the failure happens before any connection attempt.
Platform: Python 3.12 on Windows, also hit in production with Python 3.11 on Linux (Docker).
Description:
RedisCluster.execute_command lazily initializes the client on first use. The async ClusterPubSub.execute_command does not: for a subscribe it hashes the channel and looks the slot up in nodes_manager.slots_cache directly. On a client that has not executed any other command yet the slot cache is still an empty dict, so the lookup raises a bare KeyError carrying the slot number.
A second defect hides the intended error type: get_node_from_slot converts failed lookups to SlotNotCoveredError, but its except clause only catches IndexError and TypeError. Since slots_cache is a dict, a missing slot raises KeyError and skips the conversion, leaking an internal error to callers.
Minimal repro, no server needed:
import asyncio
from redis.asyncio.cluster import RedisCluster
async def main():
rc = RedisCluster.from_url("redis://127.0.0.1:9999/0")
await rc.pubsub().subscribe("some-channel")
asyncio.run(main())
Traceback (most recent call last):
...
File "redis/asyncio/client.py", line 1386, in subscribe
ret_val = await self.execute_command("SUBSCRIBE", *new_channels.keys())
File "redis/asyncio/cluster.py", line 3978, in execute_command
node = self.cluster.nodes_manager.get_node_from_slot(
File "redis/asyncio/cluster.py", line 1951, in get_node_from_slot
if len(self.slots_cache[slot]) > 1 and load_balancing_strategy:
~~~~~~~~~~~~~~~~^^^^^^
KeyError: 11554
Any application whose first operation on a fresh async cluster client is a pubsub subscribe hits this deterministically, against a perfectly healthy cluster. Open WebUI starts its pubsub listener right after creating the client, so every Redis Cluster deployment fails this way on startup (open-webui/open-webui#29138). Running any regular command first (for example ping) works around it, because that path runs initialize() and populates the slot cache.
Expected behavior: ClusterPubSub.execute_command initializes the cluster client before the slot lookup, the same way RedisCluster.execute_command does. Independently, get_node_from_slot should catch KeyError alongside IndexError and TypeError so an uncovered slot surfaces as SlotNotCoveredError.
Version: redis-py 8.0.1, code unchanged in 8.1.0. No server involved, the failure happens before any connection attempt.
Platform: Python 3.12 on Windows, also hit in production with Python 3.11 on Linux (Docker).
Description:
RedisCluster.execute_commandlazily initializes the client on first use. The asyncClusterPubSub.execute_commanddoes not: for a subscribe it hashes the channel and looks the slot up innodes_manager.slots_cachedirectly. On a client that has not executed any other command yet the slot cache is still an empty dict, so the lookup raises a bareKeyErrorcarrying the slot number.A second defect hides the intended error type:
get_node_from_slotconverts failed lookups toSlotNotCoveredError, but its except clause only catchesIndexErrorandTypeError. Sinceslots_cacheis a dict, a missing slot raisesKeyErrorand skips the conversion, leaking an internal error to callers.Minimal repro, no server needed:
Any application whose first operation on a fresh async cluster client is a pubsub subscribe hits this deterministically, against a perfectly healthy cluster. Open WebUI starts its pubsub listener right after creating the client, so every Redis Cluster deployment fails this way on startup (open-webui/open-webui#29138). Running any regular command first (for example
ping) works around it, because that path runsinitialize()and populates the slot cache.Expected behavior:
ClusterPubSub.execute_commandinitializes the cluster client before the slot lookup, the same wayRedisCluster.execute_commanddoes. Independently,get_node_from_slotshould catchKeyErroralongsideIndexErrorandTypeErrorso an uncovered slot surfaces asSlotNotCoveredError.