Search code examples
tornadononblockingcoroutinerequesthandlerconcurrent.futures

Can a Tornado RequestHandler attend requests, while waiting for a Future to finish?


Can a single Tornado RequestHandler class attend new requests, while waiting for a Future to finish in one of its instances?

I was debugging a Tornado coroutine that called a ThreadPoolExecutor and I noticed that, while the coroutine was waiting for the executor to finish, the RequestHandler was blocked. So any new requests to this handler where waiting for the coroutine to finish.

Here is the code I wrote to reproduce my observation:

from time import sleep
from concurrent.futures import ThreadPoolExecutor
from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.web import Application, RequestHandler
from tornado.gen import coroutine

class Handler1(RequestHandler):
    @coroutine
    def get(self):
        print('Setting up executor ...')
        thread_pool = ThreadPoolExecutor(1)

        print('Yielding ...')
        yield thread_pool.submit(sleep, 30)

        self.write('Ready!')
        print('Finished!')

app = Application([('/1$', Handler1)])
app.listen(8888)
PeriodicCallback(lambda: print('##'), 10000).start()
IOLoop.instance().start()

Now, if I access localhost:8888/1 twice I get the following output:

##
Setting up executor ...
Yielding ...
##
##
##
Finished!
Setting up executor ...
Yielding ...
##
##
##
Finished!
##

But I would expect the following to occur:

##
Setting up executor ...
Yielding ...
Setting up executor ...
Yielding ...
##
##
##
Finished!
Finished!
##

Notice that only the RequestHandler seems to be blocked, because we still get the ## every 10 seconds. In fact if you add another identical RequestHandler (Handler2) and you access localhost:8888/1 and localhost:8888/2, this will produce the expected output.

Is this normal? Is this the intended behaviour?

Sorry for my bad English.


Solution

  • Tornado creates a new instance of your RequestHandler for each new request. So your code indeed behaves as you expect. I run it and open two terminal windows, each running wget localhost:8888/1. Your code prints:

    Setting up executor ...
    Yielding ...
    Setting up executor ...
    Yielding ...
    ##
    ##
    ##
    Finished!
    Finished!
    

    As you expect. What you probably see is that your browser is unwilling to open two connections to the same URL at once. Indeed, I can reproduce the "blocking" behavior you see with chrome, if I open two tabs and try to load "localhost:8888/1" in both. However, if I modify your code:

    app = Application([
        ('/1$', Handler1),
        ('/2$', Handler1)])
    

    And open "localhost:8888/1" and "localhost:8888/2" in two tabs in Chrome, I see that it opens both connections concurrently.

    Try wget to test without interference from the browser.