async def send_cluster_transaction(self, stack, raise_on_error=True): # the first time sending the commands we send all of the commands that were queued up. # if we have to run through it again, we only retry the commands that failed. attempt = sorted(stack, key=lambda x: x.position) node = {} # as we move through each command that still needs to be processed, # we figure out the slot number that command maps to, then from the slot determine the node. for c in attempt: # refer to our internal node -> slot table that tells us where a given # command should route to. slot = self._determine_slot(*c.args) hashed_node = self.connection_pool.get_node_by_slot(slot) # now that we know the name of the node ( it's just a string in the form of host:port ) # we can build a list of commands for each node. if node.get('name') != hashed_node['name']: # raise error if commands in a transaction can not hash to same node if len(node) > 0: raise ClusterTransactionError( "Keys in request don't hash to the same node") node = hashed_node conn = self.connection_pool.get_connection_by_node(node) if self.watches: await self._watch(node, conn, self.watches) node_commands = NodeCommands(self.parse_response, conn, in_transaction=True) node_commands.append(PipelineCommand(('MULTI', ))) node_commands.extend(attempt) node_commands.append(PipelineCommand(('EXEC', ))) self.explicit_transaction = True await node_commands.write() # todo: make this place clear try: await node_commands.read() except ExecAbortError: if self.explicit_transaction: await conn.send_command('DISCARD') await conn.read_response() # If at least one watched key is modified before the EXEC command, # the whole transaction aborts, # and EXEC returns a Null reply to notify that the transaction failed. if node_commands.commands[-1].result is None: raise WatchError self.connection_pool.release(conn) if self.watching: self._unwatch(conn) if raise_on_error: self.raise_first_error(stack)
async def _watch(self, node, conn, names): "Watches the values at keys ``names``" for name in names: slot = self._determine_slot('WATCH', name) dist_node = self.connection_pool.get_node_by_slot(slot) if node.get('name') != dist_node['name']: # raise error if commands in a transaction can not hash to same node if len(node) > 0: raise ClusterTransactionError( "Keys in request don't hash to the same node") if self.explicit_transaction: raise RedisError('Cannot issue a WATCH after a MULTI') await conn.send_command('WATCH', *names) return await conn.read_response()