class ClusterSentinelCommands(SentinelCommandMixin): NODES_FLAGS = dict_merge( list_keys_to_dict([ 'SENTINEL GET-MASTER-ADDR-BY-NAME', 'SENTINEL MASTER', 'SENTINEL MASTERS', 'SENTINEL MONITOR', 'SENTINEL REMOVE', 'SENTINEL SENTINELS', 'SENTINEL SET', 'SENTINEL SLAVES' ], NodeFlag.BLOCKED))
def test_list_keys_to_dict(): def mock_true(): return True assert list_keys_to_dict(["FOO", "BAR"], mock_true) == { "FOO": mock_true, "BAR": mock_true }
class ClusterServerCommandMixin(ServerCommandMixin): NODES_FLAGS = dict_merge( list_keys_to_dict(['SHUTDOWN', 'SLAVEOF', 'CLIENT SETNAME'], NodeFlag.BLOCKED), list_keys_to_dict(['FLUSHALL', 'FLUSHDB'], NodeFlag.ALL_MASTERS), list_keys_to_dict([ 'SLOWLOG LEN', 'SLOWLOG RESET', 'SLOWLOG GET', 'TIME', 'SAVE', 'LASTSAVE', 'DBSIZE', 'CONFIG RESETSTAT', 'CONFIG REWRITE', 'CONFIG GET', 'CONFIG SET', 'CLIENT KILL', 'CLIENT LIST', 'CLIENT GETNAME', 'INFO', 'BGSAVE', 'BGREWRITEAOF' ], NodeFlag.ALL_NODES)) RESULT_CALLBACKS = dict_merge( list_keys_to_dict([ 'CONFIG GET', 'CONFIG SET', 'SLOWLOG GET', 'CLIENT KILL', 'INFO', 'BGREWRITEAOF', 'BGSAVE', 'CLIENT LIST', 'CLIENT GETNAME', 'CONFIG RESETSTAT', 'CONFIG REWRITE', 'DBSIZE', 'LASTSAVE', 'SAVE', 'SLOWLOG LEN', 'SLOWLOG RESET', 'TIME', 'FLUSHALL', 'FLUSHDB' ], lambda res: res))
class ClusterScriptingCommandMixin(ScriptingCommandMixin): NODES_FLAGS = dict_merge({'SCRIPT KILL': NodeFlag.BLOCKED}, list_keys_to_dict([ "SCRIPT LOAD", "SCRIPT FLUSH", "SCRIPT EXISTS", ], NodeFlag.ALL_MASTERS)) RESULT_CALLBACKS = { "SCRIPT LOAD": lambda res: list(res.values()).pop(), "SCRIPT EXISTS": lambda res: [all(k) for k in zip(*res.values())], "SCRIPT FLUSH": lambda res: all(res.values()) }
class CLusterPubSubCommandMixin(PubSubCommandMixin): NODES_FLAGS = dict_merge( list_keys_to_dict( ['PUBSUB CHANNELS', 'PUBSUB NUMSUB', 'PUBSUB NUMPAT'], NodeFlag.ALL_NODES ) ) RESULT_CALLBACKS = dict_merge( list_keys_to_dict([ "PUBSUB CHANNELS", ], parse_cluster_pubsub_channels), list_keys_to_dict([ "PUBSUB NUMSUB", ], parse_cluster_pubsub_numsub), list_keys_to_dict([ "PUBSUB NUMPAT", ], parse_cluster_pubsub_numpat), ) def pubsub(self, **kwargs): return ClusterPubSub(self.connection_pool, **kwargs)
class ClusterKeysCommandMixin(KeysCommandMixin): NODES_FLAGS = dict_merge( { 'MOVE': NodeFlag.BLOCKED, 'RANDOMKEY': NodeFlag.RANDOM, 'SCAN': NodeFlag.ALL_MASTERS, }, list_keys_to_dict( ['KEYS'], NodeFlag.ALL_NODES ) ) RESULT_CALLBACKS = { 'KEYS': merge_result, 'RANDOMKEY': first_key, 'SCAN': lambda res: res } async def rename(self, src, dst): """ Rename key ``src`` to ``dst`` Cluster impl: This operation is no longer atomic because each key must be querried then set in separate calls because they maybe will change cluster node """ if src == dst: raise ResponseError("source and destination objects are the same") data = await self.dump(src) if data is None: raise ResponseError("no such key") ttl = await self.pttl(src) if ttl is None or ttl < 1: ttl = 0 await self.delete(dst) await self.restore(dst, ttl, data) await self.delete(src) return True async def delete(self, *names): """ "Delete one or more keys specified by ``names``" Cluster impl: Iterate all keys and send DELETE for each key. This will go a lot slower than a normal delete call in StrictRedis. Operation is no longer atomic. """ count = 0 for arg in names: count += await self.execute_command('DEL', arg) return count async def renamenx(self, src, dst): """ Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist Cluster impl: Check if dst key do not exists, then calls rename(). Operation is no longer atomic. """ if not await self.exists(dst): return await self.rename(src, dst) return False
class ClusterCommandMixin: NODES_FLAGS = dict_merge( { 'CLUSTER INFO': NodeFlag.ALL_NODES, 'CLUSTER COUNTKEYSINSLOT': NodeFlag.SLOT_ID }, list_keys_to_dict(['CLUSTER NODES', 'CLUSTER SLOTS'], NodeFlag.RANDOM)) RESPONSE_CALLBACKS = { 'CLUSTER ADDSLOTS': bool_ok, 'CLUSTER COUNT-FAILURE-REPORTS': lambda x: int(x), 'CLUSTER COUNTKEYSINSLOT': lambda x: int(x), 'CLUSTER DELSLOTS': bool_ok, 'CLUSTER FAILOVER': bool_ok, 'CLUSTER FORGET': bool_ok, 'CLUSTER INFO': parse_cluster_info, 'CLUSTER KEYSLOT': lambda x: int(x), 'CLUSTER MEET': bool_ok, 'CLUSTER NODES': parse_cluster_nodes, 'CLUSTER REPLICATE': bool_ok, 'CLUSTER RESET': bool_ok, 'CLUSTER SAVECONFIG': bool_ok, 'CLUSTER SET-CONFIG-EPOCH': bool_ok, 'CLUSTER SETSLOT': bool_ok, 'CLUSTER SLAVES': parse_cluster_nodes, 'CLUSTER SLOTS': parse_cluster_slots, 'ASKING': bool_ok, 'READONLY': bool_ok, 'READWRITE': bool_ok, } RESULT_CALLBACKS = dict_merge( list_keys_to_dict([ 'CLUSTER INFO', 'CLUSTER ADDSLOTS', 'CLUSTER COUNT-FAILURE-REPORTS', 'CLUSTER DELSLOTS', 'CLUSTER FAILOVER', 'CLUSTER FORGET' ], lambda res: res)) def _nodes_slots_to_slots_nodes(self, mapping): """ Converts a mapping of {id: <node>, slots: (slot1, slot2)} to {slot1: <node>, slot2: <node>} Operation is expensive so use with caution """ out = {} for node in mapping: for slot in node['slots']: out[str(slot)] = node['id'] return out async def cluster_addslots(self, node_id, *slots): """ Assign new hash slots to receiving node Sends to specefied node """ return await self.execute_command('CLUSTER ADDSLOTS', *slots, node_id=node_id) async def cluster_count_failure_report(self, node_id=''): """ Return the number of failure reports active for a given node Sends to specefied node """ return await self.execute_command('CLUSTER COUNT-FAILURE-REPORTS', node_id=node_id) async def cluster_countkeysinslot(self, slot_id): """ Return the number of local keys in the specified hash slot Send to node based on specefied slot_id """ return await self.execute_command('CLUSTER COUNTKEYSINSLOT', slot_id) async 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(await self.cluster_nodes()) res = list() for slot in slots: res.append(await self.execute_command('CLUSTER DELSLOTS', slot, node_id=cluster_nodes[slot])) return res async def cluster_failover(self, node_id, option): """ Forces a slave to perform a manual failover of its master Sends to specefied node """ if not isinstance(option, str) or option.upper() not in { 'FORCE', 'TAKEOVER' }: raise ClusterError('Wrong option provided') return await self.execute_command('CLUSTER FAILOVER', option, node_id=node_id) async def cluster_forget(self, node_id): """ remove a node via its node ID from the set of known nodes of the Redis Cluster node receiving the command Sends to all nodes in the cluster """ return await self.execute_command('CLUSTER FORGET', node_id) async def cluster_info(self): """ Provides info about Redis Cluster node state Sends to random node in the cluster """ return await self.execute_command('CLUSTER INFO') async def cluster_keyslot(self, name): """ Returns the hash slot of the specified key Sends to random node in the cluster """ return await self.execute_command('CLUSTER KEYSLOT', name) async def cluster_meet(self, node_id, host, port): """ Force a node cluster to handshake with another node. Sends to specefied node """ return await self.execute_command('CLUSTER MEET', host, port, node_id=node_id) async def cluster_nodes(self): """ Force a node cluster to handshake with another node Sends to random node in the cluster """ return await self.execute_command('CLUSTER NODES') async def cluster_replicate(self, target_node_id): """ Reconfigure a node as a slave of the specified master node Sends to specefied node """ return await self.execute_command('CLUSTER REPLICATE', target_node_id) async def cluster_reset(self, node_id, soft=True): """ 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 """ option = 'SOFT' if soft else 'HARD' return await self.execute_command('CLUSTER RESET', option, node_id=node_id) async 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 """ option = 'SOFT' if soft else 'HARD' res = list() for node in await self.cluster_nodes(): res.append(await self.execute_command('CLUSTER RESET', option, node_id=node['id'])) return res async def cluster_save_config(self): """ Forces the node to save cluster state on disk Sends to all nodes in the cluster """ return await self.execute_command('CLUSTER SAVECONFIG') async def cluster_set_config_epoch(self, node_id, epoch): """ Set the configuration epoch in a new node Sends to specefied node """ return await self.execute_command('CLUSTER SET-CONFIG-EPOCH', epoch, node_id=node_id) async def cluster_setslot(self, node_id, slot_id, state): """ Bind an hash slot to a specific node Sends to specified node """ if state.upper() in {'IMPORTING', 'MIGRATING', 'NODE' } and node_id is not None: return await self.execute_command('CLUSTER SETSLOT', slot_id, state, node_id) elif state.upper() == 'STABLE': return await self.execute_command('CLUSTER SETSLOT', slot_id, 'STABLE') else: raise RedisError('Invalid slot state: {0}'.format(state)) async def cluster_get_keys_in_slot(self, slot_id, count): """ Return local key names in the specified hash slot Sends to specified node """ return await self.execute_command('CLUSTER GETKEYSINSLOT', slot_id, count) async def cluster_slaves(self, target_node_id): """ Force a node cluster to handshake with another node Sends to targeted cluster node """ return await self.execute_command('CLUSTER SLAVES', target_node_id) async def cluster_slots(self): """ Get array of Cluster slot to node mappings Sends to random node in the cluster """ return await self.execute_command('CLUSTER SLOTS')