예제 #1
0
class MessageServerClient(object):

    _BACKOFF_INTERVALS = (1, 1, 1, 5, 10, 30, 60)
    _MAX_BUFFER_SIZE = 1000
    _RECEIVE_TIMEOUT = 2

    def __init__(self, address):
        '''
        MessageServerClient constructor.

        @param address: address to send the messages to
        @type address: str
        '''

        self._address = 'tcp://%s' % address
        self._socket = None
        self._context = None

        self._queue = Queue()
        self._processQueueInLoop()

    @property
    def address(self):
        return self._address

    def ping(self):
        '''
        Sends a ping message to the server and checks if it answers.

        @return: True when the server answered, False otherwise
        @rtype: bool
        '''

        return self._send('ping', receiveTimeout=1)

    def send(self, message):
        '''
        Puts a message in the queue and immediately after that yields. This will
        give the worker which is running in a Greenlet the time to process the
        queue.

        @param message: message to send to the server
        @type message: str
        '''

        self._queue.put(message)
        gevent.sleep()

    @property
    def _isConnected(self):
        '''
        Returns a flag that states if the client is connected or not.

        @return: flag that states if the client is connected or not
        @rtype: bool
        '''

        if self._context:
            return not self._context.closed
        else:
            return False

    def _connect(self):
        '''Connects the client if it isn't already'''

        if self._isConnected:
            printInDebugMode('Can\'t connect to %s, already connected' %
                             self._address)
            return

        printInDebugMode('Connecting to %s' % self._address)
        self._context = zmq.Context(2)
        self._socket = self._context.socket(zmq.REQ)
        self._socket.setsockopt(zmq.LINGER, 0)
        self._socket.connect(self._address)
        printInDebugMode('Connected to %s' % self._address)

    def _disconnect(self):
        '''Disconnects the client if it isn't already'''

        if not self._isConnected:
            printInDebugMode(
                'Can\'t disconnect from %s, already disconnected' %
                self._address)
            return

        printInDebugMode('Disconnecting from %s' % self._address)
        self._socket.close()
        self._context.term()
        printInDebugMode('Disconnected from %s' % self._address)

    def _processQueue(self):
        '''
        Tries to process as many as possible queued messages. A message buffer
        is updated on every attempt. This buffer is then flattened to a single
        message. This reduces the amount of roundtrips with the server.

        When a message is not send succesfully a series of retry attempts will
        be completed before aborting. After every failed attempt the client
        backs off for a fixed amound of seconds.

        @return: True when a part of the queue is succesfully processes, False otherwise
        @rtype: bool
        '''

        maxAttempts = len(self._BACKOFF_INTERVALS) + 1
        isSucces = False
        buffer = list()

        for attempt in range(1, maxAttempts):
            buffer = self._updateBuffer(buffer)
            message = ''.join(
                buffer)  # The message is actually a batch of messages.

            isSucces = self._send(message)

            if isSucces:
                break
            else:
                interval = self._BACKOFF_INTERVALS[attempt - 1]

                printInDebugMode(
                    '''\
Failed to send message to %(address)s in %(attempts)d attempt(s)'
Retrying to send message to %(address)s in %(interval)d seconds''' % {
                        'address': self._address,
                        'attempts': attempt,
                        'interval': interval
                    })

                gevent.sleep(interval)

        if isSucces:
            printInDebugMode(
                'Successfully sent message to %s in %d attempt(s)' %
                (self._address, attempt))
        else:
            printInDebugMode('Failed to send message to %s in %d attempt(s)' %
                             (self._address, attempt))

        return isSucces

    def _processQueueInLoop(self):
        '''
        Spawns a worker in a Greenlet that processes the queue when possible.
        '''
        def worker():
            while True:
                self._processQueue()

        gevent.spawn(worker)

    def _updateBuffer(self, buffer):
        '''
        Updates a buffer by moving as much messages as possible from the queue
        to the buffer. This method will block if the queue is empty. Once a
        message is put in the queue the buffer will be updated.

        @param buffer: buffer to update
        @type buffer: set()

        @return: updated buffer
        @rtype: set()
        '''

        while len(buffer) < self._MAX_BUFFER_SIZE:
            # The get call on the queue will block until a message is put in the
            # queue.
            message = self._queue.get()
            buffer.append(message)

            if self._queue.empty():
                break

        return buffer

    def _send(self, message, receiveTimeout=_RECEIVE_TIMEOUT):
        '''
        Tries to send a message to the server and waits a fixed amount of
        seconds for it to answer.

        @param message: message to send to the server
        @type message: str

        @param receiveTimeout: max time in seconds in which the server should respond
        @type receiveTimeout: int
        '''

        if not self._isConnected:
            self._connect()

        try:
            self._socket.send(message)
        except Exception, e:
            self._disconnect()
            printInDebugMode('Couldn\'t send message, unexpected exception'
                             ' while sending to server: %s' % e)

        result = None

        timeout = Timeout(receiveTimeout)
        timeout.start()

        try:
            result = self._socket.recv()
        except Timeout, timeoutException:
            if timeoutException == timeout:
                printInDebugMode(
                    'Couldn\'t send message, server response timed'
                    ' out after %d seconds' % receiveTimeout)

            self._disconnect()
