Search code examples
pythontornado

Tornado 4.x solution of running game on ThreadPoolExecutor not working anymore. Need help refactoring it


My ThreadPoolExecutor/gen.coroutine(tornado v4.x) solution to circumvent blocking the webserver is not working anymore with tornado version 6.x.

A while back I started to develop an online Browser game using a Tornado webserver(v4.x) and websockets. Whenever user input is expected, the game would send the question to the client and wait for the response. Back than i used gen.coroutine and a ThreadPoolExecutor to make this task non-blocking. Now that I started refactoring the game, it is not working with tornado v6.x and the task is blocking the server again. I searched for possible solutions, but so far i have been unable to get it working again. It is not clear to me how to change my existing code to be non-blocking again.

server.py:

class PlayerWebSocket(tornado.websocket.WebSocketHandler):
    executor = ThreadPoolExecutor(max_workers=15)
    @run_on_executor
    def on_message(self,message):
        params = message.split(':')
        self.player.callbacks[int(params[0])]=params[1]

if __name__ == '__main__':
    application = Application()
    application.listen(9999)
    tornado.ioloop.IOLoop.instance().start()

player.py:

@gen.coroutine
def send(self, message):
   self.socket.write_message(message)

def create_choice(self, id, choices):
    d = {}
    d['id'] = id
    d['choices']=choices
    self.choice[d['id']]=d
    self.send('update',self)
    while not d['id'] in self.callbacks:
        pass
    del self.choice[d['id']]
    return self.callbacks[d['id']]

Whenever a choice is to be made, the create_choice function creates a dict with a list (choices) and an id and stores it in the players self.callbacks. After that it just stays in the while loop until the websocket.on_message function puts the received answer (which looks like this: id:Choice_id, so for example 1:12838732) into the callbacks dict.


Solution

  • The WebSocketHandler.write_message method is not thread-safe, so it can only be called from the IOLoop's thread, and not from a ThreadPoolExecutor (This has always been true, but sometimes it might have seemed to work anyway).

    The simplest way to fix this code is to save IOLoop.current() in a global variable from the main thread (the current() function accesses a thread-local variable so you can't call it from the thread pool) and use ioloop.add_callback(self.socket.write_message, message) (and remove @gen.coroutine from send - it doesn't do any good to make functions coroutines if they contain no yield expressions).