Skip to content

Commit 80f1d2f

Browse files
authored
Clusters should optionally require full slot coverage (#1845)
1 parent a5b4dcb commit 80f1d2f

File tree

2 files changed

+44
-117
lines changed

2 files changed

+44
-117
lines changed

redis/cluster.py

Lines changed: 12 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,7 @@ def __init__(
387387
port=6379,
388388
startup_nodes=None,
389389
cluster_error_retry_attempts=3,
390-
require_full_coverage=True,
391-
skip_full_coverage_check=False,
390+
require_full_coverage=False,
392391
reinitialize_steps=10,
393392
read_from_replicas=False,
394393
url=None,
@@ -404,16 +403,15 @@ def __init__(
404403
:port: 'int'
405404
Can be used to point to a startup node
406405
:require_full_coverage: 'bool'
407-
If set to True, as it is by default, all slots must be covered.
408-
If set to False and not all slots are covered, the instance
409-
creation will succeed only if 'cluster-require-full-coverage'
410-
configuration is set to 'no' in all of the cluster's nodes.
411-
Otherwise, RedisClusterException will be thrown.
412-
:skip_full_coverage_check: 'bool'
413-
If require_full_coverage is set to False, a check of
414-
cluster-require-full-coverage config will be executed against all
415-
nodes. Set skip_full_coverage_check to True to skip this check.
416-
Useful for clusters without the CONFIG command (like ElastiCache)
406+
When set to False (default value): the client will not require a
407+
full coverage of the slots. However, if not all slots are covered,
408+
and at least one node has 'cluster-require-full-coverage' set to
409+
'yes,' the server will throw a ClusterDownError for some key-based
410+
commands. See -
411+
https://redis.io/topics/cluster-tutorial#redis-cluster-configuration-parameters
412+
When set to True: all slots must be covered to construct the
413+
cluster client. If not all slots are covered, RedisClusterException
414+
will be thrown.
417415
:read_from_replicas: 'bool'
418416
Enable read from replicas in READONLY mode. You can read possibly
419417
stale data.
@@ -510,7 +508,6 @@ def __init__(
510508
startup_nodes=startup_nodes,
511509
from_url=from_url,
512510
require_full_coverage=require_full_coverage,
513-
skip_full_coverage_check=skip_full_coverage_check,
514511
**kwargs,
515512
)
516513

@@ -1111,8 +1108,7 @@ def __init__(
11111108
self,
11121109
startup_nodes,
11131110
from_url=False,
1114-
require_full_coverage=True,
1115-
skip_full_coverage_check=False,
1111+
require_full_coverage=False,
11161112
lock=None,
11171113
**kwargs,
11181114
):
@@ -1123,7 +1119,6 @@ def __init__(
11231119
self.populate_startup_nodes(startup_nodes)
11241120
self.from_url = from_url
11251121
self._require_full_coverage = require_full_coverage
1126-
self._skip_full_coverage_check = skip_full_coverage_check
11271122
self._moved_exception = None
11281123
self.connection_kwargs = kwargs
11291124
self.read_load_balancer = LoadBalancer()
@@ -1249,32 +1244,6 @@ def populate_startup_nodes(self, nodes):
12491244
for n in nodes:
12501245
self.startup_nodes[n.name] = n
12511246

1252-
def cluster_require_full_coverage(self, cluster_nodes):
1253-
"""
1254-
if exists 'cluster-require-full-coverage no' config on redis servers,
1255-
then even all slots are not covered, cluster still will be able to
1256-
respond
1257-
"""
1258-
1259-
def node_require_full_coverage(node):
1260-
try:
1261-
return (
1262-
"yes"
1263-
in node.redis_connection.config_get(
1264-
"cluster-require-full-coverage"
1265-
).values()
1266-
)
1267-
except ConnectionError:
1268-
return False
1269-
except Exception as e:
1270-
raise RedisClusterException(
1271-
'ERROR sending "config get cluster-require-full-coverage"'
1272-
f" command to redis server: {node.name}, {e}"
1273-
)
1274-
1275-
# at least one node should have cluster-require-full-coverage yes
1276-
return any(node_require_full_coverage(node) for node in cluster_nodes.values())
1277-
12781247
def check_slots_coverage(self, slots_cache):
12791248
# Validate if all slots are covered or if we should try next
12801249
# startup node
@@ -1450,29 +1419,9 @@ def initialize(self):
14501419
# isn't a full coverage
14511420
raise RedisClusterException(
14521421
f"All slots are not covered after query all startup_nodes. "
1453-
f"{len(self.slots_cache)} of {REDIS_CLUSTER_HASH_SLOTS} "
1422+
f"{len(tmp_slots)} of {REDIS_CLUSTER_HASH_SLOTS} "
14541423
f"covered..."
14551424
)
1456-
elif not fully_covered and not self._require_full_coverage:
1457-
# The user set require_full_coverage to False.
1458-
# In case of full coverage requirement in the cluster's Redis
1459-
# configurations, we will raise an exception. Otherwise, we may
1460-
# continue with partial coverage.
1461-
# see Redis Cluster configuration parameters in
1462-
# https://redis.io/topics/cluster-tutorial
1463-
if (
1464-
not self._skip_full_coverage_check
1465-
and self.cluster_require_full_coverage(tmp_nodes_cache)
1466-
):
1467-
raise RedisClusterException(
1468-
"Not all slots are covered but the cluster's "
1469-
"configuration requires full coverage. Set "
1470-
"cluster-require-full-coverage configuration to no on "
1471-
"all of the cluster nodes if you wish the cluster to "
1472-
"be able to serve without being fully covered."
1473-
f"{len(self.slots_cache)} of {REDIS_CLUSTER_HASH_SLOTS} "
1474-
f"covered..."
1475-
)
14761425

14771426
# Set the tmp variables to the real variables
14781427
self.nodes_cache = tmp_nodes_cache

tests/test_cluster.py

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
NoPermissionError,
2929
RedisClusterException,
3030
RedisError,
31+
ResponseError,
3132
)
3233
from redis.utils import str_if_bytes
3334
from tests.test_pubsub import wait_for_message
@@ -628,6 +629,32 @@ def test_get_node_from_key(self, r):
628629
assert replica.server_type == REPLICA
629630
assert replica in slot_nodes
630631

632+
def test_not_require_full_coverage_cluster_down_error(self, r):
633+
"""
634+
When require_full_coverage is set to False (default client config) and not
635+
all slots are covered, if one of the nodes has 'cluster-require_full_coverage'
636+
config set to 'yes' some key-based commands should throw ClusterDownError
637+
"""
638+
node = r.get_node_from_key("foo")
639+
missing_slot = r.keyslot("foo")
640+
assert r.set("foo", "bar") is True
641+
try:
642+
assert all(r.cluster_delslots(missing_slot))
643+
with pytest.raises(ClusterDownError):
644+
r.exists("foo")
645+
finally:
646+
try:
647+
# Add back the missing slot
648+
assert r.cluster_addslots(node, missing_slot) is True
649+
# Make sure we are not getting ClusterDownError anymore
650+
assert r.exists("foo") == 1
651+
except ResponseError as e:
652+
if f"Slot {missing_slot} is already busy" in str(e):
653+
# It can happen if the test failed to delete this slot
654+
pass
655+
else:
656+
raise e
657+
631658

632659
@pytest.mark.onlycluster
633660
class TestClusterRedisCommands:
@@ -1848,40 +1875,20 @@ def test_init_slots_cache_not_all_slots_covered(self):
18481875
[10923, 16383, ["127.0.0.1", 7002], ["127.0.0.1", 7005]],
18491876
]
18501877
with pytest.raises(RedisClusterException) as ex:
1851-
get_mocked_redis_client(
1852-
host=default_host, port=default_port, cluster_slots=cluster_slots
1853-
)
1854-
assert str(ex.value).startswith(
1855-
"All slots are not covered after query all startup_nodes."
1856-
)
1857-
1858-
def test_init_slots_cache_not_require_full_coverage_error(self):
1859-
"""
1860-
When require_full_coverage is set to False and not all slots are
1861-
covered, if one of the nodes has 'cluster-require_full_coverage'
1862-
config set to 'yes' the cluster initialization should fail
1863-
"""
1864-
# Missing slot 5460
1865-
cluster_slots = [
1866-
[0, 5459, ["127.0.0.1", 7000], ["127.0.0.1", 7003]],
1867-
[5461, 10922, ["127.0.0.1", 7001], ["127.0.0.1", 7004]],
1868-
[10923, 16383, ["127.0.0.1", 7002], ["127.0.0.1", 7005]],
1869-
]
1870-
1871-
with pytest.raises(RedisClusterException):
18721878
get_mocked_redis_client(
18731879
host=default_host,
18741880
port=default_port,
18751881
cluster_slots=cluster_slots,
1876-
require_full_coverage=False,
1877-
coverage_result="yes",
1882+
require_full_coverage=True,
18781883
)
1884+
assert str(ex.value).startswith(
1885+
"All slots are not covered after query all startup_nodes."
1886+
)
18791887

18801888
def test_init_slots_cache_not_require_full_coverage_success(self):
18811889
"""
18821890
When require_full_coverage is set to False and not all slots are
1883-
covered, if all of the nodes has 'cluster-require_full_coverage'
1884-
config set to 'no' the cluster initialization should succeed
1891+
covered the cluster client initialization should succeed
18851892
"""
18861893
# Missing slot 5460
18871894
cluster_slots = [
@@ -1895,39 +1902,10 @@ def test_init_slots_cache_not_require_full_coverage_success(self):
18951902
port=default_port,
18961903
cluster_slots=cluster_slots,
18971904
require_full_coverage=False,
1898-
coverage_result="no",
18991905
)
19001906

19011907
assert 5460 not in rc.nodes_manager.slots_cache
19021908

1903-
def test_init_slots_cache_not_require_full_coverage_skips_check(self):
1904-
"""
1905-
Test that when require_full_coverage is set to False and
1906-
skip_full_coverage_check is set to true, the cluster initialization
1907-
succeed without checking the nodes' Redis configurations
1908-
"""
1909-
# Missing slot 5460
1910-
cluster_slots = [
1911-
[0, 5459, ["127.0.0.1", 7000], ["127.0.0.1", 7003]],
1912-
[5461, 10922, ["127.0.0.1", 7001], ["127.0.0.1", 7004]],
1913-
[10923, 16383, ["127.0.0.1", 7002], ["127.0.0.1", 7005]],
1914-
]
1915-
1916-
with patch.object(
1917-
NodesManager, "cluster_require_full_coverage"
1918-
) as conf_check_mock:
1919-
rc = get_mocked_redis_client(
1920-
host=default_host,
1921-
port=default_port,
1922-
cluster_slots=cluster_slots,
1923-
require_full_coverage=False,
1924-
skip_full_coverage_check=True,
1925-
coverage_result="no",
1926-
)
1927-
1928-
assert conf_check_mock.called is False
1929-
assert 5460 not in rc.nodes_manager.slots_cache
1930-
19311909
def test_init_slots_cache(self):
19321910
"""
19331911
Test that slots cache can in initialized and all slots are covered

0 commit comments

Comments
 (0)