0

A very simple tornado app, when server receive a HTTP get request, it ping -c 2 www.google.com, then return the result. And I want use Tornado. Here is the code from an article.

class AsyncTaskHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        response = yield tornado.gen.Task(self.ping, ' www.google.com')
        print 'response', response
        self.finish('hello')

    @tornado.gen.coroutine
    def ping(self, url):
        os.system("ping -c 2 {}".format(url))
        return 'after'

and the author says that the ab test result is awesome. ab -c 5 -n 5 http://127.0.0.1:5000/async

Document Path:          /async
Document Length:        5 bytes

Concurrency Level:      5
Time taken for tests:   0.009 seconds
Complete requests:      5
Failed requests:        0
Total transferred:      985 bytes
HTML transferred:       25 bytes
Requests per second:    556.92 [#/sec] (mean)
Time per request:       8.978 [ms] (mean)
Time per request:       1.796 [ms] (mean, across all concurrent requests)
Transfer rate:          107.14 [Kbytes/sec] received

but really I use just the same code, and in my test, Requests per second is 0.77! I search for the reason. And I found this version:

class IndexHandler(tornado.web.RequestHandler):
    executor = ThreadPoolExecutor(10)

    @tornado.gen.coroutine
    def get(self):
        print "begin"
        response = yield self.pin()
        print response
        self.finish()

    @run_on_executor
    def pin(self):
        return os.system("ping -c 2 www.google.com")

And the test result, Requests per second is 0.85. I wanna use tornado coroutine to make 1000 or more ping commands no-blocking. How can I code it? Thanks a lot!

2 Answers2

1

Instead of a ThreadPoolExecutor and os.system, it's more efficient to use tornado.process.Subprocess:

class IndexHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def get(self):
        print "begin"
        response = yield self.ping()
        print response
        self.finish()

    @tornado.gen.coroutine
    def ping(self):
        proc = tornado.process.Subprocess("ping -c 2 www.google.com")
        return yield proc.wait_for_exit()

However, since ping is still starting up a separate process, it's not much better than a thread pool in this case, and the thread pool can be useful for limiting the number of concurrent ping processes.

Ben Darnell
  • 21,844
  • 3
  • 29
  • 50
0

The first version of the code runs only one "ping" at a time. The IOLoop is blocked until the os.system call returns.

The second version uses run_on_executor to defer the os.system call to a thread in a threadpool, making it non-blocking and allowing concurrent calls.

If (for some strange reason?) you want to run 1000 concurrent pings, you'll need to expand the ThreadPoolExecutor's default thread count:

thread_pool = ThreadPoolExecutor(1000)

def ping_blocking():
    os.system("ping -c 2 www.google.com")

@gen.coroutine
def ping_many():
    futures = [thread_pool.submit(ping_blocking)
               for _ in range(1000)]
    yield futures

For more info see the Tornado documentation on calling blocking functions. The documentation on parallel execution is on that page too.

A. Jesse Jiryu Davis
  • 23,641
  • 4
  • 57
  • 70
  • So appreciated for your answer. Just few minutes ago, I found another method. That is to realize a function like subprocess.popen, but having a callback param. The code is so complicated. I'm really new to tornado and used to use nodejs. When you use nodejs, you can run as many as concurrent pings until the server reboot. So I wonder maybe tornado can do the same thing. I'll read the documentation. Thanks a lot! – Herbert Kwok May 22 '16 at 12:58