Skip to content

Commit c4e4088

Browse files
authored
Add cluster support for functions (#2016)
* cluster support for functions * fix test_list_on_cluster mark * fix mark * cluster unstable url * fix * fix cluster url * skip tests * linters * linters
1 parent 6c798df commit c4e4088

File tree

4 files changed

+94
-52
lines changed

4 files changed

+94
-52
lines changed

redis/cluster.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,21 @@ class RedisCluster(RedisClusterCommands):
292292
[
293293
"FLUSHALL",
294294
"FLUSHDB",
295+
"FUNCTION DELETE",
296+
"FUNCTION FLUSH",
297+
"FUNCTION LIST",
298+
"FUNCTION LOAD",
299+
"FUNCTION RESTORE",
295300
"SCRIPT EXISTS",
296301
"SCRIPT FLUSH",
297302
"SCRIPT LOAD",
298303
],
299304
PRIMARIES,
300305
),
306+
list_keys_to_dict(
307+
["FUNCTION DUMP"],
308+
RANDOM,
309+
),
301310
list_keys_to_dict(
302311
[
303312
"CLUSTER COUNTKEYSINSLOT",
@@ -916,6 +925,10 @@ def determine_slot(self, *args):
916925
else:
917926
keys = self._get_command_keys(*args)
918927
if keys is None or len(keys) == 0:
928+
# FCALL can call a function with 0 keys, that means the function
929+
# can be run on any node so we can just return a random slot
930+
if command in ("FCALL", "FCALL_RO"):
931+
return random.randrange(0, REDIS_CLUSTER_HASH_SLOTS)
919932
raise RedisClusterException(
920933
"No way to dispatch this command to Redis Cluster. "
921934
"Missing key.\nYou can execute the command by specifying "

redis/commands/cluster.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .core import (
55
ACLCommands,
66
DataAccessCommands,
7+
FunctionCommands,
78
ManagementCommands,
89
PubSubCommands,
910
ScriptCommands,
@@ -213,6 +214,7 @@ class RedisClusterCommands(
213214
PubSubCommands,
214215
ClusterDataAccessCommands,
215216
ScriptCommands,
217+
FunctionCommands,
216218
RedisModuleCommands,
217219
):
218220
"""

tests/test_function.py

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from redis.exceptions import ResponseError
44

5+
from .conftest import skip_if_server_version_lt
6+
57
function = "redis.register_function('myfunc', function(keys, args) return args[1] end)"
68
function2 = "redis.register_function('hello', function() return 'Hello World' end)"
79
set_function = "redis.register_function('set', function(keys, args) \
@@ -10,42 +12,42 @@
1012
return redis.call('GET', keys[1]) end)"
1113

1214

13-
@pytest.mark.onlynoncluster
14-
# @skip_if_server_version_lt("7.0.0") turn on after redis 7 release
15+
@skip_if_server_version_lt("7.0.0")
1516
class TestFunction:
1617
@pytest.fixture(autouse=True)
17-
def reset_functions(self, unstable_r):
18-
unstable_r.function_flush()
18+
def reset_functions(self, r):
19+
r.function_flush()
1920

20-
def test_function_load(self, unstable_r):
21-
assert unstable_r.function_load("Lua", "mylib", function)
22-
assert unstable_r.function_load("Lua", "mylib", function, replace=True)
21+
def test_function_load(self, r):
22+
assert r.function_load("Lua", "mylib", function)
23+
assert r.function_load("Lua", "mylib", function, replace=True)
2324
with pytest.raises(ResponseError):
24-
unstable_r.function_load("Lua", "mylib", function)
25+
r.function_load("Lua", "mylib", function)
2526
with pytest.raises(ResponseError):
26-
unstable_r.function_load("Lua", "mylib2", function)
27+
r.function_load("Lua", "mylib2", function)
2728

28-
def test_function_delete(self, unstable_r):
29-
unstable_r.function_load("Lua", "mylib", set_function)
29+
def test_function_delete(self, r):
30+
r.function_load("Lua", "mylib", set_function)
3031
with pytest.raises(ResponseError):
31-
unstable_r.function_load("Lua", "mylib", set_function)
32-
assert unstable_r.fcall("set", 1, "foo", "bar") == "OK"
33-
assert unstable_r.function_delete("mylib")
32+
r.function_load("Lua", "mylib", set_function)
33+
assert r.fcall("set", 1, "foo", "bar") == "OK"
34+
assert r.function_delete("mylib")
3435
with pytest.raises(ResponseError):
35-
unstable_r.fcall("set", 1, "foo", "bar")
36-
assert unstable_r.function_load("Lua", "mylib", set_function)
36+
r.fcall("set", 1, "foo", "bar")
37+
assert r.function_load("Lua", "mylib", set_function)
3738

38-
def test_function_flush(self, unstable_r):
39-
unstable_r.function_load("Lua", "mylib", function)
40-
assert unstable_r.fcall("myfunc", 0, "hello") == "hello"
41-
assert unstable_r.function_flush()
39+
def test_function_flush(self, r):
40+
r.function_load("Lua", "mylib", function)
41+
assert r.fcall("myfunc", 0, "hello") == "hello"
42+
assert r.function_flush()
4243
with pytest.raises(ResponseError):
43-
unstable_r.fcall("myfunc", 0, "hello")
44+
r.fcall("myfunc", 0, "hello")
4445
with pytest.raises(ResponseError):
45-
unstable_r.function_flush("ABC")
46+
r.function_flush("ABC")
4647

47-
def test_function_list(self, unstable_r):
48-
unstable_r.function_load("Lua", "mylib", function)
48+
@pytest.mark.onlynoncluster
49+
def test_function_list(self, r):
50+
r.function_load("Lua", "mylib", function)
4951
res = [
5052
[
5153
"library_name",
@@ -58,37 +60,61 @@ def test_function_list(self, unstable_r):
5860
[["name", "myfunc", "description", None]],
5961
],
6062
]
61-
assert unstable_r.function_list() == res
62-
assert unstable_r.function_list(library="*lib") == res
63-
assert unstable_r.function_list(withcode=True)[0][9] == function
63+
assert r.function_list() == res
64+
assert r.function_list(library="*lib") == res
65+
assert r.function_list(withcode=True)[0][9] == function
66+
67+
@pytest.mark.onlycluster
68+
def test_function_list_on_cluster(self, r):
69+
r.function_load("Lua", "mylib", function)
70+
function_list = [
71+
[
72+
"library_name",
73+
"mylib",
74+
"engine",
75+
"LUA",
76+
"description",
77+
None,
78+
"functions",
79+
[["name", "myfunc", "description", None]],
80+
],
81+
]
82+
primaries = r.get_primaries()
83+
res = {}
84+
for node in primaries:
85+
res[node.name] = function_list
86+
assert r.function_list() == res
87+
assert r.function_list(library="*lib") == res
88+
node = primaries[0].name
89+
assert r.function_list(withcode=True)[node][0][9] == function
6490

65-
def test_fcall(self, unstable_r):
66-
unstable_r.function_load("Lua", "mylib", set_function)
67-
unstable_r.function_load("Lua", "mylib2", get_function)
68-
assert unstable_r.fcall("set", 1, "foo", "bar") == "OK"
69-
assert unstable_r.fcall("get", 1, "foo") == "bar"
91+
def test_fcall(self, r):
92+
r.function_load("Lua", "mylib", set_function)
93+
r.function_load("Lua", "mylib2", get_function)
94+
assert r.fcall("set", 1, "foo", "bar") == "OK"
95+
assert r.fcall("get", 1, "foo") == "bar"
7096
with pytest.raises(ResponseError):
71-
unstable_r.fcall("myfunc", 0, "hello")
97+
r.fcall("myfunc", 0, "hello")
7298

73-
def test_fcall_ro(self, unstable_r):
74-
unstable_r.function_load("Lua", "mylib", function)
75-
assert unstable_r.fcall_ro("myfunc", 0, "hello") == "hello"
76-
unstable_r.function_load("Lua", "mylib2", set_function)
99+
def test_fcall_ro(self, r):
100+
r.function_load("Lua", "mylib", function)
101+
assert r.fcall_ro("myfunc", 0, "hello") == "hello"
102+
r.function_load("Lua", "mylib2", set_function)
77103
with pytest.raises(ResponseError):
78-
unstable_r.fcall_ro("set", 1, "foo", "bar")
104+
r.fcall_ro("set", 1, "foo", "bar")
79105

80-
def test_function_dump_restore(self, unstable_r):
81-
unstable_r.function_load("Lua", "mylib", set_function)
82-
payload = unstable_r.function_dump()
83-
assert unstable_r.fcall("set", 1, "foo", "bar") == "OK"
84-
unstable_r.function_delete("mylib")
106+
def test_function_dump_restore(self, r):
107+
r.function_load("Lua", "mylib", set_function)
108+
payload = r.function_dump()
109+
assert r.fcall("set", 1, "foo", "bar") == "OK"
110+
r.function_delete("mylib")
85111
with pytest.raises(ResponseError):
86-
unstable_r.fcall("set", 1, "foo", "bar")
87-
assert unstable_r.function_restore(payload)
88-
assert unstable_r.fcall("set", 1, "foo", "bar") == "OK"
89-
unstable_r.function_load("Lua", "mylib2", get_function)
90-
assert unstable_r.fcall("get", 1, "foo") == "bar"
91-
unstable_r.function_delete("mylib")
92-
assert unstable_r.function_restore(payload, "FLUSH")
112+
r.fcall("set", 1, "foo", "bar")
113+
assert r.function_restore(payload)
114+
assert r.fcall("set", 1, "foo", "bar") == "OK"
115+
r.function_load("Lua", "mylib2", get_function)
116+
assert r.fcall("get", 1, "foo") == "bar"
117+
r.function_delete("mylib")
118+
assert r.function_restore(payload, "FLUSH")
93119
with pytest.raises(ResponseError):
94-
unstable_r.fcall("get", 1, "foo")
120+
r.fcall("get", 1, "foo")

tox.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,11 @@ extras =
283283
ocsp: cryptography, pyopenssl, requests
284284
setenv =
285285
CLUSTER_URL = "redis://localhost:16379/0"
286+
UNSTABLE_CLUSTER_URL = "redis://localhost:6372/0"
286287
commands =
287288
standalone: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' {posargs}
288289
standalone-uvloop: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop {posargs}
289-
cluster: pytest --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={env:CLUSTER_URL:} --redismod-url={env:CLUSTER_URL:} {posargs}
290+
cluster: pytest --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={env:CLUSTER_URL:} --redis-unstable-url={env:UNSTABLE_CLUSTER_URL:} {posargs}
290291
cluster-uvloop: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop {posargs}
291292

292293
[testenv:redis5]

0 commit comments

Comments
 (0)