Search code examples
pythonasynchronoustornado

Correct use of coroutine in Tornado web server


I'm trying to convert a simple syncronous server to an asyncronous version, the server receives post requestes and it retrieves the response from an external web service (amazon sqs). Here's the syncronous code

def post(self):

    zoom_level = self.get_argument('zoom_level')
    neLat = self.get_argument('neLat')
    neLon = self.get_argument('neLon')
    swLat = self.get_argument('swLat')
    swLon = self.get_argument('swLon')
    data = self._create_request_message(zoom_level, neLat, neLon, swLat, swLon)

    self._send_parking_spots_request(data)

    #....other stuff

def _send_parking_spots_request(self, data):

    msg = Message()
    msg.set_body(json.dumps(data))
    self._sqs_send_queue.write(msg)

Reading Tornado documentation and some threads here I ended with this code using coroutines:

def post(self):

    zoom_level = self.get_argument('zoom_level')
    neLat = self.get_argument('neLat')
    neLon = self.get_argument('neLon')
    swLat = self.get_argument('swLat')
    swLon = self.get_argument('swLon')
    data = self._create_request_message(zoom_level, neLat, neLon, swLat, swLon)
    self._send_parking_spots_request(data)
    self.finish()

@gen.coroutine
def _send_parking_spots_request(self, data):

    msg = Message()
    msg.set_body(json.dumps(data))
    yield gen.Task(write_msg, self._sqs_send_queue, msg)

def write_msg(queue, msg, callback=None):
    queue.write(msg)

Comparing the performances using siege I get that the second version is even worse than the original one, so probably there's something about coroutines and Torndado asyncronous programming that I didn't understand at all. Could you please help me with this?

Edit: self._sqs_send_queue it's a queue object retrieved from boto interface and queue.write(msg) returns the message that has been written on the queue


Solution

  • tornado relies on you converting all your I/O to be non-blocking. Simply sticking the same code you were using before inside of a gen.Task will not improve performance at all, because the I/O itself is still going to block the event loop. Additionally, you need to make your post method a coroutine, and call _send_parking_spots_requests using yield for the code to behave properly. So, a "correct" solution would look something like this:

    @gen.coroutine
    def post(self):
        ...
        yield self._send_parking_spots_request(data)  # wait (without blocking the event loop) until the method is done
        self.finish()
    
    @gen.coroutine
    def _send_parking_spots_request(self, data):
    
        msg = Message()
        msg.set_body(json.dumps(data))
        yield gen.Task(write_msg, self._sqs_send_queue, msg)
    
    def write_msg(queue, msg, callback=None):
        yield queue.write(msg, callback=callback)  # This has to do non-blocking I/O.
    

    In this example, queue.write would need to be some API that sends your request using non-blocking I/O, and executes callback when a response is received. Without knowing exactly what queue in your original example is, I can't specify exactly how that can be implemented in your case.

    Edit: Assuming you're using boto, you may want to check out bototornado, which implements the exact same API I described above:

    def write(self, message, callback=None):
        """
        Add a single message to the queue.
        :type message: Message
        :param message: The message to be written to the queue
        :rtype: :class:`boto.sqs.message.Message`
        :return: The :class:`boto.sqs.message.Message` object that was written.