예제 #1
0
    def _fire_reply(self, message, reply_to):
        """
        Tries to fire a reply to the given message

        :param message: Message to send as a reply
        :param reply_to: Message the first argument replies to
        :return: The UID of the sent message
        """
        # Use the message source peer
        try:
            transport = self._transports[reply_to.access]
        except KeyError:
            # Reception transport is not available anymore...
            raise NoTransport(beans.Target(uid=reply_to.sender),
                              "No reply transport for access {0}"
                              .format(reply_to.access))
        else:
            # Try to get the Peer bean. If unknown, consider that the
            # "extra" data will help the transport to reply
            try:
                peer = self._directory.get_peer(reply_to.sender)
            except KeyError:
                peer = None

            try:
                # Send the reply
                transport.fire(peer, message, reply_to.extra)
            except InvalidPeerAccess:
                raise NoTransport(beans.Target(uid=reply_to.sender),
                                  "Can't reply to {0} using {1} transport"
                                  .format(peer, reply_to.access))
            else:
                # Reply sent. Stop here
                return message.uid
예제 #2
0
    def fire(self, target, message):
        """
        Fires (and forget) the given message to the target

        :param target: The UID of a Peer, or a Peer object
        :param message: A Message bean
        :return: The UID of the message sent
        :raise KeyError: Unknown peer UID
        :raise NoTransport: No transport found to send the message
        """
        # Standard behavior
        # Get the Peer object
        if not isinstance(target, beans.Peer):
            peer = self._directory.get_peer(target)
        else:
            peer = target

        # Check if some transports are bound
        if not self._transports:
            raise NoTransport(beans.Target(uid=peer.uid),
                              "No transport bound yet.")

        # Get accesses
        accesses = peer.get_accesses()
        for access in accesses:
            try:
                transport = self._transports[access]
            except KeyError:
                # No transport for this kind of access
                pass
            else:
                try:
                    # Call it
                    transport.fire(peer, message)
                except InvalidPeerAccess as ex:
                    # Transport can't read peer access data
                    _logger.debug("Error reading access for transport %s: %s",
                                  access, ex)
                except Exception as ex:
                    # Exception during transport
                    _logger.warning("Error using transport %s: %s", access, ex)
                else:
                    # Success
                    break
        else:
            # No transport for those accesses
            raise NoTransport(beans.Target(uid=peer.uid),
                              "No working transport found for peer {0}"
                              .format(peer))

        return message.uid
예제 #3
0
    def send(self, target, message, timeout=None):
        """
        Sends a message, and waits for its reply

        :param target: The UID of a Peer, or a Peer object
        :param message: A Message bean
        :param timeout: Maximum time to wait for an answer
        :return: The reply message bean
        :raise KeyError: Unknown peer UID
        :raise NoTransport: No transport found to send the message
        :raise NoListener: Message received, but nobody was registered to
                           listen to it
        :raise HeraldTimeout: Timeout raised before getting an answer
        """
        # Prepare an event, which will be set when the answer will be received
        event = pelix.utilities.EventData()
        self.__waiting_events[message.uid] = event

        # Get the Peer object
        if not isinstance(target, beans.Peer):
            peer = self._directory.get_peer(target)
        else:
            peer = target

        try:
            # Fire the message
            self.fire(peer, message)

            # Message sent, wait for an answer
            if event.wait(timeout):
                if event.data is not None:
                    return event.data
                else:
                    # Message cancelled due to invalidation
                    raise HeraldTimeout(beans.Target(uid=peer.uid),
                                        "Herald stops listening to messages",
                                        message)
            else:
                raise HeraldTimeout(beans.Target(uid=peer.uid),
                                    "Timeout reached before receiving a reply",
                                    message)
        finally:
            try:
                # Clean up
                del self.__waiting_events[message.uid]
            except KeyError:
                # Ignore errors at this point
                pass
예제 #4
0
    def fire(self, peer, message, extra=None):
        """
        Fires a message to a peer

        :param peer: A Peer bean
        :param message: Message bean to send
        :param extra: Extra information used in case of a reply
        :raise InvalidPeerAccess: No information found to access the peer
        :raise Exception: Error sending the request or on the server side
        """
        # Get the request message UID, if any
        parent_uid = None
        if extra is not None:
            parent_uid = extra.get('parent_uid')

        # Try to read extra information
        url = self.__get_access(peer, extra)
        if not url:
            # No HTTP access description
            raise InvalidPeerAccess(beans.Target(uid=peer.uid),
                                    "No '{0}' access found"
                                    .format(self._access_id))

        # Send the HTTP request (blocking) and raise an error if necessary
        headers, content = self.__prepare_message(message, parent_uid)
        response = self.__session.post(url, content, headers=headers)
        response.raise_for_status()
