Skip to content

Commit ba44296

Browse files
authored
Merge branch 'master' into aggregation_loadall
2 parents df49500 + d425227 commit ba44296

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+14416
-5586
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ _Please make sure to review and check all of these items:_
66
- [ ] Do the CI tests pass with this change (enable it first in your forked repo and wait for the github action build to finish)?
77
- [ ] Is the new or changed code fully tested?
88
- [ ] Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
9+
- [ ] Is there an example added to the examples folder (if applicable)?
910

1011
_NOTE: these things are not required to open a PR and can be done
1112
afterwards / while the PR is open._

.github/workflows/install_and_test.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@ cd ${TESTDIR}
3838

3939
# install, run tests
4040
pip install ${PKG}
41-
pytest
41+
# Redis tests
42+
pytest -m 'not onlycluster'
43+
# RedisCluster tests
44+
CLUSTER_URL="redis://localhost:16379/0"
45+
pytest -m 'not onlynoncluster and not redismod' --redis-url=${CLUSTER_URL}

.github/workflows/pypi-publish.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: install python
1414
uses: actions/setup-python@v2
1515
with:
16-
python-version: 3.0
16+
python-version: 3.9
1717
- name: Install dev tools
1818
run: |
1919
pip install -r dev_requirements.txt
@@ -22,7 +22,7 @@ jobs:
2222
- name: Build package
2323
run: |
2424
python setup.py build
25-
python setup.py dist bdist_wheel
25+
python setup.py sdist bdist_wheel
2626
2727
- name: Publish to Pypi
2828
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ env
1515
venv
1616
coverage.xml
1717
.venv
18+
*.xml
19+
.coverage*

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ configuration](https://redis.io/topics/sentinel).
6868

6969
## Testing
7070

71+
Call `invoke tests` to run all tests, or `invoke all-tests` to run linters
72+
tests as well. With the 'tests' and 'all-tests' targets, all Redis and
73+
RedisCluster tests will be run.
74+
75+
It is possible to run only Redis client tests (with cluster mode disabled) by
76+
using `invoke redis-tests`; similarly, RedisCluster tests can be run by using
77+
`invoke cluster-tests`.
78+
7179
Each run of tox starts and stops the various dockers required. Sometimes
7280
things get stuck, an `invoke clean` can help.
7381

README.md

Lines changed: 267 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The Python interface to the Redis key-value store.
99
[![codecov](https://codecov.io/gh/redis/redis-py/branch/master/graph/badge.svg?token=yenl5fzxxr)](https://codecov.io/gh/redis/redis-py)
1010
[![Total alerts](https://img.shields.io/lgtm/alerts/g/redis/redis-py.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/redis/redis-py/alerts/)
1111

12-
[Installation](##installation) | [Contributing](##contributing) | [Getting Started](##getting-started) | [Connecting To Redis](##connecting-to-redis)
12+
[Installation](#installation) | [Contributing](#contributing) | [Getting Started](#getting-started) | [Connecting To Redis](#connecting-to-redis)
1313

1414
---------------------------------------------
1515

@@ -948,8 +948,272 @@ C 3
948948

949949
### Cluster Mode
950950

951-
redis-py does not currently support [Cluster
952-
Mode](https://redis.io/topics/cluster-tutorial).
951+
redis-py is now supports cluster mode and provides a client for
952+
[Redis Cluster](<https://redis.io/topics/cluster-tutorial>).
953+
954+
The cluster client is based on Grokzen's
955+
[redis-py-cluster](https://github.com/Grokzen/redis-py-cluster), has added bug
956+
fixes, and now supersedes that library. Support for these changes is thanks to
957+
his contributions.
958+
959+
960+
**Create RedisCluster:**
961+
962+
Connecting redis-py to a Redis Cluster instance(s) requires at a minimum a
963+
single node for cluster discovery. There are multiple ways in which a cluster
964+
instance can be created:
965+
966+
- Using 'host' and 'port' arguments:
967+
968+
``` pycon
969+
>>> from redis.cluster import RedisCluster as Redis
970+
>>> rc = Redis(host='localhost', port=6379)
971+
>>> print(rc.get_nodes())
972+
[[host=127.0.0.1,port=6379,name=127.0.0.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>], [host=127.0.0.1,port=6378,name=127.0.0.1:6378,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6378,db=0>>>], [host=127.0.0.1,port=6377,name=127.0.0.1:6377,server_type=replica,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6377,db=0>>>]]
973+
```
974+
- Using the Redis URL specification:
975+
976+
``` pycon
977+
>>> from redis.cluster import RedisCluster as Redis
978+
>>> rc = Redis.from_url("redis://localhost:6379/0")
979+
```
980+
981+
- Directly, via the ClusterNode class:
982+
983+
``` pycon
984+
>>> from redis.cluster import RedisCluster as Redis
985+
>>> from redis.cluster import ClusterNode
986+
>>> nodes = [ClusterNode('localhost', 6379), ClusterNode('localhost', 6378)]
987+
>>> rc = Redis(startup_nodes=nodes)
988+
```
989+
990+
When a RedisCluster instance is being created it first attempts to establish a
991+
connection to one of the provided startup nodes. If none of the startup nodes
992+
are reachable, a 'RedisClusterException' will be thrown.
993+
After a connection to the one of the cluster's nodes is established, the
994+
RedisCluster instance will be initialized with 3 caches:
995+
a slots cache which maps each of the 16384 slots to the node/s handling them,
996+
a nodes cache that contains ClusterNode objects (name, host, port, redis connection)
997+
for all of the cluster's nodes, and a commands cache contains all the server
998+
supported commands that were retrieved using the Redis 'COMMAND' output.
999+
1000+
RedisCluster instance can be directly used to execute Redis commands. When a
1001+
command is being executed through the cluster instance, the target node(s) will
1002+
be internally determined. When using a key-based command, the target node will
1003+
be the node that holds the key's slot.
1004+
Cluster management commands and other commands that are not key-based have a
1005+
parameter called 'target_nodes' where you can specify which nodes to execute
1006+
the command on. In the absence of target_nodes, the command will be executed
1007+
on the default cluster node. As part of cluster instance initialization, the
1008+
cluster's default node is randomly selected from the cluster's primaries, and
1009+
will be updated upon reinitialization. Using r.get_default_node(), you can
1010+
get the cluster's default node, or you can change it using the
1011+
'set_default_node' method.
1012+
1013+
The 'target_nodes' parameter is explained in the following section,
1014+
'Specifying Target Nodes'.
1015+
1016+
``` pycon
1017+
>>> # target-nodes: the node that holds 'foo1's key slot
1018+
>>> rc.set('foo1', 'bar1')
1019+
>>> # target-nodes: the node that holds 'foo2's key slot
1020+
>>> rc.set('foo2', 'bar2')
1021+
>>> # target-nodes: the node that holds 'foo1's key slot
1022+
>>> print(rc.get('foo1'))
1023+
b'bar'
1024+
>>> # target-node: default-node
1025+
>>> print(rc.keys())
1026+
[b'foo1']
1027+
>>> # target-node: default-node
1028+
>>> rc.ping()
1029+
```
1030+
1031+
**Specifying Target Nodes:**
1032+
1033+
As mentioned above, all non key-based RedisCluster commands accept the kwarg
1034+
parameter 'target_nodes' that specifies the node/nodes that the command should
1035+
be executed on.
1036+
The best practice is to specify target nodes using RedisCluster class's node
1037+
flags: PRIMARIES, REPLICAS, ALL_NODES, RANDOM. When a nodes flag is passed
1038+
along with a command, it will be internally resolved to the relevant node/s.
1039+
If the nodes topology of the cluster changes during the execution of a command,
1040+
the client will be able to resolve the nodes flag again with the new topology
1041+
and attempt to retry executing the command.
1042+
1043+
``` pycon
1044+
>>> from redis.cluster import RedisCluster as Redis
1045+
>>> # run cluster-meet command on all of the cluster's nodes
1046+
>>> rc.cluster_meet('127.0.0.1', 6379, target_nodes=Redis.ALL_NODES)
1047+
>>> # ping all replicas
1048+
>>> rc.ping(target_nodes=Redis.REPLICAS)
1049+
>>> # ping a random node
1050+
>>> rc.ping(target_nodes=Redis.RANDOM)
1051+
>>> # get the keys from all cluster nodes
1052+
>>> rc.keys(target_nodes=Redis.ALL_NODES)
1053+
[b'foo1', b'foo2']
1054+
>>> # execute bgsave in all primaries
1055+
>>> rc.bgsave(Redis.PRIMARIES)
1056+
```
1057+
1058+
You could also pass ClusterNodes directly if you want to execute a command on a
1059+
specific node / node group that isn't addressed by the nodes flag. However, if
1060+
the command execution fails due to cluster topology changes, a retry attempt
1061+
will not be made, since the passed target node/s may no longer be valid, and
1062+
the relevant cluster or connection error will be returned.
1063+
1064+
``` pycon
1065+
>>> node = rc.get_node('localhost', 6379)
1066+
>>> # Get the keys only for that specific node
1067+
>>> rc.keys(target_nodes=node)
1068+
>>> # get Redis info from a subset of primaries
1069+
>>> subset_primaries = [node for node in rc.get_primaries() if node.port > 6378]
1070+
>>> rc.info(target_nodes=subset_primaries)
1071+
```
1072+
1073+
In addition, the RedisCluster instance can query the Redis instance of a
1074+
specific node and execute commands on that node directly. The Redis client,
1075+
however, does not handle cluster failures and retries.
1076+
1077+
``` pycon
1078+
>>> cluster_node = rc.get_node(host='localhost', port=6379)
1079+
>>> print(cluster_node)
1080+
[host=127.0.0.1,port=6379,name=127.0.0.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>]
1081+
>>> r = cluster_node.redis_connection
1082+
>>> r.client_list()
1083+
[{'id': '276', 'addr': '127.0.0.1:64108', 'fd': '16', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '26', 'qbuf-free': '32742', 'argv-mem': '10', 'obl': '0', 'oll': '0', 'omem': '0', 'tot-mem': '54298', 'events': 'r', 'cmd': 'client', 'user': 'default'}]
1084+
>>> # Get the keys only for that specific node
1085+
>>> r.keys()
1086+
[b'foo1']
1087+
```
1088+
1089+
**Multi-key commands:**
1090+
1091+
Redis supports multi-key commands in Cluster Mode, such as Set type unions or
1092+
intersections, mset and mget, as long as the keys all hash to the same slot.
1093+
By using RedisCluster client, you can use the known functions (e.g. mget, mset)
1094+
to perform an atomic multi-key operation. However, you must ensure all keys are
1095+
mapped to the same slot, otherwise a RedisClusterException will be thrown.
1096+
Redis Cluster implements a concept called hash tags that can be used in order
1097+
to force certain keys to be stored in the same hash slot, see
1098+
[Keys hash tag](https://redis.io/topics/cluster-spec#keys-hash-tags).
1099+
You can also use nonatomic for some of the multikey operations, and pass keys
1100+
that aren't mapped to the same slot. The client will then map the keys to the
1101+
relevant slots, sending the commands to the slots' node owners. Non-atomic
1102+
operations batch the keys according to their hash value, and then each batch is
1103+
sent separately to the slot's owner.
1104+
1105+
``` pycon
1106+
# Atomic operations can be used when all keys are mapped to the same slot
1107+
>>> rc.mset({'{foo}1': 'bar1', '{foo}2': 'bar2'})
1108+
>>> rc.mget('{foo}1', '{foo}2')
1109+
[b'bar1', b'bar2']
1110+
# Non-atomic multi-key operations splits the keys into different slots
1111+
>>> rc.mset_nonatomic({'foo': 'value1', 'bar': 'value2', 'zzz': 'value3')
1112+
>>> rc.mget_nonatomic('foo', 'bar', 'zzz')
1113+
[b'value1', b'value2', b'value3']
1114+
```
1115+
1116+
**Cluster PubSub:**
1117+
1118+
When a ClusterPubSub instance is created without specifying a node, a single
1119+
node will be transparently chosen for the pubsub connection on the
1120+
first command execution. The node will be determined by:
1121+
1. Hashing the channel name in the request to find its keyslot
1122+
2. Selecting a node that handles the keyslot: If read_from_replicas is
1123+
set to true, a replica can be selected.
1124+
1125+
*Known limitations with pubsub:*
1126+
1127+
Pattern subscribe and publish do not currently work properly due to key slots.
1128+
If we hash a pattern like fo* we will receive a keyslot for that string but
1129+
there are endless possibilities for channel names based on this pattern -
1130+
unknowable in advance. This feature is not disabled but the commands are not
1131+
currently recommended for use.
1132+
See [redis-py-cluster documentation](https://redis-py-cluster.readthedocs.io/en/stable/pubsub.html)
1133+
for more.
1134+
1135+
``` pycon
1136+
>>> p1 = rc.pubsub()
1137+
# p1 connection will be set to the node that holds 'foo' keyslot
1138+
>>> p1.subscribe('foo')
1139+
# p2 connection will be set to node 'localhost:6379'
1140+
>>> p2 = rc.pubsub(rc.get_node('localhost', 6379))
1141+
```
1142+
1143+
**Read Only Mode**
1144+
1145+
By default, Redis Cluster always returns MOVE redirection response on accessing
1146+
a replica node. You can overcome this limitation and scale read commands by
1147+
triggering READONLY mode.
1148+
1149+
To enable READONLY mode pass read_from_replicas=True to RedisCluster
1150+
constructor. When set to true, read commands will be assigned between the
1151+
primary and its replications in a Round-Robin manner.
1152+
1153+
READONLY mode can be set at runtime by calling the readonly() method with
1154+
target_nodes='replicas', and read-write access can be restored by calling the
1155+
readwrite() method.
1156+
1157+
``` pycon
1158+
>>> from cluster import RedisCluster as Redis
1159+
# Use 'debug' log level to print the node that the command is executed on
1160+
>>> rc_readonly = Redis(startup_nodes=startup_nodes,
1161+
read_from_replicas=True)
1162+
>>> rc_readonly.set('{foo}1', 'bar1')
1163+
>>> for i in range(0, 4):
1164+
# Assigns read command to the slot's hosts in a Round-Robin manner
1165+
>>> rc_readonly.get('{foo}1')
1166+
# set command would be directed only to the slot's primary node
1167+
>>> rc_readonly.set('{foo}2', 'bar2')
1168+
# reset READONLY flag
1169+
>>> rc_readonly.readwrite(target_nodes='replicas')
1170+
# now the get command would be directed only to the slot's primary node
1171+
>>> rc_readonly.get('{foo}1')
1172+
```
1173+
1174+
**Cluster Pipeline**
1175+
1176+
ClusterPipeline is a subclass of RedisCluster that provides support for Redis
1177+
pipelines in cluster mode.
1178+
When calling the execute() command, all the commands are grouped by the node
1179+
on which they will be executed, and are then executed by the respective nodes
1180+
in parallel. The pipeline instance will wait for all the nodes to respond
1181+
before returning the result to the caller. Command responses are returned as a
1182+
list sorted in the same order in which they were sent.
1183+
Pipelines can be used to dramatically increase the throughput of Redis Cluster
1184+
by significantly reducing the the number of network round trips between the
1185+
client and the server.
1186+
1187+
``` pycon
1188+
>>> with rc.pipeline() as pipe:
1189+
>>> pipe.set('foo', 'value1')
1190+
>>> pipe.set('bar', 'value2')
1191+
>>> pipe.get('foo')
1192+
>>> pipe.get('bar')
1193+
>>> print(pipe.execute())
1194+
[True, True, b'value1', b'value2']
1195+
>>> pipe.set('foo1', 'bar1').get('foo1').execute()
1196+
[True, b'bar1']
1197+
```
1198+
Please note:
1199+
- RedisCluster pipelines currently only support key-based commands.
1200+
- The pipeline gets its 'read_from_replicas' value from the cluster's parameter.
1201+
Thus, if read from replications is enabled in the cluster instance, the pipeline
1202+
will also direct read commands to replicas.
1203+
- The 'transcation' option is NOT supported in cluster-mode. In non-cluster mode,
1204+
the 'transaction' option is available when executing pipelines. This wraps the
1205+
pipeline commands with MULTI/EXEC commands, and effectively turns the pipeline
1206+
commands into a single transaction block. This means that all commands are
1207+
executed sequentially without any interruptions from other clients. However,
1208+
in cluster-mode this is not possible, because commands are partitioned
1209+
according to their respective destination nodes. This means that we can not
1210+
turn the pipeline commands into one transaction block, because in most cases
1211+
they are split up into several smaller pipelines.
1212+
1213+
1214+
See [Redis Cluster tutorial](https://redis.io/topics/cluster-tutorial) and
1215+
[Redis Cluster specifications](https://redis.io/topics/cluster-spec)
1216+
to learn more about Redis Cluster.
9531217

9541218
### Author
9551219

benchmarks/base.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import functools
22
import itertools
3-
import redis
43
import sys
54
import timeit
65

6+
import redis
7+
78

89
class Benchmark:
910
ARGUMENTS = ()
@@ -15,9 +16,7 @@ def get_client(self, **kwargs):
1516
# eventually make this more robust and take optional args from
1617
# argparse
1718
if self._client is None or kwargs:
18-
defaults = {
19-
'db': 9
20-
}
19+
defaults = {"db": 9}
2120
defaults.update(kwargs)
2221
pool = redis.ConnectionPool(**kwargs)
2322
self._client = redis.Redis(connection_pool=pool)
@@ -30,16 +29,16 @@ def run(self, **kwargs):
3029
pass
3130

3231
def run_benchmark(self):
33-
group_names = [group['name'] for group in self.ARGUMENTS]
34-
group_values = [group['values'] for group in self.ARGUMENTS]
32+
group_names = [group["name"] for group in self.ARGUMENTS]
33+
group_values = [group["values"] for group in self.ARGUMENTS]
3534
for value_set in itertools.product(*group_values):
3635
pairs = list(zip(group_names, value_set))
37-
arg_string = ', '.join(['%s=%s' % (p[0], p[1]) for p in pairs])
38-
sys.stdout.write('Benchmark: %s... ' % arg_string)
36+
arg_string = ", ".join(f"{p[0]}={p[1]}" for p in pairs)
37+
sys.stdout.write(f"Benchmark: {arg_string}... ")
3938
sys.stdout.flush()
4039
kwargs = dict(pairs)
4140
setup = functools.partial(self.setup, **kwargs)
4241
run = functools.partial(self.run, **kwargs)
4342
t = timeit.timeit(stmt=run, setup=setup, number=1000)
44-
sys.stdout.write('%f\n' % t)
43+
sys.stdout.write(f"{t:f}\n")
4544
sys.stdout.flush()

0 commit comments

Comments
 (0)