Repository: redis/redis-py

Issue:
KeyError in asyncio cluster get_node_from_slot
**Version**: 4.3.4

**Platform**: Python 3.8.10, AWS Elasticache, Redis 6

**Description**:

When calling `await cluster.delete(key)`, some of our machines will throw a `KeyError` (e.g. `KeyError: 14097`) from this line:

https://github.com/redis/redis-py/blob/bea00b16c8043397c6bb6f3df45d48bceefa685f/redis/asyncio/cluster.py#L1041

This isn't consistent, though. We load balance across 3 servers, and only 2 out of the 3 serve this error when it happens.


Discussion:
I think I have a reproducible example:

```python
cluster = RedisCluster(
  host=config.REDIS_HOST,
  port=config.REDIS_PORT,
  decode_responses=True,
  read_from_replicas=read_from_replicas,
  skip_full_coverage_check=True,
)

await cluster.delete("foo")  # -> KeyError
```

However, if I run `cluster.ping()` first, it works:

```python
cluster = RedisCluster(
  host=config.REDIS_HOST,
  port=config.REDIS_PORT,
  decode_responses=True,
  read_from_replicas=read_from_replicas,
  skip_full_coverage_check=True,
)

await cluster.ping()
await cluster.delete("foo")  # -> Success
```

Similarly, if I run `await cluster.initialize()`, this also fixes it
Seems like there might be an auto-initialization piece missing in some of the asyncio cluster code, or `.initialize()` needs to be enforced as a required call

Resolving patch:
diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py
--- a/redis/commands/cluster.py
+++ b/redis/commands/cluster.py
@@ -316,6 +316,25 @@ async def _split_command_across_slots(self, command: str, *keys: KeyT) -> int:
         # Sum up the reply from each command
         return sum(await self._execute_pipeline_by_slot(command, slots_to_keys))
 
+    async def _execute_pipeline_by_slot(
+        self, command: str, slots_to_args: Mapping[int, Iterable[EncodableT]]
+    ) -> List[Any]:
+        if self._initialize:
+            await self.initialize()
+        read_from_replicas = self.read_from_replicas and command in READ_COMMANDS
+        pipe = self.pipeline()
+        [
+            pipe.execute_command(
+                command,
+                *slot_args,
+                target_nodes=[
+                    self.nodes_manager.get_node_from_slot(slot, read_from_replicas)
+                ],
+            )
+            for slot, slot_args in slots_to_args.items()
+        ]
+        return await pipe.execute()
+
 
 class ClusterManagementCommands(ManagementCommands):
     """


Test patch:
diff --git a/tests/test_asyncio/test_cluster.py b/tests/test_asyncio/test_cluster.py
--- a/tests/test_asyncio/test_cluster.py
+++ b/tests/test_asyncio/test_cluster.py
@@ -802,6 +802,15 @@ async def test_unlink(self, r: RedisCluster) -> None:
         await asyncio.sleep(0.1)
         assert await r.unlink(*d.keys()) == 0
 
+    async def test_initialize_before_execute_multi_key_command(
+        self, request: FixtureRequest
+    ) -> None:
+        # Test for issue https://github.com/redis/redis-py/issues/2437
+        url = request.config.getoption("--redis-url")
+        r = RedisCluster.from_url(url)
+        assert 0 == await r.exists("a", "b", "c")
+        await r.close()
+
     @skip_if_redis_enterprise()
     async def test_cluster_myid(self, r: RedisCluster) -> None:
         node = r.get_random_node()