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
@@ -505,6 +506,11 @@ class initializer. In the case of conflicting arguments, querystring
505
506
"""
506
507
return cls (url = url , ** kwargs )
507
508
509
+ @deprecated_args (
510
+ args_to_warn = ["read_from_replicas" ],
511
+ reason = "Please configure the 'load_balancing_strategy' instead" ,
512
+ version = "5.0.3" ,
513
+ )
508
514
def __init__ (
509
515
self ,
510
516
host : Optional [str ] = None ,
@@ -515,6 +521,7 @@ def __init__(
515
521
require_full_coverage : bool = False ,
516
522
reinitialize_steps : int = 5 ,
517
523
read_from_replicas : bool = False ,
524
+ load_balancing_strategy : Optional ["LoadBalancingStrategy" ] = None ,
518
525
dynamic_startup_nodes : bool = True ,
519
526
url : Optional [str ] = None ,
520
527
address_remap : Optional [Callable [[Tuple [str , int ]], Tuple [str , int ]]] = None ,
@@ -543,11 +550,16 @@ def __init__(
543
550
cluster client. If not all slots are covered, RedisClusterException
544
551
will be thrown.
545
552
:param read_from_replicas:
553
+ @deprecated - please use load_balancing_strategy instead
546
554
Enable read from replicas in READONLY mode. You can read possibly
547
555
stale data.
548
556
When set to true, read commands will be assigned between the
549
557
primary and its replications in a Round-Robin manner.
550
- :param dynamic_startup_nodes:
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 with the data in primary nodes.
562
+ :param dynamic_startup_nodes:
551
563
Set the RedisCluster's startup nodes to all of the discovered nodes.
552
564
If true (default value), the cluster's discovered nodes will be used to
553
565
determine the cluster nodes-slots mapping in the next topology refresh.
@@ -652,6 +664,7 @@ def __init__(
652
664
self .command_flags = self .__class__ .COMMAND_FLAGS .copy ()
653
665
self .node_flags = self .__class__ .NODE_FLAGS .copy ()
654
666
self .read_from_replicas = read_from_replicas
667
+ self .load_balancing_strategy = load_balancing_strategy
655
668
self .reinitialize_counter = 0
656
669
self .reinitialize_steps = reinitialize_steps
657
670
if event_dispatcher is None :
@@ -704,7 +717,7 @@ def on_connect(self, connection):
704
717
connection .set_parser (ClusterParser )
705
718
connection .on_connect ()
706
719
707
- if self .read_from_replicas :
720
+ if self .read_from_replicas or self . load_balancing_strategy :
708
721
# Sending READONLY command to server to configure connection as
709
722
# readonly. Since each cluster node may change its server type due
710
723
# to a failover, we should establish a READONLY connection
@@ -831,6 +844,7 @@ def pipeline(self, transaction=None, shard_hint=None):
831
844
cluster_response_callbacks = self .cluster_response_callbacks ,
832
845
cluster_error_retry_attempts = self .cluster_error_retry_attempts ,
833
846
read_from_replicas = self .read_from_replicas ,
847
+ load_balancing_strategy = self .load_balancing_strategy ,
834
848
reinitialize_steps = self .reinitialize_steps ,
835
849
lock = self ._lock ,
836
850
)
@@ -948,7 +962,9 @@ def _determine_nodes(self, *args, **kwargs) -> List["ClusterNode"]:
948
962
# get the node that holds the key's slot
949
963
slot = self .determine_slot (* args )
950
964
node = self .nodes_manager .get_node_from_slot (
951
- slot , self .read_from_replicas and command in READ_COMMANDS
965
+ slot ,
966
+ self .read_from_replicas and command in READ_COMMANDS ,
967
+ self .load_balancing_strategy if command in READ_COMMANDS else None ,
952
968
)
953
969
return [node ]
954
970
@@ -1172,7 +1188,11 @@ def _execute_command(self, target_node, *args, **kwargs):
1172
1188
# refresh the target node
1173
1189
slot = self .determine_slot (* args )
1174
1190
target_node = self .nodes_manager .get_node_from_slot (
1175
- slot , self .read_from_replicas and command in READ_COMMANDS
1191
+ slot ,
1192
+ self .read_from_replicas and command in READ_COMMANDS ,
1193
+ self .load_balancing_strategy
1194
+ if command in READ_COMMANDS
1195
+ else None ,
1176
1196
)
1177
1197
moved = False
1178
1198
@@ -1327,6 +1347,12 @@ def __del__(self):
1327
1347
self .redis_connection .close ()
1328
1348
1329
1349
1350
+ class LoadBalancingStrategy (Enum ):
1351
+ ROUND_ROBIN = "round_robin"
1352
+ ROUND_ROBIN_REPLICAS = "round_robin_replicas"
1353
+ RANDOM_REPLICA = "random_replica"
1354
+
1355
+
1330
1356
class LoadBalancer :
1331
1357
"""
1332
1358
Round-Robin Load Balancing
@@ -1336,15 +1362,38 @@ def __init__(self, start_index: int = 0) -> None:
1336
1362
self .primary_to_idx = {}
1337
1363
self .start_index = start_index
1338
1364
1339
- def get_server_index (self , primary : str , list_size : int ) -> int :
1340
- server_index = self .primary_to_idx .setdefault (primary , self .start_index )
1341
- # Update the index
1342
- self .primary_to_idx [primary ] = (server_index + 1 ) % list_size
1343
- return server_index
1365
+ def get_server_index (
1366
+ self ,
1367
+ primary : str ,
1368
+ list_size : int ,
1369
+ load_balancing_strategy : LoadBalancingStrategy = LoadBalancingStrategy .ROUND_ROBIN ,
1370
+ ) -> int :
1371
+ if load_balancing_strategy == LoadBalancingStrategy .RANDOM_REPLICA :
1372
+ return self ._get_random_replica_index (list_size )
1373
+ else :
1374
+ return self ._get_round_robin_index (
1375
+ primary ,
1376
+ list_size ,
1377
+ load_balancing_strategy == LoadBalancingStrategy .ROUND_ROBIN_REPLICAS ,
1378
+ )
1344
1379
1345
1380
def reset (self ) -> None :
1346
1381
self .primary_to_idx .clear ()
1347
1382
1383
+ def _get_random_replica_index (self , list_size : int ) -> int :
1384
+ return random .randint (1 , list_size - 1 )
1385
+
1386
+ def _get_round_robin_index (
1387
+ self , primary : str , list_size : int , replicas_only : bool
1388
+ ) -> int :
1389
+ server_index = self .primary_to_idx .setdefault (primary , self .start_index )
1390
+ if replicas_only and server_index == 0 :
1391
+ # skip the primary node index
1392
+ server_index = 1
1393
+ # Update the index for the next round
1394
+ self .primary_to_idx [primary ] = (server_index + 1 ) % list_size
1395
+ return server_index
1396
+
1348
1397
1349
1398
class NodesManager :
1350
1399
def __init__ (
@@ -1448,7 +1497,21 @@ def _update_moved_slots(self):
1448
1497
# Reset moved_exception
1449
1498
self ._moved_exception = None
1450
1499
1451
- def get_node_from_slot (self , slot , read_from_replicas = False , server_type = None ):
1500
+ @deprecated_args (
1501
+ args_to_warn = ["server_type" ],
1502
+ reason = (
1503
+ "In case you need select some load balancing strategy "
1504
+ "that will use replicas, please set it through 'load_balancing_strategy'"
1505
+ ),
1506
+ version = "5.0.3" ,
1507
+ )
1508
+ def get_node_from_slot (
1509
+ self ,
1510
+ slot ,
1511
+ read_from_replicas = False ,
1512
+ load_balancing_strategy = None ,
1513
+ server_type = None ,
1514
+ ):
1452
1515
"""
1453
1516
Gets a node that servers this hash slot
1454
1517
"""
@@ -1463,11 +1526,14 @@ def get_node_from_slot(self, slot, read_from_replicas=False, server_type=None):
1463
1526
f'"require_full_coverage={ self ._require_full_coverage } "'
1464
1527
)
1465
1528
1466
- if read_from_replicas is True :
1467
- # get the server index in a Round-Robin manner
1529
+ if read_from_replicas is True and load_balancing_strategy is None :
1530
+ load_balancing_strategy = LoadBalancingStrategy .ROUND_ROBIN
1531
+
1532
+ if len (self .slots_cache [slot ]) > 1 and load_balancing_strategy :
1533
+ # get the server index using the strategy defined in load_balancing_strategy
1468
1534
primary_name = self .slots_cache [slot ][0 ].name
1469
1535
node_idx = self .read_load_balancer .get_server_index (
1470
- primary_name , len (self .slots_cache [slot ])
1536
+ primary_name , len (self .slots_cache [slot ]), load_balancing_strategy
1471
1537
)
1472
1538
elif (
1473
1539
server_type is None
@@ -1750,7 +1816,7 @@ def __init__(
1750
1816
first command execution. The node will be determined by:
1751
1817
1. Hashing the channel name in the request to find its keyslot
1752
1818
2. Selecting a node that handles the keyslot: If read_from_replicas is
1753
- set to true, a replica can be selected.
1819
+ set to true or load_balancing_strategy is set , a replica can be selected.
1754
1820
1755
1821
:type redis_cluster: RedisCluster
1756
1822
:type node: ClusterNode
@@ -1846,7 +1912,9 @@ def execute_command(self, *args):
1846
1912
channel = args [1 ]
1847
1913
slot = self .cluster .keyslot (channel )
1848
1914
node = self .cluster .nodes_manager .get_node_from_slot (
1849
- slot , self .cluster .read_from_replicas
1915
+ slot ,
1916
+ self .cluster .read_from_replicas ,
1917
+ self .cluster .load_balancing_strategy ,
1850
1918
)
1851
1919
else :
1852
1920
# Get a random node
@@ -1989,6 +2057,7 @@ def __init__(
1989
2057
cluster_response_callbacks : Optional [Dict [str , Callable ]] = None ,
1990
2058
startup_nodes : Optional [List ["ClusterNode" ]] = None ,
1991
2059
read_from_replicas : bool = False ,
2060
+ load_balancing_strategy : Optional [LoadBalancingStrategy ] = None ,
1992
2061
cluster_error_retry_attempts : int = 3 ,
1993
2062
reinitialize_steps : int = 5 ,
1994
2063
lock = None ,
@@ -2004,6 +2073,7 @@ def __init__(
2004
2073
)
2005
2074
self .startup_nodes = startup_nodes if startup_nodes else []
2006
2075
self .read_from_replicas = read_from_replicas
2076
+ self .load_balancing_strategy = load_balancing_strategy
2007
2077
self .command_flags = self .__class__ .COMMAND_FLAGS .copy ()
2008
2078
self .cluster_response_callbacks = cluster_response_callbacks
2009
2079
self .cluster_error_retry_attempts = cluster_error_retry_attempts
0 commit comments