예제 #5
0
    def reply(self, message, content, subject=None):
        """
        Replies to a message

        :param message: Original message
        :param content: Content of the response
        :param subject: Reply message subject (same as request if None)
        :raise NoTransport: No transport/access found to send the reply
        """
        # Normalize subject. By default, add a 'reply' prefix,
        # to avoid potential loops
        if not subject:
            subject = '/'.join(('reply', message.subject))

        try:
            # Try to reuse the same transport
            self._fire_reply(beans.Message(subject, content), message)
        except NoTransport:
            # Continue...
            pass
        else:
            # No error
            return

        # If not possible: fire a standard reply
        try:
            # Fire the reply
            self.fire(message.sender, beans.Message(subject, content))
        except KeyError:
            # Convert KeyError to NoTransport
            raise NoTransport(beans.Target(uid=message.sender),
                              "No access to reply to {0}"
                              .format(message.sender))
예제 #6
0
    def _handle_error(self, message, kind):
        """
        Handles an error message

        :param message: MessageReceived bean, received from another peer
        :param kind: Kind of error
        """
        if kind == 'no-listener':
            # No listener found for a given message
            # ... release send() calls
            try:
                # Get the original message UID and Subject
                uid = message.content['uid']
                exception = NoListener(beans.Target(message.sender), uid,
                                       message.content['subject'])
            except KeyError:
                # Invalid error content...
                return

            try:
                # Unlock the poster with an exception
                self.__waiting_events.pop(uid).raise_exception(exception)
            except KeyError:
                # Nobody was waiting for the event
                pass

            # ... notify post() callers
            try:
                self.__waiting_posts[uid].errback(self, exception)
            except KeyError:
                # No error callback for this message
                pass
예제 #7
0
    def fire(self, peer, message, extra=None):
        """
        Fires a message to a peer

        :param peer: A Peer bean
        :param message: Message bean to send
        :param extra: Extra information used in case of a reply
        :raise InvalidPeerAccess: No information found to access the peer
        :raise Exception: Error sending the request or on the server side
        """
        # Get the request message UID, if any
        parent_uid = None
        if extra is not None:
            parent_uid = extra.get('parent_uid')

        # Try to read extra information
        url = self.__get_access(peer, extra)
        if not url:
            # No HTTP access description
            raise InvalidPeerAccess(
                beans.Target(peer=peer),
                "No '{0}' access found".format(self._access_id))
        # Send the HTTP request (blocking) and raise an error if necessary
        headers, content = self.__prepare_message(message,
                                                  parent_uid,
                                                  target_peer=peer)

        # Log before sending
        self._probe.store(
            herald.PROBE_CHANNEL_MSG_SEND, {
                "uid": message.uid,
                "timestamp": time.time(),
                "transport": ACCESS_ID,
                "subject": message.subject,
                "target": peer.uid if peer else "<unknown>",
                "transportTarget": url,
                "repliesTo": parent_uid or ""
            })

        self._probe.store(herald.PROBE_CHANNEL_MSG_CONTENT, {
            "uid": message.uid,
            "content": content
        })

        response = self.__post_message(url, content, headers)
        if response is None:
            # The error has been logged in post_message
            raise IOError("Error sending message {0}".format(message.uid))
        else:
            # Raise an error if the status isn't 2XX
            response.raise_for_status()
예제 #8
0
    def fire(self, peer, message, extra=None):
        """
        Fires a message to a peer

        :param peer: A Peer bean
        :param message: Message to send
        :param extra: Extra information used in case of a reply
        """
        # Get the request message UID, if any
        parent_uid = None
        if extra is not None:
            parent_uid = extra.get('parent_uid')

        # Try to read extra information
        jid = self.__get_jid(peer, extra)

        if jid:
            # Log before sending
            self._probe.store(
                herald.PROBE_CHANNEL_MSG_SEND, {
                    "uid": message.uid,
                    "timestamp": time.time(),
                    "transport": ACCESS_ID,
                    "subject": message.subject,
                    "target": peer.uid if peer else "<unknown>",
                    "transportTarget": str(jid),
                    "repliesTo": parent_uid or ""
                })

            # Send the XMPP message
            self.__send_message("chat",
                                jid,
                                message,
                                parent_uid,
                                target_peer=peer)
        else:
            # No XMPP access description
            raise InvalidPeerAccess(
                beans.Target(uid=peer.uid),
                "No '{0}' access found".format(self._access_id))
