redis 5.3.0b5__tar.gz → 5.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {redis-5.3.0b5/redis.egg-info → redis-5.3.1}/PKG-INFO +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/client.py +6 -10
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/cluster.py +47 -12
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/connection.py +13 -3
- {redis-5.3.0b5 → redis-5.3.1}/redis/backoff.py +15 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/client.py +6 -8
- {redis-5.3.0b5 → redis-5.3.1}/redis/cluster.py +115 -30
- {redis-5.3.0b5 → redis-5.3.1}/redis/connection.py +19 -7
- {redis-5.3.0b5 → redis-5.3.1}/redis/utils.py +65 -0
- {redis-5.3.0b5 → redis-5.3.1/redis.egg-info}/PKG-INFO +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/redis.egg-info/SOURCES.txt +1 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis.egg-info/requires.txt +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/setup.py +2 -2
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_cluster.py +137 -14
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_connection.py +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_connection_pool.py +32 -32
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_credentials.py +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_encoding.py +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_retry.py +2 -2
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_sentinel.py +1 -1
- redis-5.3.1/tests/test_backoff.py +17 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_cache.py +3 -3
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_cluster.py +157 -18
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_connection_pool.py +45 -30
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_credentials.py +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_multiprocessing.py +39 -5
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_retry.py +2 -2
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_sentinel.py +1 -1
- {redis-5.3.0b5 → redis-5.3.1}/INSTALL +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/LICENSE +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/MANIFEST.in +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/README.md +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/base.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/encoders.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/helpers.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/hiredis.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/resp2.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/resp3.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/_parsers/socket.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/lock.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/retry.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/sentinel.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/asyncio/utils.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/auth/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/auth/err.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/auth/idp.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/auth/token.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/auth/token_manager.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/cache.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/bf/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/bf/commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/bf/info.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/cluster.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/core.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/edge.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/exceptions.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/execution_plan.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/node.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/path.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/graph/query_result.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/helpers.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/json/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/json/_util.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/json/commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/json/decoders.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/json/path.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/redismodules.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/_util.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/aggregation.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/document.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/field.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/indexDefinition.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/query.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/querystring.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/reducers.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/result.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/search/suggestion.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/sentinel.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/timeseries/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/timeseries/commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/timeseries/info.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/commands/timeseries/utils.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/crc.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/credentials.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/event.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/exceptions.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/lock.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/ocsp.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/retry.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/sentinel.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis/typing.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis.egg-info/dependency_links.txt +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/redis.egg-info/top_level.txt +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/setup.cfg +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/conftest.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/mocks.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/ssl_utils.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/compat.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/conftest.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/mocks.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_bloom.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_connect.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_cwe_404.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_graph.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_hash.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_json.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_lock.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_monitor.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_pipeline.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_pubsub.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_scripting.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_search.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_sentinel_managed_connection.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/test_timeseries.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/testdata/jsontestdata.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/testdata/titles.csv +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_asyncio/testdata/will_play_text.csv.bz2 +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_auth/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_auth/test_token.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_auth/test_token_manager.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_bloom.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_command_parser.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_commands.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_connect.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_connection.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_encoding.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_function.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_graph.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_graph_utils/__init__.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_graph_utils/test_edge.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_graph_utils/test_node.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_graph_utils/test_path.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_hash.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_helpers.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_json.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_lock.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_monitor.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_parsers/test_helpers.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_pipeline.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_pubsub.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_scripting.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_search.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_ssl.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_timeseries.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/test_utils.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/testdata/jsontestdata.py +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/testdata/titles.csv +0 -0
- {redis-5.3.0b5 → redis-5.3.1}/tests/testdata/will_play_text.csv.bz2 +0 -0
|
@@ -375,7 +375,7 @@ class Redis(
|
|
|
375
375
|
if self.single_connection_client:
|
|
376
376
|
async with self._single_conn_lock:
|
|
377
377
|
if self.connection is None:
|
|
378
|
-
self.connection = await self.connection_pool.get_connection(
|
|
378
|
+
self.connection = await self.connection_pool.get_connection()
|
|
379
379
|
|
|
380
380
|
self._event_dispatcher.dispatch(
|
|
381
381
|
AfterSingleConnectionInstantiationEvent(
|
|
@@ -638,7 +638,7 @@ class Redis(
|
|
|
638
638
|
await self.initialize()
|
|
639
639
|
pool = self.connection_pool
|
|
640
640
|
command_name = args[0]
|
|
641
|
-
conn = self.connection or await pool.get_connection(
|
|
641
|
+
conn = self.connection or await pool.get_connection()
|
|
642
642
|
|
|
643
643
|
if self.single_connection_client:
|
|
644
644
|
await self._single_conn_lock.acquire()
|
|
@@ -712,7 +712,7 @@ class Monitor:
|
|
|
712
712
|
|
|
713
713
|
async def connect(self):
|
|
714
714
|
if self.connection is None:
|
|
715
|
-
self.connection = await self.connection_pool.get_connection(
|
|
715
|
+
self.connection = await self.connection_pool.get_connection()
|
|
716
716
|
|
|
717
717
|
async def __aenter__(self):
|
|
718
718
|
await self.connect()
|
|
@@ -900,9 +900,7 @@ class PubSub:
|
|
|
900
900
|
Ensure that the PubSub is connected
|
|
901
901
|
"""
|
|
902
902
|
if self.connection is None:
|
|
903
|
-
self.connection = await self.connection_pool.get_connection(
|
|
904
|
-
"pubsub", self.shard_hint
|
|
905
|
-
)
|
|
903
|
+
self.connection = await self.connection_pool.get_connection()
|
|
906
904
|
# register a callback that re-subscribes to any channels we
|
|
907
905
|
# were listening to when we were disconnected
|
|
908
906
|
self.connection.register_connect_callback(self.on_connect)
|
|
@@ -1370,9 +1368,7 @@ class Pipeline(Redis): # lgtm [py/init-calls-subclass]
|
|
|
1370
1368
|
conn = self.connection
|
|
1371
1369
|
# if this is the first call, we need a connection
|
|
1372
1370
|
if not conn:
|
|
1373
|
-
conn = await self.connection_pool.get_connection(
|
|
1374
|
-
command_name, self.shard_hint
|
|
1375
|
-
)
|
|
1371
|
+
conn = await self.connection_pool.get_connection()
|
|
1376
1372
|
self.connection = conn
|
|
1377
1373
|
|
|
1378
1374
|
return await conn.retry.call_with_retry(
|
|
@@ -1568,7 +1564,7 @@ class Pipeline(Redis): # lgtm [py/init-calls-subclass]
|
|
|
1568
1564
|
|
|
1569
1565
|
conn = self.connection
|
|
1570
1566
|
if not conn:
|
|
1571
|
-
conn = await self.connection_pool.get_connection(
|
|
1567
|
+
conn = await self.connection_pool.get_connection()
|
|
1572
1568
|
# assign to self.connection so reset() releases the connection
|
|
1573
1569
|
# back to the pool after we're done
|
|
1574
1570
|
self.connection = conn
|
|
@@ -39,6 +39,7 @@ from redis.cluster import (
|
|
|
39
39
|
SLOT_ID,
|
|
40
40
|
AbstractRedisCluster,
|
|
41
41
|
LoadBalancer,
|
|
42
|
+
LoadBalancingStrategy,
|
|
42
43
|
block_pipeline_command,
|
|
43
44
|
get_node_name,
|
|
44
45
|
parse_cluster_slots,
|
|
@@ -67,6 +68,7 @@ from redis.exceptions import (
|
|
|
67
68
|
)
|
|
68
69
|
from redis.typing import AnyKeyT, EncodableT, KeyT
|
|
69
70
|
from redis.utils import (
|
|
71
|
+
deprecated_args,
|
|
70
72
|
deprecated_function,
|
|
71
73
|
dict_merge,
|
|
72
74
|
get_lib_version,
|
|
@@ -133,9 +135,17 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
133
135
|
| See:
|
|
134
136
|
https://redis.io/docs/manual/scaling/#redis-cluster-configuration-parameters
|
|
135
137
|
:param read_from_replicas:
|
|
136
|
-
|
|
|
138
|
+
| @deprecated - please use load_balancing_strategy instead
|
|
139
|
+
| Enable read from replicas in READONLY mode.
|
|
137
140
|
When set to true, read commands will be assigned between the primary and
|
|
138
141
|
its replications in a Round-Robin manner.
|
|
142
|
+
The data read from replicas is eventually consistent
|
|
143
|
+
with the data in primary nodes.
|
|
144
|
+
:param load_balancing_strategy:
|
|
145
|
+
| Enable read from replicas in READONLY mode and defines the load balancing
|
|
146
|
+
strategy that will be used for cluster node selection.
|
|
147
|
+
The data read from replicas is eventually consistent
|
|
148
|
+
with the data in primary nodes.
|
|
139
149
|
:param reinitialize_steps:
|
|
140
150
|
| Specifies the number of MOVED errors that need to occur before reinitializing
|
|
141
151
|
the whole cluster topology. If a MOVED error occurs and the cluster does not
|
|
@@ -228,6 +238,11 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
228
238
|
"result_callbacks",
|
|
229
239
|
)
|
|
230
240
|
|
|
241
|
+
@deprecated_args(
|
|
242
|
+
args_to_warn=["read_from_replicas"],
|
|
243
|
+
reason="Please configure the 'load_balancing_strategy' instead",
|
|
244
|
+
version="5.3.0",
|
|
245
|
+
)
|
|
231
246
|
def __init__(
|
|
232
247
|
self,
|
|
233
248
|
host: Optional[str] = None,
|
|
@@ -236,6 +251,7 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
236
251
|
startup_nodes: Optional[List["ClusterNode"]] = None,
|
|
237
252
|
require_full_coverage: bool = True,
|
|
238
253
|
read_from_replicas: bool = False,
|
|
254
|
+
load_balancing_strategy: Optional[LoadBalancingStrategy] = None,
|
|
239
255
|
reinitialize_steps: int = 5,
|
|
240
256
|
cluster_error_retry_attempts: int = 3,
|
|
241
257
|
connection_error_retry_attempts: int = 3,
|
|
@@ -335,7 +351,7 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
335
351
|
}
|
|
336
352
|
)
|
|
337
353
|
|
|
338
|
-
if read_from_replicas:
|
|
354
|
+
if read_from_replicas or load_balancing_strategy:
|
|
339
355
|
# Call our on_connect function to configure READONLY mode
|
|
340
356
|
kwargs["redis_connect_func"] = self.on_connect
|
|
341
357
|
|
|
@@ -384,6 +400,7 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
384
400
|
)
|
|
385
401
|
self.encoder = Encoder(encoding, encoding_errors, decode_responses)
|
|
386
402
|
self.read_from_replicas = read_from_replicas
|
|
403
|
+
self.load_balancing_strategy = load_balancing_strategy
|
|
387
404
|
self.reinitialize_steps = reinitialize_steps
|
|
388
405
|
self.cluster_error_retry_attempts = cluster_error_retry_attempts
|
|
389
406
|
self.connection_error_retry_attempts = connection_error_retry_attempts
|
|
@@ -602,6 +619,7 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
602
619
|
self.nodes_manager.get_node_from_slot(
|
|
603
620
|
await self._determine_slot(command, *args),
|
|
604
621
|
self.read_from_replicas and command in READ_COMMANDS,
|
|
622
|
+
self.load_balancing_strategy if command in READ_COMMANDS else None,
|
|
605
623
|
)
|
|
606
624
|
]
|
|
607
625
|
|
|
@@ -782,7 +800,13 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
782
800
|
# refresh the target node
|
|
783
801
|
slot = await self._determine_slot(*args)
|
|
784
802
|
target_node = self.nodes_manager.get_node_from_slot(
|
|
785
|
-
slot,
|
|
803
|
+
slot,
|
|
804
|
+
self.read_from_replicas and args[0] in READ_COMMANDS,
|
|
805
|
+
(
|
|
806
|
+
self.load_balancing_strategy
|
|
807
|
+
if args[0] in READ_COMMANDS
|
|
808
|
+
else None
|
|
809
|
+
),
|
|
786
810
|
)
|
|
787
811
|
moved = False
|
|
788
812
|
|
|
@@ -799,10 +823,16 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
|
|
|
799
823
|
# and try again with the new setup
|
|
800
824
|
await self.aclose()
|
|
801
825
|
raise
|
|
802
|
-
except ClusterDownError:
|
|
826
|
+
except (ClusterDownError, SlotNotCoveredError):
|
|
803
827
|
# ClusterDownError can occur during a failover and to get
|
|
804
828
|
# self-healed, we will try to reinitialize the cluster layout
|
|
805
829
|
# and retry executing the command
|
|
830
|
+
|
|
831
|
+
# SlotNotCoveredError can occur when the cluster is not fully
|
|
832
|
+
# initialized or can be temporary issue.
|
|
833
|
+
# We will try to reinitialize the cluster topology
|
|
834
|
+
# and retry executing the command
|
|
835
|
+
|
|
806
836
|
await self.aclose()
|
|
807
837
|
await asyncio.sleep(0.25)
|
|
808
838
|
raise
|
|
@@ -1177,9 +1207,7 @@ class NodesManager:
|
|
|
1177
1207
|
return self.nodes_cache.get(node_name)
|
|
1178
1208
|
else:
|
|
1179
1209
|
raise DataError(
|
|
1180
|
-
"get_node requires one of the following: "
|
|
1181
|
-
"1. node name "
|
|
1182
|
-
"2. host and port"
|
|
1210
|
+
"get_node requires one of the following: 1. node name 2. host and port"
|
|
1183
1211
|
)
|
|
1184
1212
|
|
|
1185
1213
|
def set_nodes(
|
|
@@ -1239,17 +1267,24 @@ class NodesManager:
|
|
|
1239
1267
|
self._moved_exception = None
|
|
1240
1268
|
|
|
1241
1269
|
def get_node_from_slot(
|
|
1242
|
-
self,
|
|
1270
|
+
self,
|
|
1271
|
+
slot: int,
|
|
1272
|
+
read_from_replicas: bool = False,
|
|
1273
|
+
load_balancing_strategy=None,
|
|
1243
1274
|
) -> "ClusterNode":
|
|
1244
1275
|
if self._moved_exception:
|
|
1245
1276
|
self._update_moved_slots()
|
|
1246
1277
|
|
|
1278
|
+
if read_from_replicas is True and load_balancing_strategy is None:
|
|
1279
|
+
load_balancing_strategy = LoadBalancingStrategy.ROUND_ROBIN
|
|
1280
|
+
|
|
1247
1281
|
try:
|
|
1248
|
-
if
|
|
1249
|
-
# get the server index
|
|
1282
|
+
if len(self.slots_cache[slot]) > 1 and load_balancing_strategy:
|
|
1283
|
+
# get the server index using the strategy defined
|
|
1284
|
+
# in load_balancing_strategy
|
|
1250
1285
|
primary_name = self.slots_cache[slot][0].name
|
|
1251
1286
|
node_idx = self.read_load_balancer.get_server_index(
|
|
1252
|
-
primary_name, len(self.slots_cache[slot])
|
|
1287
|
+
primary_name, len(self.slots_cache[slot]), load_balancing_strategy
|
|
1253
1288
|
)
|
|
1254
1289
|
return self.slots_cache[slot][node_idx]
|
|
1255
1290
|
return self.slots_cache[slot][0]
|
|
@@ -1361,7 +1396,7 @@ class NodesManager:
|
|
|
1361
1396
|
if len(disagreements) > 5:
|
|
1362
1397
|
raise RedisClusterException(
|
|
1363
1398
|
f"startup_nodes could not agree on a valid "
|
|
1364
|
-
f
|
|
1399
|
+
f"slots cache: {', '.join(disagreements)}"
|
|
1365
1400
|
)
|
|
1366
1401
|
|
|
1367
1402
|
# Validate if all slots are covered or if we should try next startup node
|
|
@@ -29,7 +29,7 @@ from urllib.parse import ParseResult, parse_qs, unquote, urlparse
|
|
|
29
29
|
|
|
30
30
|
from ..auth.token import TokenInterface
|
|
31
31
|
from ..event import AsyncAfterConnectionReleasedEvent, EventDispatcher
|
|
32
|
-
from ..utils import format_error_message
|
|
32
|
+
from ..utils import deprecated_args, format_error_message
|
|
33
33
|
|
|
34
34
|
# the functionality is available in 3.11.x but has a major issue before
|
|
35
35
|
# 3.11.3. See https://github.com/redis/redis-py/issues/2633
|
|
@@ -1087,7 +1087,12 @@ class ConnectionPool:
|
|
|
1087
1087
|
or len(self._in_use_connections) < self.max_connections
|
|
1088
1088
|
)
|
|
1089
1089
|
|
|
1090
|
-
|
|
1090
|
+
@deprecated_args(
|
|
1091
|
+
args_to_warn=["*"],
|
|
1092
|
+
reason="Use get_connection() without args instead",
|
|
1093
|
+
version="5.3.0",
|
|
1094
|
+
)
|
|
1095
|
+
async def get_connection(self, command_name=None, *keys, **options):
|
|
1091
1096
|
async with self._lock:
|
|
1092
1097
|
"""Get a connected connection from the pool"""
|
|
1093
1098
|
connection = self.get_available_connection()
|
|
@@ -1255,7 +1260,12 @@ class BlockingConnectionPool(ConnectionPool):
|
|
|
1255
1260
|
self._condition = asyncio.Condition()
|
|
1256
1261
|
self.timeout = timeout
|
|
1257
1262
|
|
|
1258
|
-
|
|
1263
|
+
@deprecated_args(
|
|
1264
|
+
args_to_warn=["*"],
|
|
1265
|
+
reason="Use get_connection() without args instead",
|
|
1266
|
+
version="5.3.0",
|
|
1267
|
+
)
|
|
1268
|
+
async def get_connection(self, command_name=None, *keys, **options):
|
|
1259
1269
|
"""Gets a connection from the pool, blocking until one is available"""
|
|
1260
1270
|
try:
|
|
1261
1271
|
async with self._condition:
|
|
@@ -110,5 +110,20 @@ class DecorrelatedJitterBackoff(AbstractBackoff):
|
|
|
110
110
|
return self._previous_backoff
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
class ExponentialWithJitterBackoff(AbstractBackoff):
|
|
114
|
+
"""Exponential backoff upon failure, with jitter"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None:
|
|
117
|
+
"""
|
|
118
|
+
`cap`: maximum backoff time in seconds
|
|
119
|
+
`base`: base backoff time in seconds
|
|
120
|
+
"""
|
|
121
|
+
self._cap = cap
|
|
122
|
+
self._base = base
|
|
123
|
+
|
|
124
|
+
def compute(self, failures: int) -> float:
|
|
125
|
+
return min(self._cap, random.random() * self._base * 2**failures)
|
|
126
|
+
|
|
127
|
+
|
|
113
128
|
def default_backoff():
|
|
114
129
|
return EqualJitterBackoff()
|
|
@@ -366,7 +366,7 @@ class Redis(RedisModuleCommands, CoreCommands, SentinelCommands):
|
|
|
366
366
|
self.connection = None
|
|
367
367
|
self._single_connection_client = single_connection_client
|
|
368
368
|
if self._single_connection_client:
|
|
369
|
-
self.connection = self.connection_pool.get_connection(
|
|
369
|
+
self.connection = self.connection_pool.get_connection()
|
|
370
370
|
self._event_dispatcher.dispatch(
|
|
371
371
|
AfterSingleConnectionInstantiationEvent(
|
|
372
372
|
self.connection, ClientType.SYNC, self.single_connection_lock
|
|
@@ -608,7 +608,7 @@ class Redis(RedisModuleCommands, CoreCommands, SentinelCommands):
|
|
|
608
608
|
"""Execute a command and return a parsed response"""
|
|
609
609
|
pool = self.connection_pool
|
|
610
610
|
command_name = args[0]
|
|
611
|
-
conn = self.connection or pool.get_connection(
|
|
611
|
+
conn = self.connection or pool.get_connection()
|
|
612
612
|
|
|
613
613
|
if self._single_connection_client:
|
|
614
614
|
self.single_connection_lock.acquire()
|
|
@@ -667,7 +667,7 @@ class Monitor:
|
|
|
667
667
|
|
|
668
668
|
def __init__(self, connection_pool):
|
|
669
669
|
self.connection_pool = connection_pool
|
|
670
|
-
self.connection = self.connection_pool.get_connection(
|
|
670
|
+
self.connection = self.connection_pool.get_connection()
|
|
671
671
|
|
|
672
672
|
def __enter__(self):
|
|
673
673
|
self.connection.send_command("MONITOR")
|
|
@@ -840,9 +840,7 @@ class PubSub:
|
|
|
840
840
|
# subscribed to one or more channels
|
|
841
841
|
|
|
842
842
|
if self.connection is None:
|
|
843
|
-
self.connection = self.connection_pool.get_connection(
|
|
844
|
-
"pubsub", self.shard_hint
|
|
845
|
-
)
|
|
843
|
+
self.connection = self.connection_pool.get_connection()
|
|
846
844
|
# register a callback that re-subscribes to any channels we
|
|
847
845
|
# were listening to when we were disconnected
|
|
848
846
|
self.connection.register_connect_callback(self.on_connect)
|
|
@@ -1397,7 +1395,7 @@ class Pipeline(Redis):
|
|
|
1397
1395
|
conn = self.connection
|
|
1398
1396
|
# if this is the first call, we need a connection
|
|
1399
1397
|
if not conn:
|
|
1400
|
-
conn = self.connection_pool.get_connection(
|
|
1398
|
+
conn = self.connection_pool.get_connection()
|
|
1401
1399
|
self.connection = conn
|
|
1402
1400
|
|
|
1403
1401
|
return conn.retry.call_with_retry(
|
|
@@ -1583,7 +1581,7 @@ class Pipeline(Redis):
|
|
|
1583
1581
|
|
|
1584
1582
|
conn = self.connection
|
|
1585
1583
|
if not conn:
|
|
1586
|
-
conn = self.connection_pool.get_connection(
|
|
1584
|
+
conn = self.connection_pool.get_connection()
|
|
1587
1585
|
# assign to self.connection so reset() releases the connection
|
|
1588
1586
|
# back to the pool after we're done
|
|
1589
1587
|
self.connection = conn
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
6
|
from collections import OrderedDict
|
|
7
|
+
from enum import Enum
|
|
7
8
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
8
9
|
|
|
9
10
|
from redis._parsers import CommandsParser, Encoder
|
|
@@ -42,6 +43,7 @@ from redis.lock import Lock
|
|
|
42
43
|
from redis.retry import Retry
|
|
43
44
|
from redis.utils import (
|
|
44
45
|
HIREDIS_AVAILABLE,
|
|
46
|
+
deprecated_args,
|
|
45
47
|
dict_merge,
|
|
46
48
|
list_keys_to_dict,
|
|
47
49
|
merge_result,
|
|
@@ -54,10 +56,13 @@ def get_node_name(host: str, port: Union[str, int]) -> str:
|
|
|
54
56
|
return f"{host}:{port}"
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
@deprecated_args(
|
|
60
|
+
allowed_args=["redis_node"],
|
|
61
|
+
reason="Use get_connection(redis_node) instead",
|
|
62
|
+
version="5.3.0",
|
|
63
|
+
)
|
|
57
64
|
def get_connection(redis_node, *args, **options):
|
|
58
|
-
return redis_node.connection or redis_node.connection_pool.get_connection(
|
|
59
|
-
args[0], **options
|
|
60
|
-
)
|
|
65
|
+
return redis_node.connection or redis_node.connection_pool.get_connection()
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
def parse_scan_result(command, res, **options):
|
|
@@ -424,7 +429,12 @@ class AbstractRedisCluster:
|
|
|
424
429
|
list_keys_to_dict(["SCRIPT FLUSH"], lambda command, res: all(res.values())),
|
|
425
430
|
)
|
|
426
431
|
|
|
427
|
-
ERRORS_ALLOW_RETRY = (
|
|
432
|
+
ERRORS_ALLOW_RETRY = (
|
|
433
|
+
ConnectionError,
|
|
434
|
+
TimeoutError,
|
|
435
|
+
ClusterDownError,
|
|
436
|
+
SlotNotCoveredError,
|
|
437
|
+
)
|
|
428
438
|
|
|
429
439
|
def replace_default_node(self, target_node: "ClusterNode" = None) -> None:
|
|
430
440
|
"""Replace the default cluster node.
|
|
@@ -496,6 +506,11 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
496
506
|
"""
|
|
497
507
|
return cls(url=url, **kwargs)
|
|
498
508
|
|
|
509
|
+
@deprecated_args(
|
|
510
|
+
args_to_warn=["read_from_replicas"],
|
|
511
|
+
reason="Please configure the 'load_balancing_strategy' instead",
|
|
512
|
+
version="5.3.0",
|
|
513
|
+
)
|
|
499
514
|
def __init__(
|
|
500
515
|
self,
|
|
501
516
|
host: Optional[str] = None,
|
|
@@ -506,6 +521,7 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
506
521
|
require_full_coverage: bool = False,
|
|
507
522
|
reinitialize_steps: int = 5,
|
|
508
523
|
read_from_replicas: bool = False,
|
|
524
|
+
load_balancing_strategy: Optional["LoadBalancingStrategy"] = None,
|
|
509
525
|
dynamic_startup_nodes: bool = True,
|
|
510
526
|
url: Optional[str] = None,
|
|
511
527
|
address_remap: Optional[Callable[[Tuple[str, int]], Tuple[str, int]]] = None,
|
|
@@ -534,11 +550,17 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
534
550
|
cluster client. If not all slots are covered, RedisClusterException
|
|
535
551
|
will be thrown.
|
|
536
552
|
:param read_from_replicas:
|
|
553
|
+
@deprecated - please use load_balancing_strategy instead
|
|
537
554
|
Enable read from replicas in READONLY mode. You can read possibly
|
|
538
555
|
stale data.
|
|
539
556
|
When set to true, read commands will be assigned between the
|
|
540
557
|
primary and its replications in a Round-Robin manner.
|
|
541
|
-
|
|
558
|
+
:param load_balancing_strategy:
|
|
559
|
+
Enable read from replicas in READONLY mode and defines the load balancing
|
|
560
|
+
strategy that will be used for cluster node selection.
|
|
561
|
+
The data read from replicas is eventually consistent
|
|
562
|
+
with the data in primary nodes.
|
|
563
|
+
:param dynamic_startup_nodes:
|
|
542
564
|
Set the RedisCluster's startup nodes to all of the discovered nodes.
|
|
543
565
|
If true (default value), the cluster's discovered nodes will be used to
|
|
544
566
|
determine the cluster nodes-slots mapping in the next topology refresh.
|
|
@@ -643,6 +665,7 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
643
665
|
self.command_flags = self.__class__.COMMAND_FLAGS.copy()
|
|
644
666
|
self.node_flags = self.__class__.NODE_FLAGS.copy()
|
|
645
667
|
self.read_from_replicas = read_from_replicas
|
|
668
|
+
self.load_balancing_strategy = load_balancing_strategy
|
|
646
669
|
self.reinitialize_counter = 0
|
|
647
670
|
self.reinitialize_steps = reinitialize_steps
|
|
648
671
|
if event_dispatcher is None:
|
|
@@ -695,7 +718,7 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
695
718
|
connection.set_parser(ClusterParser)
|
|
696
719
|
connection.on_connect()
|
|
697
720
|
|
|
698
|
-
if self.read_from_replicas:
|
|
721
|
+
if self.read_from_replicas or self.load_balancing_strategy:
|
|
699
722
|
# Sending READONLY command to server to configure connection as
|
|
700
723
|
# readonly. Since each cluster node may change its server type due
|
|
701
724
|
# to a failover, we should establish a READONLY connection
|
|
@@ -822,6 +845,7 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
822
845
|
cluster_response_callbacks=self.cluster_response_callbacks,
|
|
823
846
|
cluster_error_retry_attempts=self.cluster_error_retry_attempts,
|
|
824
847
|
read_from_replicas=self.read_from_replicas,
|
|
848
|
+
load_balancing_strategy=self.load_balancing_strategy,
|
|
825
849
|
reinitialize_steps=self.reinitialize_steps,
|
|
826
850
|
lock=self._lock,
|
|
827
851
|
)
|
|
@@ -939,7 +963,9 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
939
963
|
# get the node that holds the key's slot
|
|
940
964
|
slot = self.determine_slot(*args)
|
|
941
965
|
node = self.nodes_manager.get_node_from_slot(
|
|
942
|
-
slot,
|
|
966
|
+
slot,
|
|
967
|
+
self.read_from_replicas and command in READ_COMMANDS,
|
|
968
|
+
self.load_balancing_strategy if command in READ_COMMANDS else None,
|
|
943
969
|
)
|
|
944
970
|
return [node]
|
|
945
971
|
|
|
@@ -1163,12 +1189,18 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
1163
1189
|
# refresh the target node
|
|
1164
1190
|
slot = self.determine_slot(*args)
|
|
1165
1191
|
target_node = self.nodes_manager.get_node_from_slot(
|
|
1166
|
-
slot,
|
|
1192
|
+
slot,
|
|
1193
|
+
self.read_from_replicas and command in READ_COMMANDS,
|
|
1194
|
+
(
|
|
1195
|
+
self.load_balancing_strategy
|
|
1196
|
+
if command in READ_COMMANDS
|
|
1197
|
+
else None
|
|
1198
|
+
),
|
|
1167
1199
|
)
|
|
1168
1200
|
moved = False
|
|
1169
1201
|
|
|
1170
1202
|
redis_node = self.get_redis_connection(target_node)
|
|
1171
|
-
connection = get_connection(redis_node
|
|
1203
|
+
connection = get_connection(redis_node)
|
|
1172
1204
|
if asking:
|
|
1173
1205
|
connection.send_command("ASKING")
|
|
1174
1206
|
redis_node.parse_response(connection, "ASKING", **kwargs)
|
|
@@ -1225,13 +1257,19 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
|
|
|
1225
1257
|
except AskError as e:
|
|
1226
1258
|
redirect_addr = get_node_name(host=e.host, port=e.port)
|
|
1227
1259
|
asking = True
|
|
1228
|
-
except ClusterDownError
|
|
1260
|
+
except (ClusterDownError, SlotNotCoveredError):
|
|
1229
1261
|
# ClusterDownError can occur during a failover and to get
|
|
1230
1262
|
# self-healed, we will try to reinitialize the cluster layout
|
|
1231
1263
|
# and retry executing the command
|
|
1264
|
+
|
|
1265
|
+
# SlotNotCoveredError can occur when the cluster is not fully
|
|
1266
|
+
# initialized or can be temporary issue.
|
|
1267
|
+
# We will try to reinitialize the cluster topology
|
|
1268
|
+
# and retry executing the command
|
|
1269
|
+
|
|
1232
1270
|
time.sleep(0.25)
|
|
1233
1271
|
self.nodes_manager.initialize()
|
|
1234
|
-
raise
|
|
1272
|
+
raise
|
|
1235
1273
|
except ResponseError:
|
|
1236
1274
|
raise
|
|
1237
1275
|
except Exception as e:
|
|
@@ -1312,6 +1350,12 @@ class ClusterNode:
|
|
|
1312
1350
|
self.redis_connection.close()
|
|
1313
1351
|
|
|
1314
1352
|
|
|
1353
|
+
class LoadBalancingStrategy(Enum):
|
|
1354
|
+
ROUND_ROBIN = "round_robin"
|
|
1355
|
+
ROUND_ROBIN_REPLICAS = "round_robin_replicas"
|
|
1356
|
+
RANDOM_REPLICA = "random_replica"
|
|
1357
|
+
|
|
1358
|
+
|
|
1315
1359
|
class LoadBalancer:
|
|
1316
1360
|
"""
|
|
1317
1361
|
Round-Robin Load Balancing
|
|
@@ -1321,15 +1365,38 @@ class LoadBalancer:
|
|
|
1321
1365
|
self.primary_to_idx = {}
|
|
1322
1366
|
self.start_index = start_index
|
|
1323
1367
|
|
|
1324
|
-
def get_server_index(
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1368
|
+
def get_server_index(
|
|
1369
|
+
self,
|
|
1370
|
+
primary: str,
|
|
1371
|
+
list_size: int,
|
|
1372
|
+
load_balancing_strategy: LoadBalancingStrategy = LoadBalancingStrategy.ROUND_ROBIN, # noqa: line too long ignored
|
|
1373
|
+
) -> int:
|
|
1374
|
+
if load_balancing_strategy == LoadBalancingStrategy.RANDOM_REPLICA:
|
|
1375
|
+
return self._get_random_replica_index(list_size)
|
|
1376
|
+
else:
|
|
1377
|
+
return self._get_round_robin_index(
|
|
1378
|
+
primary,
|
|
1379
|
+
list_size,
|
|
1380
|
+
load_balancing_strategy == LoadBalancingStrategy.ROUND_ROBIN_REPLICAS,
|
|
1381
|
+
)
|
|
1329
1382
|
|
|
1330
1383
|
def reset(self) -> None:
|
|
1331
1384
|
self.primary_to_idx.clear()
|
|
1332
1385
|
|
|
1386
|
+
def _get_random_replica_index(self, list_size: int) -> int:
|
|
1387
|
+
return random.randint(1, list_size - 1)
|
|
1388
|
+
|
|
1389
|
+
def _get_round_robin_index(
|
|
1390
|
+
self, primary: str, list_size: int, replicas_only: bool
|
|
1391
|
+
) -> int:
|
|
1392
|
+
server_index = self.primary_to_idx.setdefault(primary, self.start_index)
|
|
1393
|
+
if replicas_only and server_index == 0:
|
|
1394
|
+
# skip the primary node index
|
|
1395
|
+
server_index = 1
|
|
1396
|
+
# Update the index for the next round
|
|
1397
|
+
self.primary_to_idx[primary] = (server_index + 1) % list_size
|
|
1398
|
+
return server_index
|
|
1399
|
+
|
|
1333
1400
|
|
|
1334
1401
|
class NodesManager:
|
|
1335
1402
|
def __init__(
|
|
@@ -1433,7 +1500,21 @@ class NodesManager:
|
|
|
1433
1500
|
# Reset moved_exception
|
|
1434
1501
|
self._moved_exception = None
|
|
1435
1502
|
|
|
1436
|
-
|
|
1503
|
+
@deprecated_args(
|
|
1504
|
+
args_to_warn=["server_type"],
|
|
1505
|
+
reason=(
|
|
1506
|
+
"In case you need select some load balancing strategy "
|
|
1507
|
+
"that will use replicas, please set it through 'load_balancing_strategy'"
|
|
1508
|
+
),
|
|
1509
|
+
version="5.3.0",
|
|
1510
|
+
)
|
|
1511
|
+
def get_node_from_slot(
|
|
1512
|
+
self,
|
|
1513
|
+
slot,
|
|
1514
|
+
read_from_replicas=False,
|
|
1515
|
+
load_balancing_strategy=None,
|
|
1516
|
+
server_type=None,
|
|
1517
|
+
):
|
|
1437
1518
|
"""
|
|
1438
1519
|
Gets a node that servers this hash slot
|
|
1439
1520
|
"""
|
|
@@ -1448,11 +1529,14 @@ class NodesManager:
|
|
|
1448
1529
|
f'"require_full_coverage={self._require_full_coverage}"'
|
|
1449
1530
|
)
|
|
1450
1531
|
|
|
1451
|
-
if read_from_replicas is True:
|
|
1452
|
-
|
|
1532
|
+
if read_from_replicas is True and load_balancing_strategy is None:
|
|
1533
|
+
load_balancing_strategy = LoadBalancingStrategy.ROUND_ROBIN
|
|
1534
|
+
|
|
1535
|
+
if len(self.slots_cache[slot]) > 1 and load_balancing_strategy:
|
|
1536
|
+
# get the server index using the strategy defined in load_balancing_strategy
|
|
1453
1537
|
primary_name = self.slots_cache[slot][0].name
|
|
1454
1538
|
node_idx = self.read_load_balancer.get_server_index(
|
|
1455
|
-
primary_name, len(self.slots_cache[slot])
|
|
1539
|
+
primary_name, len(self.slots_cache[slot]), load_balancing_strategy
|
|
1456
1540
|
)
|
|
1457
1541
|
elif (
|
|
1458
1542
|
server_type is None
|
|
@@ -1641,7 +1725,7 @@ class NodesManager:
|
|
|
1641
1725
|
if len(disagreements) > 5:
|
|
1642
1726
|
raise RedisClusterException(
|
|
1643
1727
|
f"startup_nodes could not agree on a valid "
|
|
1644
|
-
f
|
|
1728
|
+
f"slots cache: {', '.join(disagreements)}"
|
|
1645
1729
|
)
|
|
1646
1730
|
|
|
1647
1731
|
fully_covered = self.check_slots_coverage(tmp_slots)
|
|
@@ -1735,7 +1819,7 @@ class ClusterPubSub(PubSub):
|
|
|
1735
1819
|
first command execution. The node will be determined by:
|
|
1736
1820
|
1. Hashing the channel name in the request to find its keyslot
|
|
1737
1821
|
2. Selecting a node that handles the keyslot: If read_from_replicas is
|
|
1738
|
-
set to true, a replica can be selected.
|
|
1822
|
+
set to true or load_balancing_strategy is set, a replica can be selected.
|
|
1739
1823
|
|
|
1740
1824
|
:type redis_cluster: RedisCluster
|
|
1741
1825
|
:type node: ClusterNode
|
|
@@ -1831,7 +1915,9 @@ class ClusterPubSub(PubSub):
|
|
|
1831
1915
|
channel = args[1]
|
|
1832
1916
|
slot = self.cluster.keyslot(channel)
|
|
1833
1917
|
node = self.cluster.nodes_manager.get_node_from_slot(
|
|
1834
|
-
slot,
|
|
1918
|
+
slot,
|
|
1919
|
+
self.cluster.read_from_replicas,
|
|
1920
|
+
self.cluster.load_balancing_strategy,
|
|
1835
1921
|
)
|
|
1836
1922
|
else:
|
|
1837
1923
|
# Get a random node
|
|
@@ -1839,9 +1925,7 @@ class ClusterPubSub(PubSub):
|
|
|
1839
1925
|
self.node = node
|
|
1840
1926
|
redis_connection = self.cluster.get_redis_connection(node)
|
|
1841
1927
|
self.connection_pool = redis_connection.connection_pool
|
|
1842
|
-
self.connection = self.connection_pool.get_connection(
|
|
1843
|
-
"pubsub", self.shard_hint
|
|
1844
|
-
)
|
|
1928
|
+
self.connection = self.connection_pool.get_connection()
|
|
1845
1929
|
# register a callback that re-subscribes to any channels we
|
|
1846
1930
|
# were listening to when we were disconnected
|
|
1847
1931
|
self.connection.register_connect_callback(self.on_connect)
|
|
@@ -1976,6 +2060,7 @@ class ClusterPipeline(RedisCluster):
|
|
|
1976
2060
|
cluster_response_callbacks: Optional[Dict[str, Callable]] = None,
|
|
1977
2061
|
startup_nodes: Optional[List["ClusterNode"]] = None,
|
|
1978
2062
|
read_from_replicas: bool = False,
|
|
2063
|
+
load_balancing_strategy: Optional[LoadBalancingStrategy] = None,
|
|
1979
2064
|
cluster_error_retry_attempts: int = 3,
|
|
1980
2065
|
reinitialize_steps: int = 5,
|
|
1981
2066
|
lock=None,
|
|
@@ -1991,6 +2076,7 @@ class ClusterPipeline(RedisCluster):
|
|
|
1991
2076
|
)
|
|
1992
2077
|
self.startup_nodes = startup_nodes if startup_nodes else []
|
|
1993
2078
|
self.read_from_replicas = read_from_replicas
|
|
2079
|
+
self.load_balancing_strategy = load_balancing_strategy
|
|
1994
2080
|
self.command_flags = self.__class__.COMMAND_FLAGS.copy()
|
|
1995
2081
|
self.cluster_response_callbacks = cluster_response_callbacks
|
|
1996
2082
|
self.cluster_error_retry_attempts = cluster_error_retry_attempts
|
|
@@ -2062,8 +2148,7 @@ class ClusterPipeline(RedisCluster):
|
|
|
2062
2148
|
"""
|
|
2063
2149
|
cmd = " ".join(map(safe_str, command))
|
|
2064
2150
|
msg = (
|
|
2065
|
-
f"Command # {number} ({cmd}) of pipeline "
|
|
2066
|
-
f"caused error: {exception.args[0]}"
|
|
2151
|
+
f"Command # {number} ({cmd}) of pipeline caused error: {exception.args[0]}"
|
|
2067
2152
|
)
|
|
2068
2153
|
exception.args = (msg,) + exception.args[1:]
|
|
2069
2154
|
|
|
@@ -2201,8 +2286,8 @@ class ClusterPipeline(RedisCluster):
|
|
|
2201
2286
|
if node_name not in nodes:
|
|
2202
2287
|
redis_node = self.get_redis_connection(node)
|
|
2203
2288
|
try:
|
|
2204
|
-
connection = get_connection(redis_node
|
|
2205
|
-
except ConnectionError:
|
|
2289
|
+
connection = get_connection(redis_node)
|
|
2290
|
+
except (ConnectionError, TimeoutError):
|
|
2206
2291
|
for n in nodes.values():
|
|
2207
2292
|
n.connection_pool.release(n.connection)
|
|
2208
2293
|
# Connection retries are being handled in the node's
|