예제 #2
0
    def wsgi_app(self, environ, start_response):
        """Execute this instance as a WSGI application.

        See the PEP for the meaning of parameters. The separation of
        __call__ and wsgi_app eases the insertion of middlewares.

        """
        request = Request(environ)
        request.encoding_errors = "strict"

        # The problem here is that we'd like to send an infinite stream
        # of events, but WSGI has been designed to handle only finite
        # responses. Hence, to do this we will have to "abuse" the API
        # a little. This works well with gevent's pywsgi implementation
        # but it may not with others (still PEP-compliant). Therefore,
        # just to be extra-safe, we will terminate the response anyway,
        # after a long timeout, to make it finite.

        # The first such "hack" is the mechanism to trigger the chunked
        # transfer-encoding. The PEP states just that "the server *may*
        # use chunked encoding" to send each piece of data we give it,
        # if we don't specify a Content-Length header and if both the
        # client and the server support it. According to the HTTP spec.
        # all (and only) HTTP/1.1 compliant clients have to support it.
        # We'll assume that the server software supports it too, and
        # actually uses it (gevent does!) even if we have no way to
        # check it. We cannot try to force such behavior as the PEP
        # doesn't even allow us to set the Transfer-Encoding header.

        # The second abuse is the use of the write() callable, returned
        # by start_response, even if the PEP strongly discourages its
        # use in new applications. We do it because we need a way to
        # detect when the client disconnects, and we hope to achieve
        # this by seeing when a call to write() fails, i.e. raises an
        # exception. This behavior isn't documented by the PEP, but it
        # seems reasonable and it's present in gevent (which raises a
        # socket.error).

        # The third non-standard behavior that we expect (related to
        # the previous one) is that no one in the application-to-client
        # chain does response buffering: neither any middleware nor the
        # server (gevent doesn't!). This should also hold outside the
        # server realm (i.e. no proxy buffering) but that's definitely
        # not our responsibility.

        # The fourth "hack" is to avoid an error to be printed on the
        # logs. If the client terminates the connection, we catch and
        # silently ignore the exception and return gracefully making
        # the server try to write the last zero-sized chunk (used to
        # mark the end of the stream). This will fail and produce an
        # error. To avoid this we detect if we're running on a gevent
        # server and make it "forget" this was a chunked response.

        # Check if the client will understand what we will produce.
        if request.accept_mimetypes.quality("text/event-stream") <= 0:
            return NotAcceptable()(environ, start_response)

        # Initialize the response and get the write() callback. The
        # Cache-Control header is useless for conforming clients, as
        # the spec. already imposes that behavior on them, but we set
        # it explicitly to avoid unwanted caching by unaware proxies and
        # middlewares.
        write = start_response(
            text_to_native_str("200 OK"),
            [(text_to_native_str("Content-Type"),
              text_to_native_str("text/event-stream; charset=utf-8")),
             (text_to_native_str("Cache-Control"),
              text_to_native_str("no-cache"))])

        # This is a part of the fourth hack (see above).
        if hasattr(start_response, "__self__") and \
                isinstance(start_response.__self__, WSGIHandler):
            handler = start_response.__self__
        else:
            handler = None

        # One-shot means that we will terminate the request after the
        # first batch of sent events. We do this when we believe the
        # client doesn't support chunked transfer. As this encoding has
        # been introduced in HTTP/1.1 (as mandatory!) we restrict to
        # requests in that HTTP version. Also, if it comes from an
        # XMLHttpRequest it has been probably sent from a polyfill (not
        # from the native browser implementation) which will be able to
        # read the response body only when it has been fully received.
        if environ["SERVER_PROTOCOL"] != "HTTP/1.1" or request.is_xhr:
            one_shot = True
        else:
            one_shot = False

        # As for the Server-Sent Events [1] spec., this is the way for
        # the client to tell us the ID of the last event it received
        # and to ask us to send it the ones that happened since then.
        # [1] http://www.w3.org/TR/eventsource/
        # The spec. requires implementations to retry the connection
        # when it fails, adding the "Last-Event-ID" HTTP header. But in
        # case of an error they stop, and we have to (manually) delete
        # the EventSource and create a new one. To obtain that behavior
        # again we give the "last_event_id" as a URL query parameter
        # (with lower priority, to have the header override it).
        last_event_id = request.headers.get("Last-Event-ID")
        if last_event_id is None:
            last_event_id = request.args.get("last_event_id")

        # We subscribe to the publisher to receive events.
        sub = self._pub.get_subscriber(last_event_id)

        # Send some data down the pipe. We need that to make the user
        # agent announces the connection (see the spec.). Since it's a
        # comment it will be ignored.
        write(b":\n")

        # XXX We could make the client change its reconnection timeout
        # by sending a "retry:" line.

        # As a last line of defence from very bad-behaving servers we
        # don't want to the request to last longer than _GLOBAL_TIMEOUT
        # seconds (see above). We use "False" to just cause the control
        # exit the with block, instead of raising an exception.
        with Timeout(self._GLOBAL_TIMEOUT, False):
            # Repeat indefinitely.
            while True:
                # Proxies often have a read timeout. We try not to hit
                # it by not being idle for more than _PING_TIMEOUT
                # seconds, sending a ping (i.e. a comment) if there's
                # no real data.
                try:
                    with Timeout(self._PING_TIMEOUT):
                        data = b"".join(sub.get())
                        got_sth = True
                except Timeout:
                    data = b":\n"
                    got_sth = False

                try:
                    with Timeout(self._WRITE_TIMEOUT):
                        write(data)
                # The PEP doesn't tell what has to happen when a write
                # fails. We're conservative, and allow any unexpected
                # event to interrupt the request. We hope it's enough
                # to detect when the client disconnects. It is with
                # gevent, which raises a socket.error. The timeout (we
                # catch that too) is just an extra precaution.
                except Exception:
                    # This is part of the fourth hack (see above).
                    if handler is not None:
                        handler.response_use_chunked = False
                    break

                # If we decided this is one-shot, stop the long-poll as
                # soon as we sent the client some real data.
                if one_shot and got_sth:
                    break

        # An empty iterable tells the server not to send anything.
        return []