예제 #9
0
    def fire(self, peer, message, extra=None):
        """
        Fires a message to a peer

        :param peer: A Peer bean
        :param message: Message to send
        :param extra: Extra information used in case of a reply
        """
        # Get the request message UID, if any
        parent_uid = None
        if extra is not None:
            parent_uid = extra.get('parent_uid')

        # Try to read extra information
        jid = self.__get_jid(peer, extra)

        if jid:
            # Send the XMPP message
            self.__send_message("chat", jid, message, parent_uid)
        else:
            # No XMPP access description
            raise InvalidPeerAccess(
                beans.Target(uid=peer.uid),
                "No '{0}' access found".format(self._access_id))
예제 #10
0
    def post_group(self, group, message, callback, errback,
                   timeout=180):
        """
        Posts a message to a group of peers. The given methods will be called
        back as soon as a result is given, or in case of error.

        If no timeout is given, the message UID must be forgotten manually.

        :param group: The name of a group of peers
        :param message: A Message bean
        :param callback: Method to call back when a reply is received
        :param errback: Method to call back if an error occurs
        :param timeout: Time after which the message will be forgotten
        :return: The message UID
        :raise KeyError: Unknown group
        :raise NoTransport: No transport found to send the message
        """
        # Get all peers known in the group
        all_peers = self._directory.get_peers_for_group(group)

        # Check if some transports are bound
        if not self._transports:
            raise NoTransport(
                beans.Target(group=group,
                             uids=[peer.uid for peer in all_peers]),
                "No transport bound yet.")

        with self.__gc_lock:
            # Prepare an entry in the waiting posts
            self.__waiting_posts[message.uid] = \
                _WaitingPost(callback, errback, timeout, False)

        # Find the common accesses
        accesses = {}
        for peer in all_peers:
            for access in peer.get_accesses():
                accesses.setdefault(access, set()).add(peer)

        for access, access_peers in accesses.items():
            if not access_peers:
                # Nothing to do
                continue

            try:
                transport = self._transports[access]
            except KeyError:
                # No transport for this kind of access
                pass
            else:
                try:
                    # Call it
                    transport.fire_group(group, access_peers, message)
                except InvalidPeerAccess:
                    # Transport can't find group access data
                    pass
                else:
                    # Success: clean up waiting peers
                    all_done = True
                    for remaining_peers in accesses.values():
                        remaining_peers.difference_update(access_peers)
                        if remaining_peers:
                            # Still some peers to notify
                            all_done = False

                    if all_done:
                        break

        return message.uid
예제 #11
0
    def fire_group(self, group, message):
        """
        Fires (and forget) the given message to the given group of peers

        :param group: The name of a group of peers
        :param message: A Message bean
        :return: A tuple: the UID of the message sent and the list of peers
        :raise KeyError: Unknown group
        :raise NoTransport: No transport found to send the message
        """
        # Get all peers known in the group
        all_peers = self._directory.get_peers_for_group(group)
        if not all_peers:
            _logger.warning("No peer in group %s", group)
            return message.uid, set()

        # Check if some transports are bound
        if not self._transports:
            raise NoTransport(
                beans.Target(group=group,
                             uids=[peer.uid for peer in all_peers]),
                "No transport bound yet.")

        # Find the common accesses
        accesses = {}
        for peer in all_peers:
            for access in peer.get_accesses():
                accesses.setdefault(access, set()).add(peer)

        missing = []
        for access, access_peers in accesses.items():
            if not access_peers:
                # Nothing to do
                continue

            try:
                transport = self._transports[access]
            except KeyError:
                # No transport for this kind of access
                _logger.debug("No transport for %s", access)
            else:
                try:
                    # Call it
                    reached_peers = transport.fire_group(group, access_peers,
                                                         message)
                    if reached_peers is None:
                        reached_peers = access_peers
                except InvalidPeerAccess as ex:
                    # Transport can't find group access data
                    _logger.debug("Missing access info: %s", ex)
                else:
                    # Success: clean up waiting peers
                    all_done = True
                    for remaining_peers in accesses.values():
                        remaining_peers.difference_update(reached_peers)
                        if remaining_peers:
                            # Still some peers to notify
                            all_done = False

                    if all_done:
                        break
        else:
            missing = set(itertools.chain(*accesses.values()))
            if missing:
                _logger.warning("Some peers haven't been notified: %s",
                                ', '.join(str(peer) for peer in missing))
            else:
                _logger.debug("No peer to send the message to.")

        return message.uid, missing