Example #1
0
 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)
Example #2
0
 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]
Example #3
0
 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])
Example #4
0
 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)
Example #5
0
 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)
Example #6
0
 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))
Example #7
0
 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)
Example #8
0
 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
Example #9
0
    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))