def nack(self, message, subscription_id: Optional[int] = None, **kwargs): """Reject receipt of a message. This only makes sense when the 'acknowledgement' flag was set for the relevant subscription. :param message: ID of the message to be rejected, OR a dictionary containing a field 'message-id'. :param subscription_id: ID of the associated subscription. Optional when a dictionary is passed as first parameter and that dictionary contains field 'subscription'. :param **kwargs: Further parameters for the transport layer. For example transaction: Transaction ID if rejection should be part of a transaction """ if isinstance(message, dict): message_id = message.get("message-id") if not subscription_id: subscription_id = message.get("subscription") else: message_id = message if not message_id: raise workflows.Error("Cannot reject message without message ID") if not subscription_id: raise workflows.Error( "Cannot reject message without subscription ID") self.log.debug("Rejecting message %s on subscription %d", message_id, subscription_id) self._nack(message_id, subscription_id=subscription_id, **kwargs)
def drop_callback_reference(self, subscription: int): """Drop reference to the callback function after unsubscribing. Any future messages arriving for that subscription will result in exceptions being raised. :param subscription: Subscription ID to delete callback reference for. """ if subscription not in self.__subscriptions: raise workflows.Error( "Attempting to drop callback reference for unknown subscription" ) if not self.__subscriptions[subscription]["unsubscribed"]: raise workflows.Error( "Attempting to drop callback reference for live subscription") del self.__subscriptions[subscription]
def find_cycles(path): """Depth-First-Search helper function to identify cycles.""" if path[-1] not in self.recipe: raise workflows.Error( 'Invalid recipe: Node "%s" is referenced via "%s" but missing' % (str(path[-1]), str(path[:-1]))) touched_nodes.add(path[-1]) node = self.recipe[path[-1]] for outgoing in ("output", "error"): if outgoing in node: references = flatten_links(node[outgoing]) for n in references: if n in path: raise workflows.Error( "Invalid recipe: Recipe contains cycle (%s -> %s)" % (str(path), str(n))) find_cycles(path + [n])
def transaction_commit(self, transaction_id: int, **kwargs): """Commit a transaction. :param transaction_id: ID of transaction to be committed. :param **kwargs: Further parameters for the transport layer. """ if transaction_id not in self.__transactions: raise workflows.Error("Attempting to commit unknown transaction") self.log.debug("Committing transaction %s", transaction_id) self.__transactions.remove(transaction_id) self._transaction_commit(transaction_id, **kwargs)
def transaction_abort(self, transaction_id: int, **kwargs): """Abort a transaction and roll back all operations. :param transaction_id: ID of transaction to be aborted. :param **kwargs: Further parameters for the transport layer. """ if transaction_id not in self.__transactions: raise workflows.Error("Attempting to abort unknown transaction") self.log.debug("Aborting transaction %s", transaction_id) self.__transactions.remove(transaction_id) self._transaction_abort(transaction_id, **kwargs)
def flatten_links(struct): """Take an output/error link object, list or dictionary and return flat list of linked nodes.""" if struct is None: return [] if isinstance(struct, int): return [struct] if isinstance(struct, list): if not all(isinstance(x, int) for x in struct): raise workflows.Error( "Invalid recipe: Invalid link in recipe (%s)" % str(struct)) return struct if isinstance(struct, dict): joined_list = [] for sub_list in struct.values(): joined_list += flatten_links(sub_list) return joined_list raise workflows.Error( "Invalid recipe: Invalid link in recipe (%s)" % str(struct))
def unsubscribe(self, subscription: int, drop_callback_reference=False, **kwargs): """Stop listening to a queue or a broadcast :param subscription: Subscription ID to cancel :param drop_callback_reference: Drop the reference to the registered callback function immediately. This means any buffered messages still in flight will not arrive at the intended destination and cause exceptions to be raised instead. :param **kwargs: Further parameters for the transport layer. """ if subscription not in self.__subscriptions: raise workflows.Error( "Attempting to unsubscribe unknown subscription") if self.__subscriptions[subscription]["unsubscribed"]: raise workflows.Error( "Attempting to unsubscribe already unsubscribed subscription") self._unsubscribe(subscription, **kwargs) self.__subscriptions[subscription]["unsubscribed"] = True if drop_callback_reference: self.drop_callback_reference(subscription)
def subscription_callback(self, subscription: int) -> MessageCallback: """Retrieve the callback function for a subscription. Raise a workflows.Error if the subscription does not exist. All transport callbacks can be intercepted by setting an interceptor function with subscription_callback_intercept(). :param subscription: Subscription ID to look up :return: Callback function """ subscription_record = self.__subscriptions.get(subscription) if not subscription_record: raise workflows.Error( "Attempting to callback on unknown subscription") callback = subscription_record["callback"] if self.__callback_interceptor: return self.__callback_interceptor(callback) return callback
def validate(self): """Check whether the encoded recipe is valid. It must describe a directed acyclical graph, all connections must be defined, etc.""" if not self.recipe: raise workflows.Error("Invalid recipe: No recipe defined") # Without a 'start' node nothing would happen if "start" not in self.recipe: raise workflows.Error('Invalid recipe: "start" node missing') if not self.recipe["start"]: raise workflows.Error('Invalid recipe: "start" node empty') if not all( isinstance(x, (list, tuple)) and len(x) == 2 for x in self.recipe["start"]): raise workflows.Error('Invalid recipe: "start" node invalid') if any(x[0] == "start" for x in self.recipe["start"]): raise workflows.Error( 'Invalid recipe: "start" node points to itself') # Check that 'error' node points to regular nodes only if "error" in self.recipe and isinstance(self.recipe["error"], (list, tuple, basestring)): if "start" in self.recipe["error"]: raise workflows.Error( 'Invalid recipe: "error" node points to "start" node') if "error" in self.recipe["error"]: raise workflows.Error( 'Invalid recipe: "error" node points to itself') # All other nodes must be numeric nodes = list( filter( lambda x: not isinstance(x, int) and x not in ("start", "error"), self.recipe, )) if nodes: raise workflows.Error('Invalid recipe: Node "%s" is not numeric' % nodes[0]) # Detect cycles touched_nodes = {"start", "error"} def flatten_links(struct): """Take an output/error link object, list or dictionary and return flat list of linked nodes.""" if struct is None: return [] if isinstance(struct, int): return [struct] if isinstance(struct, list): if not all(isinstance(x, int) for x in struct): raise workflows.Error( "Invalid recipe: Invalid link in recipe (%s)" % str(struct)) return struct if isinstance(struct, dict): joined_list = [] for sub_list in struct.values(): joined_list += flatten_links(sub_list) return joined_list raise workflows.Error( "Invalid recipe: Invalid link in recipe (%s)" % str(struct)) def find_cycles(path): """Depth-First-Search helper function to identify cycles.""" if path[-1] not in self.recipe: raise workflows.Error( 'Invalid recipe: Node "%s" is referenced via "%s" but missing' % (str(path[-1]), str(path[:-1]))) touched_nodes.add(path[-1]) node = self.recipe[path[-1]] for outgoing in ("output", "error"): if outgoing in node: references = flatten_links(node[outgoing]) for n in references: if n in path: raise workflows.Error( "Invalid recipe: Recipe contains cycle (%s -> %s)" % (str(path), str(n))) find_cycles(path + [n]) for link in self.recipe["start"]: find_cycles(["start", link[0]]) if "error" in self.recipe: if isinstance(self.recipe["error"], (list, tuple)): for link in self.recipe["error"]: find_cycles(["error", link]) else: find_cycles(["error", self.recipe["error"]]) # Test recipe for unreferenced nodes for node in self.recipe: if node not in touched_nodes: raise workflows.Error( 'Invalid recipe: Recipe contains unreferenced node "%s"' % str(node))