diff --git a/.travis.yml b/.travis.yml index f32f0329..3dbae178 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ install: - pip install -e . - "if [[ $HIREDIS == '1' ]]; then pip install hiredis; fi" env: - # Redis 3.0.6 + # Redis 3.0.7 - HIREDIS=0 REDIS_VERSION=3.0 - # Redis 3.0.6 and HIREDIS + # Redis 3.0.7 and HIREDIS - HIREDIS=1 REDIS_VERSION=3.0 # Redis 3.2.0-rc3 - HIREDIS=0 REDIS_VERSION=3.2 diff --git a/README.md b/README.md index abeebc60..fb7534fa 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ This client provides a client for redis cluster that was added in redis 3.0. This project is a port of `redis-rb-cluster` by antirez, with alot of added functionality. The original source can be found at https://github.com/antirez/redis-rb-cluster -[![Build Status](https://travis-ci.org/Grokzen/redis-py-cluster.svg?branch=master)](https://travis-ci.org/Grokzen/redis-py-cluster) [![Coverage Status](https://coveralls.io/repos/Grokzen/redis-py-cluster/badge.png)](https://coveralls.io/r/Grokzen/redis-py-cluster) [![PyPI version](https://badge.fury.io/py/redis-py-cluster.svg)](http://badge.fury.io/py/redis-py-cluster) [![Code Health](https://landscape.io/github/Grokzen/redis-py-cluster/unstable/landscape.svg)](https://landscape.io/github/Grokzen/redis-py-cluster/unstable) +Gitter chat room: [![Gitter](https://badges.gitter.im/Grokzen/redis-py-cluster.svg)](https://gitter.im/Grokzen/redis-py-cluster?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +[![Build Status](https://travis-ci.org/Grokzen/redis-py-cluster.svg?branch=master)](https://travis-ci.org/Grokzen/redis-py-cluster) [![Coverage Status](https://coveralls.io/repos/Grokzen/redis-py-cluster/badge.png)](https://coveralls.io/r/Grokzen/redis-py-cluster) [![PyPI version](https://badge.fury.io/py/redis-py-cluster.svg)](http://badge.fury.io/py/redis-py-cluster) @@ -14,7 +16,9 @@ All documentation can be found at http://redis-py-cluster.readthedocs.org/en/mas This Readme contains a reduced version of the full documentation. -Upgrading instructions between each released version can be found [docs/upgrading.rst](Here) +Upgrading instructions between each released version can be found [here](docs/upgrading.rst) + +Changelog for next release and all older releasess can be found [here](docs/release-notes.rst) diff --git a/dev-requirements.txt b/dev-requirements.txt index f4c7fc84..0d60c0e0 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,5 +7,5 @@ mock>=1.3.0,<2.0.0 docopt>=0.6.2,<1.0.0 tox>=2.2.0,<3.0.0 python-coveralls>=2.5.0,<3.0.0 -ptpdb==0.16 -ptpython==0.31 +ptpdb>=0.16,<1.0 +ptpython>=0.31,<1.0 diff --git a/docs/authors.rst b/docs/authors.rst index e29438d3..2d64a2c7 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -19,3 +19,4 @@ Authors who contributed code or testing: - etng - https://github.com/etng - gmolight - https://github.com/gmolight - baranbartu - https://github.com/baranbartu + - monklof - https://github.com/monklof diff --git a/docs/cluster-mgt.rst b/docs/cluster-mgt.rst deleted file mode 100644 index ec3b215c..00000000 --- a/docs/cluster-mgt.rst +++ /dev/null @@ -1,52 +0,0 @@ -Cluster Mgt class -================= - -The redis cluster can be managed through a cluster management class. It can for example be used to query the cluster for the current slots or the nodes setup. - -The following methods is implemented: - -- info -- slots -- nodes - -The following methods is not yet implemented: - -- addslots -- count_failure_reports -- countkeysinslot -- delslots -- failover -- forget -- getkeysinslot -- keyslot -- meet -- replicate -- reset -- saveconfig -- set_config_epoch -- setslot -- slaves - - - -Usage example -------------- - -.. code-block:: python - - >>> from rediscluster.cluster_mgt import RedisClusterMgt - >>> startup_nodes = [{"host": "127.0.0.1", "port": "7000"}] - >>> r = RedisClusterMgt(startup_nodes) - >>> r.slots() - { - 'slave': defaultdict(, { - '172.17.42.12:7003': [[0L, 5460L]], - '172.17.42.12:7005': [[10923L, 16383L]], - '172.17.42.12:7004': [[5461L, 10922L]] - }), - 'master': defaultdict(, { - '172.17.42.12:7002': [[10923L, 16383L]], - '172.17.42.12:7001': [[5461L, 10922L]], - '172.17.42.12:7000': [[0L, 5460L]] - }) - } diff --git a/docs/index.rst b/docs/index.rst index e34f3f53..bba309b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -83,6 +83,28 @@ Experimental: +Regarding duplicate pypi and python naming +------------------------------------------ + +It has been found that the python module name that is used in this library (rediscluster) is already shared with a similar but older project. + +This lib will not change the naming of the module to something else to prevent collisions between the libs. + +My reasoning for this is the following + + - Changing the namespace is a major task and probably should only be done in a complete rewrite of the lib, or if the lib had plans for a version 2.0.0 where this kind of backwards incompatibility could be introduced. + - This project is more up to date, the last merged PR in the other project was 3 years ago. + - This project is aimed for implement support for the cluster support in 3.0+, the other lib do not have that right now, but they implement almost the same cluster solution as the 3.0+ but in much more in the client side. + - The 2 libs is not compatible to be run at the same time even if the name would not collide. It is not recommended to run both in the same python interpreter. + +An issue has been raised in each repository to have tracking of the problem. + +redis-py-cluster: https://github.com/Grokzen/redis-py-cluster/issues/150 + +rediscluster: https://github.com/salimane/rediscluster-py/issues/11 + + + The Usage Guide --------------- @@ -92,7 +114,6 @@ The Usage Guide :maxdepth: 2 :glob: - cluster-mgt commands limitations-and-differences pipelines diff --git a/docs/release-notes.rst b/docs/release-notes.rst index b11555db..66fb9cc2 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -2,6 +2,26 @@ Release Notes ============= +1.3.0 (Sep 11, 2016) +-------------------- + + * Removed RedisClusterMgt class and file + * Fixed a bug when using pipelines with RedisCluster class (Ozahata) + * Bump redis-server during travis tests to 3.0.7 + * Added docs about same module name in another python redis cluster project. + * Fix a bug when a connection was to be tracked for a node but the node either do not yet exists or + was removed because of resharding was done in another thread. (ashishbaghudana) + * Fixed a bug with "CLUSTER ..." commands when a node_id argument was needed and the return type + was supposed to be converted to bool with bool_ok in redis._compat. + * Add back gitter chat room link + * Add new client commands + - cluster_reset_all_nodes + * Command cluster_delslots now determines what cluster shard each slot is on and sends each slot deletion + command to the correct node. Command have changed argument spec (Read Upgrading.rst for details) + * Fixed a bug when hashing the key it if was a python 3 byte string and it would cause it to route to wrong slot in the cluster (fossilet, Grokzen) + * Fixed a bug when reinitialize the nodemanager it would use the old nodes_cache instead of the new one that was just parsed (monklof) + + 1.2.0 (Apr 09, 2016) -------------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 3c4769bb..aa3a7a35 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -7,7 +7,11 @@ This document describes what must be done when upgrading between different versi 1.2.0 --> Next release ---------------------- -Class RedisClusterMgt will be removed. +Class RedisClusterMgt has been removed. You should use the `CLUSTER ...` methods that exists in the `StrictRedisCluster` client class. + +Method `cluster_delslots` changed argument specification from `self, node_id, *slots` to `self, *slots` and changed the behaviour of the method to now automatically determine the slot_id based on the current cluster structure and where each slot that you want to delete is loated. + +Method pfcount no longer has custom logic and exceptions to prevent CROSSSLOT errors. If method is used with different slots then a regular CROSSSLOT error (rediscluster.exceptions.ClusterCrossSlotError) will be returned. diff --git a/rediscluster/__init__.py b/rediscluster/__init__.py index 220fbd51..9f2299b0 100644 --- a/rediscluster/__init__.py +++ b/rediscluster/__init__.py @@ -5,7 +5,6 @@ # Import shortcut from .client import StrictRedisCluster, RedisCluster -from .cluster_mgt import RedisClusterMgt # NOQA from .pipeline import StrictClusterPipeline from .pubsub import ClusterPubSub @@ -17,7 +16,7 @@ setattr(redis, "StrictClusterPipeline", StrictClusterPipeline) # Major, Minor, Fix version -__version__ = (1, 2, 0) +__version__ = (1, 3, 0) if sys.version_info[0:3] == (3, 4, 0): raise RuntimeError("CRITICAL: rediscluster do not work with python 3.4.0. Please use 3.4.1 or higher.") diff --git a/rediscluster/client.py b/rediscluster/client.py index 9a41034a..ee524243 100644 --- a/rediscluster/client.py +++ b/rediscluster/client.py @@ -14,6 +14,7 @@ ) from .pubsub import ClusterPubSub from .utils import ( + bool_ok, string_keys_to_dict, dict_merge, blocked_command, @@ -25,7 +26,7 @@ ) # 3rd party imports from redis import StrictRedis -from redis.client import list_or_args, bool_ok, parse_info +from redis.client import list_or_args, parse_info from redis.connection import Token from redis._compat import iteritems, basestring, b, izip, nativestr from redis.exceptions import RedisError, ResponseError, TimeoutError, DataError, ConnectionError, BusyLoadingError @@ -370,80 +371,160 @@ def _execute_command_on_nodes(self, nodes, *args, **kwargs): ########## # Cluster management commands - # Send to specefied node + def _nodes_slots_to_slots_nodes(self, mapping): + """ + Converts a mapping of + {id: , slots: (slot1, slot2)} + to + {slot1: , slot2: } + + Operation is expensive so use with caution + """ + out = {} + for node in mapping: + for slot in node['slots']: + out[str(slot)] = node['id'] + return out + def cluster_addslots(self, node_id, *slots): - """Assign new hash slots to receiving node""" + """ + Assign new hash slots to receiving node + + Sends to specefied node + """ return self.execute_command('CLUSTER ADDSLOTS', *slots, node_id=node_id) - # Send to node based on slot_id def cluster_countkeysinslot(self, slot_id): - """Return the number of local keys in the specified hash slot""" + """ + Return the number of local keys in the specified hash slot + + Send to node based on specefied slot_id + """ return self.execute_command('CLUSTER COUNTKEYSINSLOT', slot_id) - # Send to specefied node def cluster_count_failure_report(self, node_id): - """Return the number of failure reports active for a given node""" + """ + Return the number of failure reports active for a given node + + Sends to specefied node + """ return self.execute_command('CLUSTER COUNT-FAILURE-REPORTS', node_id=node_id) - # Send to specefied node - def cluster_delslots(self, node_id, *slots): - """Set hash slots as unbound in receiving node""" - return self.execute_command('CLUSTER DELSLOTS', *slots, node_id=node_id) + def cluster_delslots(self, *slots): + """ + Set hash slots as unbound in the cluster. + It determines by it self what node the slot is in and sends it there + + Returns a list of the results for each processed slot. + """ + cluster_nodes = self._nodes_slots_to_slots_nodes(self.cluster_nodes()) + + return [ + self.execute_command('CLUSTER DELSLOTS', slot, node_id=cluster_nodes[slot]) + for slot in slots + ] - # Send to specefied node def cluster_failover(self, node_id, option): - """Forces a slave to perform a manual failover of its master.""" + """ + Forces a slave to perform a manual failover of its master + + Sends to specefied node + """ assert option.upper() in ('FORCE', 'TAKEOVER') # TODO: change this option handling return self.execute_command('CLUSTER FAILOVER', Token(option)) - # Send to random node def cluster_info(self): - """Provides info about Redis Cluster node state""" + """ + Provides info about Redis Cluster node state + + Sends to random node in the cluster + """ return self.execute_command('CLUSTER INFO') - # Send to random node def cluster_keyslot(self, name): - """Returns the hash slot of the specified key""" + """ + Returns the hash slot of the specified key + + Sends to random node in the cluster + """ return self.execute_command('CLUSTER KEYSLOT', name) - # Send to specefied node def cluster_meet(self, node_id, host, port): - """Force a node cluster to handshake with another node""" + """ + Force a node cluster to handshake with another node. + + Sends to specefied node + """ return self.execute_command('CLUSTER MEET', host, port, node_id=node_id) - # Send to random node def cluster_nodes(self): - """Force a node cluster to handshake with another node""" + """ + Force a node cluster to handshake with another node + + Sends to random node in the cluster + """ return self.execute_command('CLUSTER NODES') - # Send to specefied node def cluster_replicate(self, target_node_id): - """Reconfigure a node as a slave of the specified master node""" + """ + Reconfigure a node as a slave of the specified master node + + Sends to specefied node + """ return self.execute_command('CLUSTER REPLICATE', target_node_id) - # Send to specific node def cluster_reset(self, node_id, soft=True): """ - Reset a Redis Cluster node. + Reset a Redis Cluster node + If 'soft' is True then it will send 'SOFT' argument If 'soft' is False then it will send 'HARD' argument + + Sends to specefied node """ return self.execute_command('CLUSTER RESET', Token('SOFT' if soft else 'HARD'), node_id=node_id) - # Send to all nodes + def cluster_reset_all_nodes(self, soft=True): + """ + Send CLUSTER RESET to all nodes in the cluster + + If 'soft' is True then it will send 'SOFT' argument + If 'soft' is False then it will send 'HARD' argument + + Sends to all nodes in the cluster + """ + return [ + self.execute_command( + 'CLUSTER RESET', + Token('SOFT' if soft else 'HARD'), + node_id=node['id'], + ) + for node in self.cluster_nodes() + ] + def cluster_save_config(self): - """Forces the node to save cluster state on disk""" + """ + Forces the node to save cluster state on disk + + Sends to all nodes in the cluster + """ return self.execute_command('CLUSTER SAVECONFIG') - # Send to specefied node_id def cluster_set_config_epoch(self, node_id, epoch): - """Set the configuration epoch in a new node""" + """ + Set the configuration epoch in a new node + + Sends to specefied node + """ return self.execute_command('CLUSTER SET-CONFIG-EPOCH', epoch, node_id=node_id) - # Send to specefied node_id # TODO: Determine what the purpose of bind_to_node_ip is going to be def cluster_setslot(self, node_id, slot_id, state, bind_to_node_id=None): - """Bind an hash slot to a specific node""" + """ + Bind an hash slot to a specific node + + Sends to specefied node + """ if state.upper() in ('IMPORTING', 'MIGRATING', 'NODE') and node_id is not None: return self.execute_command('CLUSTER SETSLOT', slot_id, Token(state), node_id) elif state.upper() == 'STABLE': @@ -451,14 +532,20 @@ def cluster_setslot(self, node_id, slot_id, state, bind_to_node_id=None): else: raise RedisError('Invalid slot state: {0}'.format(state)) - # Specefied node def cluster_slaves(self, target_node_id): - """Force a node cluster to handshake with another node""" + """ + Force a node cluster to handshake with another node + + Sends to targeted cluster node + """ return self.execute_command('CLUSTER SLAVES', target_node_id) - # Random node def cluster_slots(self): - """Get array of Cluster slot to node mappings""" + """ + Get array of Cluster slot to node mappings + + Sends to random node in the cluster + """ return self.execute_command('CLUSTER SLOTS') ########## @@ -900,22 +987,11 @@ def sunionstore(self, dest, keys, *args): return self.sadd(dest, *res) - def ensure_same_slot(self, keys): - """ - Returns True if all slots for the list of keys point - to the same hash slot. - """ - slots = [self.connection_pool.nodes.keyslot(key) for key in keys] - return len(slots) == 1 - def pfcount(self, *sources): """ pfcount only works when all sources point to the same hash slot. """ - if self.ensure_same_slot(sources): - return super(self.__class__, self).pfcount(*sources) - else: - raise RedisClusterException("pfcount can't be used when sources point to different hashslots") + return super(self.__class__, self).pfcount(*sources) def pfmerge(self, dest, *sources): """ @@ -1020,7 +1096,6 @@ def pipeline(self, transaction=True, shard_hint=None): connection_pool=self.connection_pool, startup_nodes=self.connection_pool.nodes.startup_nodes, refresh_table_asap=self.refresh_table_asap, - nodes_callbacks=self.nodes_callbacks, response_callbacks=self.response_callbacks ) diff --git a/rediscluster/cluster_mgt.py b/rediscluster/cluster_mgt.py deleted file mode 100644 index 7ff85282..00000000 --- a/rediscluster/cluster_mgt.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -import warnings -from collections import defaultdict - -from .connection import ClusterConnectionPool -from .exceptions import RedisClusterException -from .utils import clusterdown_wrapper, first_key, nslookup - -from redis.exceptions import ConnectionError, TimeoutError - - -class RedisClusterMgt(object): - """ - """ - blocked_args = ( - 'addslots', 'count_failure_reports', - 'countkeysinslot', 'delslots', 'failover', 'forget', - 'getkeysinslot', 'keyslot', 'meet', 'replicate', 'reset', - 'saveconfig', 'set_config_epoch', 'setslot', 'slaves', - ) - - def __init__(self, startup_nodes=None, **kwargs): - """ - """ - warnings.warn('RedisClusterMgt class will be removed in release 1.3.0. All CLUSTER commands is now implemented as commands in StrictRedisCluster class', FutureWarning) - - self.connection_pool = ClusterConnectionPool( - startup_nodes=startup_nodes, - init_slot_cache=True, **kwargs - ) - - def __getattr__(self, attr): - """ - """ - if attr in self.blocked_args: - raise RedisClusterException('{0} is currently not supported'.format(attr)) - - raise RedisClusterException('{0} is not a valid Redis cluster argument'.format(attr)) - - @clusterdown_wrapper - def _execute_command_on_nodes(self, nodes, *args, **kwargs): - """ - """ - command = args[0] - res = {} - - for node in nodes: - c = self.connection_pool.get_connection_by_node(node) - - try: - c.send_command(*args) - res[node["name"]] = c.read_response() - except (ConnectionError, TimeoutError) as e: - c.disconnect() - - if not c.retry_on_timeout and isinstance(e, TimeoutError): - raise - - c.send_command(*args) - res[node["name"]] = c.read_response() - finally: - self.connection_pool.release(c) - - return first_key(command, res) - - def _execute_cluster_commands(self, *args, **kwargs): - """ - """ - args = ('cluster',) + args - node = self.connection_pool.nodes.random_node() - - return self._execute_command_on_nodes([node], *args, **kwargs) - - def info(self): - """ - """ - raw = self._execute_cluster_commands('info') - - def _split(line): - k, v = line.split(':') - yield k - yield v - - return {k: v for k, v in - [_split(line) for line in raw.split('\r\n') if line]} - - def _make_host(self, host, port): - """ - """ - return '{0}:{1}'.format(host, port) - - def slots(self, host_required=False): - """ - """ - slots_info = self._execute_cluster_commands('slots') - master_slots = defaultdict(list) - slave_slots = defaultdict(list) - - for item in slots_info: - master_ip, master_port = item[2] - slots = [item[0], item[1]] - master_host = nslookup(master_ip) if host_required else master_ip - master_slots[self._make_host(master_host, master_port)].append(slots) - slaves = item[3:] - - for slave_ip, slave_port in slaves: - slave_host = nslookup(slave_ip) if host_required else slave_ip - slave_slots[self._make_host(slave_host, slave_port)].append(slots) - - return { - 'master': master_slots, - 'slave': slave_slots - } - - def _parse_node_line(self, line): - """ - """ - line_items = line.split(' ') - ret = line_items[:8] - slots = [sl.split('-') for sl in line_items[8:]] - ret.append(slots) - - return ret - - def nodes(self, host_required=False): - """ - """ - raw = self._execute_cluster_commands('nodes') - ret = {} - - for line in raw.split('\n'): - if not line: - continue - - node_id, ip_port, flags, master_id, ping, pong, epoch, \ - status, slots = self._parse_node_line(line) - role = flags - - if ',' in flags: - if "slave" in flags: - role = "slave" - elif "master" in flags: - role = "master" - - host = nslookup(ip_port) if host_required else ip_port - ret[host] = { - 'node_id': node_id, - 'role': role, - 'master_id': master_id, - 'last_ping_sent': ping, - 'last_pong_rcvd': pong, - 'epoch': epoch, - 'status': status, - 'slots': slots - } - - return ret diff --git a/rediscluster/connection.py b/rediscluster/connection.py index 5a7dbe3f..a645de4c 100644 --- a/rediscluster/connection.py +++ b/rediscluster/connection.py @@ -146,6 +146,10 @@ def get_connection(self, command_name, *keys, **options): connection = self._available_connections.get(node["name"], []).pop() except IndexError: connection = self.make_connection(node) + + if node['name'] not in self._in_use_connections: + self._in_use_connections[node['name']] = set() + self._in_use_connections[node['name']].add(connection) return connection diff --git a/rediscluster/nodemanager.py b/rediscluster/nodemanager.py index 61b12ced..762ead03 100644 --- a/rediscluster/nodemanager.py +++ b/rediscluster/nodemanager.py @@ -2,6 +2,7 @@ # python std lib import random +import sys # rediscluster imports from .crc import crc16 @@ -30,13 +31,41 @@ def __init__(self, startup_nodes=None, **connection_kwargs): if not self.startup_nodes: raise RedisClusterException("No startup nodes provided") - def keyslot(self, key): + # Minor performance tweak to avoid having to check inside the method + # for each call to keyslot method. + if sys.version_info[0] < 3: + self.keyslot = self.keyslot_py_2 + else: + self.keyslot = self.keyslot_py_3 + + def keyslot_py_2(self, key): """ Calculate keyslot for a given key. - - This also works for binary keys that is used in python 3. + Tuned for compatibility with python 2.7.x """ k = unicode(key) + + start = k.find("{") + + if start > -1: + end = k.find("}", start + 1) + if end > -1 and end != start + 1: + k = k[start + 1:end] + + return crc16(k) % self.RedisClusterHashSlots + + def keyslot_py_3(self, key): + """ + Calculate keyslot for a given key. + Tuned for compatibility with supported python 3.x versions + """ + try: + # Handle bytes case + k = str(key, encoding='utf-8') + except TypeError: + # Convert others to str. + k = str(key) + start = k.find("{") if start > -1: @@ -215,7 +244,7 @@ def cluster_require_full_coverage(self, nodes_cache): then even all slots are not covered, cluster still will be able to respond """ - nodes = self.nodes or nodes_cache + nodes = nodes_cache or self.nodes def node_require_full_coverage(node): r_node = self.get_redis_link(host=node["host"], port=node["port"], decode_responses=True) diff --git a/rediscluster/utils.py b/rediscluster/utils.py index 7c2c7b06..b275d6db 100644 --- a/rediscluster/utils.py +++ b/rediscluster/utils.py @@ -8,7 +8,15 @@ ) # 3rd party imports -from redis._compat import basestring +from redis._compat import basestring, nativestr + + +def bool_ok(response, *args, **kwargs): + """ + Borrowed from redis._compat becuase that method to not support extra arguments + when used in a cluster environment. + """ + return nativestr(response) == 'OK' def string_keys_to_dict(key_strings, callback): @@ -125,8 +133,8 @@ def fix_server(host, port): start, end, master = slot[:3] slaves = slot[3:] slots[start, end] = { - 'master': fix_server(master), - 'slaves': [fix_server(slave) for slave in slaves], + 'master': fix_server(*master), + 'slaves': [fix_server(*slave) for slave in slaves], } return slots diff --git a/setup.py b/setup.py index 4e8a2b3d..9bd8e769 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name="redis-py-cluster", - version="1.2.0", + version="1.3.0", description="Cluster library for redis 3.0.0 built on top of redis-py lib", long_description=readme + '\n\n' + history, author="Johan Andersson", diff --git a/tests/conftest.py b/tests/conftest.py index 55c6576c..87cbc01c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import json # rediscluster imports -from rediscluster import StrictRedisCluster, RedisCluster, RedisClusterMgt +from rediscluster import StrictRedisCluster, RedisCluster # 3rd party imports import pytest @@ -148,11 +148,3 @@ def sr(request, *args, **kwargs): Returns a instance of StrictRedisCluster """ return _init_client(request, reinitialize_steps=1, cls=StrictRedisCluster, **kwargs) - - -@pytest.fixture() -def rcm(request, *args, **kwargs): - """ - Returns a instance of RedisClusterMgt - """ - return _init_mgt_client(request, cls=RedisClusterMgt, decode_responses=True, **kwargs) diff --git a/tests/test_cluster_connection_pool.py b/tests/test_cluster_connection_pool.py index 887a5762..bb69bb01 100644 --- a/tests/test_cluster_connection_pool.py +++ b/tests/test_cluster_connection_pool.py @@ -46,6 +46,15 @@ def get_pool(self, connection_kwargs=None, max_connections=None, max_connections **connection_kwargs) return pool + def test_in_use_not_exists(self): + """ + Test that if for some reason, the node that it tries to get the connectino for + do not exists in the _in_use_connection variable. + """ + pool = self.get_pool() + pool._in_use_connections = {} + pool.get_connection("pubsub", channel="foobar") + def test_connection_creation(self): connection_kwargs = {'foo': 'bar', 'biz': 'baz'} pool = self.get_pool(connection_kwargs=connection_kwargs) diff --git a/tests/test_node_manager.py b/tests/test_node_manager.py index 4a1291e4..6a9677a6 100644 --- a/tests/test_node_manager.py +++ b/tests/test_node_manager.py @@ -39,6 +39,9 @@ def test_keyslot(): assert n.keyslot("{foo}bar") == 12182 assert n.keyslot("{foo}") == 12182 assert n.keyslot(1337) == 4314 + assert n.keyslot(b"abc") == n.keyslot("abc") + assert n.keyslot("abc") == n.keyslot(unicode("abc")) + assert n.keyslot(unicode("abc")) == n.keyslot(b"abc") def test_init_slots_cache_not_all_slots(s): diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index fbea1577..f0129630 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -8,6 +8,7 @@ from rediscluster.client import StrictRedisCluster from rediscluster.connection import ClusterConnectionPool, ClusterReadOnlyConnectionPool from rediscluster.exceptions import RedisClusterException +from tests.conftest import _get_client # 3rd party imports import pytest @@ -298,6 +299,14 @@ def test_blocked_arguments(self, r): assert unicode(ex.value).startswith("shard_hint is deprecated in cluster mode"), True + def test_redis_cluster_pipeline(self): + """ + Test that we can use a pipeline with the RedisCluster class + """ + r = _get_client(cls=None) + with r.pipeline(transaction=False) as pipe: + pipe.get("foobar") + def test_mget_disabled(self, r): with r.pipeline(transaction=False) as pipe: with pytest.raises(RedisClusterException): diff --git a/tests/test_redis_cluster_mgt.py b/tests/test_redis_cluster_mgt.py deleted file mode 100644 index cea7ba4a..00000000 --- a/tests/test_redis_cluster_mgt.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- - - -class TestRedisClusterMgt(object): - - def test_info(self, rcm): - info = rcm.info() - assert 'cluster_state' in info - - def test_slots(self, rcm): - slots = rcm.slots() - assert 'master' in slots - assert 'slave' in slots - - master_slots = slots['master'] - for host, slots in master_slots.items(): - s = slots[0] - # node can have multiple slots - # as a result, the format is [[1, 2], [3, 4]] - assert isinstance(s, list) - assert len(s) == 2 - - def test_nodes(self, rcm): - nodes = rcm.nodes() - for host, info in nodes.items(): - assert 'node_id' in info - assert 'role' in info - assert 'master_id' in info - assert 'last_ping_sent' in info - assert 'last_pong_rcvd' in info - assert 'epoch' in info - assert 'status' in info - assert 'slots' in info