Skip to content

Commit 827dcde

Browse files
[CLUSTER] Fix scan command cursors & Fix scan_iter (#2054)
* cluster/scan: fix return cursor & change default node to primaries * cluster/scan_iter: fix iteration Co-authored-by: dvora-h <[email protected]>
1 parent 032fd22 commit 827dcde

File tree

4 files changed

+96
-21
lines changed

4 files changed

+96
-21
lines changed

CHANGES

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11

22
* Add `items` parameter to `hset` signature
3-
* Create codeql-analysis.yml (#1988). Thanks @chayim
3+
* Create codeql-analysis.yml (#1988). Thanks @chayim
44
* Add limited support for Lua scripting with RedisCluster
55
* Implement `.lock()` method on RedisCluster
6+
* Fix cursor returned by SCAN for RedisCluster & change default target to PRIMARIES
7+
* Fix scan_iter for RedisCluster
68
* Remove verbose logging when initializing ClusterPubSub, ClusterPipeline or RedisCluster
79

810
* 4.1.3 (Feb 8, 2022)

redis/cluster.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
from collections import OrderedDict
99

10-
from redis.client import CaseInsensitiveDict, PubSub, Redis
10+
from redis.client import CaseInsensitiveDict, PubSub, Redis, parse_scan
1111
from redis.commands import CommandsParser, RedisClusterCommands
1212
from redis.connection import ConnectionPool, DefaultParser, Encoder, parse_url
1313
from redis.crc import REDIS_CLUSTER_HASH_SLOTS, key_slot
@@ -51,10 +51,14 @@ def get_connection(redis_node, *args, **options):
5151

5252

5353
def parse_scan_result(command, res, **options):
54-
keys_list = []
55-
for primary_res in res.values():
56-
keys_list += primary_res[1]
57-
return 0, keys_list
54+
cursors = {}
55+
ret = []
56+
for node_name, response in res.items():
57+
cursor, r = parse_scan(response, **options)
58+
cursors[node_name] = cursor
59+
ret += r
60+
61+
return cursors, ret
5862

5963

6064
def parse_pubsub_numsub(command, res, **options):
@@ -244,7 +248,6 @@ class RedisCluster(RedisClusterCommands):
244248
"INFO",
245249
"SHUTDOWN",
246250
"KEYS",
247-
"SCAN",
248251
"DBSIZE",
249252
"BGSAVE",
250253
"SLOWLOG GET",
@@ -298,6 +301,7 @@ class RedisCluster(RedisClusterCommands):
298301
"FUNCTION LIST",
299302
"FUNCTION LOAD",
300303
"FUNCTION RESTORE",
304+
"SCAN",
301305
"SCRIPT EXISTS",
302306
"SCRIPT FLUSH",
303307
"SCRIPT LOAD",

redis/commands/cluster.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Iterator, Union
2+
13
from redis.crc import key_slot
24
from redis.exceptions import RedisClusterException, RedisError
5+
from redis.typing import PatternT
36

47
from .core import (
58
ACLCommands,
@@ -206,6 +209,41 @@ def stralgo(
206209
**kwargs,
207210
)
208211

212+
def scan_iter(
213+
self,
214+
match: Union[PatternT, None] = None,
215+
count: Union[int, None] = None,
216+
_type: Union[str, None] = None,
217+
**kwargs,
218+
) -> Iterator:
219+
# Do the first query with cursor=0 for all nodes
220+
cursors, data = self.scan(match=match, count=count, _type=_type, **kwargs)
221+
yield from data
222+
223+
cursors = {name: cursor for name, cursor in cursors.items() if cursor != 0}
224+
if cursors:
225+
# Get nodes by name
226+
nodes = {name: self.get_node(node_name=name) for name in cursors.keys()}
227+
228+
# Iterate over each node till its cursor is 0
229+
kwargs.pop("target_nodes", None)
230+
while cursors:
231+
for name, cursor in cursors.items():
232+
cur, data = self.scan(
233+
cursor=cursor,
234+
match=match,
235+
count=count,
236+
_type=_type,
237+
target_nodes=nodes[name],
238+
**kwargs,
239+
)
240+
yield from data
241+
cursors[name] = cur[name]
242+
243+
cursors = {
244+
name: cursor for name, cursor in cursors.items() if cursor != 0
245+
}
246+
209247

210248
class RedisClusterCommands(
211249
ClusterMultiKeyCommands,

tests/test_cluster.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,29 +1773,60 @@ def test_cluster_scan(self, r):
17731773
r.set("a", 1)
17741774
r.set("b", 2)
17751775
r.set("c", 3)
1776-
cursor, keys = r.scan(target_nodes="primaries")
1777-
assert cursor == 0
1778-
assert set(keys) == {b"a", b"b", b"c"}
1779-
_, keys = r.scan(match="a", target_nodes="primaries")
1780-
assert set(keys) == {b"a"}
1776+
1777+
for target_nodes, nodes in zip(
1778+
["primaries", "replicas"], [r.get_primaries(), r.get_replicas()]
1779+
):
1780+
cursors, keys = r.scan(target_nodes=target_nodes)
1781+
assert sorted(keys) == [b"a", b"b", b"c"]
1782+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1783+
assert all(cursor == 0 for cursor in cursors.values())
1784+
1785+
cursors, keys = r.scan(match="a*", target_nodes=target_nodes)
1786+
assert sorted(keys) == [b"a"]
1787+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1788+
assert all(cursor == 0 for cursor in cursors.values())
17811789

17821790
@skip_if_server_version_lt("6.0.0")
17831791
def test_cluster_scan_type(self, r):
17841792
r.sadd("a-set", 1)
1793+
r.sadd("b-set", 1)
1794+
r.sadd("c-set", 1)
17851795
r.hset("a-hash", "foo", 2)
17861796
r.lpush("a-list", "aux", 3)
1787-
_, keys = r.scan(match="a*", _type="SET", target_nodes="primaries")
1788-
assert set(keys) == {b"a-set"}
1797+
1798+
for target_nodes, nodes in zip(
1799+
["primaries", "replicas"], [r.get_primaries(), r.get_replicas()]
1800+
):
1801+
cursors, keys = r.scan(_type="SET", target_nodes=target_nodes)
1802+
assert sorted(keys) == [b"a-set", b"b-set", b"c-set"]
1803+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1804+
assert all(cursor == 0 for cursor in cursors.values())
1805+
1806+
cursors, keys = r.scan(_type="SET", match="a*", target_nodes=target_nodes)
1807+
assert sorted(keys) == [b"a-set"]
1808+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1809+
assert all(cursor == 0 for cursor in cursors.values())
17891810

17901811
@skip_if_server_version_lt("2.8.0")
17911812
def test_cluster_scan_iter(self, r):
1792-
r.set("a", 1)
1793-
r.set("b", 2)
1794-
r.set("c", 3)
1795-
keys = list(r.scan_iter(target_nodes="primaries"))
1796-
assert set(keys) == {b"a", b"b", b"c"}
1797-
keys = list(r.scan_iter(match="a", target_nodes="primaries"))
1798-
assert set(keys) == {b"a"}
1813+
keys_all = []
1814+
keys_1 = []
1815+
for i in range(100):
1816+
s = str(i)
1817+
r.set(s, 1)
1818+
keys_all.append(s.encode("utf-8"))
1819+
if s.startswith("1"):
1820+
keys_1.append(s.encode("utf-8"))
1821+
keys_all.sort()
1822+
keys_1.sort()
1823+
1824+
for target_nodes in ["primaries", "replicas"]:
1825+
keys = r.scan_iter(target_nodes=target_nodes)
1826+
assert sorted(keys) == keys_all
1827+
1828+
keys = r.scan_iter(match="1*", target_nodes=target_nodes)
1829+
assert sorted(keys) == keys_1
17991830

18001831
def test_cluster_randomkey(self, r):
18011832
node = r.get_node_from_key("{foo}")

0 commit comments

Comments
 (0)