Asynchronous Shell Commands with Tornado
I’ve been playing around with Tornado a bit and wanted to asynchronously call a long-running shell command without blocking the server process. Here is my solution.
Running this server I am able to visit http://localhost:8888/test/ numerous times while a request for http://localhost:8888/ is waiting for the command to finish. The beauty of this is that this is a single process with a single thread. With a process this light and fast, a relatively large number of applications can be crammed into a $10/mo hosted account.
#!/usr/bin/env python
import os
import tornado.httpserver
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.ioloop = tornado.ioloop.IOLoop.instance()
self.pipe = p = os.popen('sleep 5; cat /etc/mime.types')
self.ioloop.add_handler( p.fileno(), self.async_callback(self.on_response), self.ioloop.READ )
def on_response(self,fd,events):
for line in self.pipe:
self.write( line )
self.ioloop.remove_handler(fd)
self.finish()
class TestHandler(tornado.web.RequestHandler):
def get(self):
self.write('this is a test')
application = tornado.web.Application([
(r"/", MainHandler),
(r"/test/", TestHandler),
])
if __name__ == "__main__":
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()
Categories: Programming
asynchronous, python, tornado
Nice solution. (You should maybe change to subprocess.Popen, as os.popen is deprecated in 2.6+)
Yes, I was very disappointed that os.popen was deprecated. subprocess.Popen may be more flexible, but it’s also more complicated. For most cases I used it for os.popen was perfect. Now I just have to write more code every time I use it.
Thanks for this code! I was able to adapt it for use on my site.
Is there an easy way to put a timeout on it? I.e. to run the command, but if it doesn’t complete in 30 seconds then terminate it anyway?
I’m afraid I don’t know the answer to that question.
I’ve noticed that requests to the same handler seem to be handled synchronously. Try loading your MainHandler in three windows at the same time; the requests will finish around the 5, 10, and 15 second marks. I haven’t been able to conquer this problem, and I’m not sure if you know more about it than I do. Any thoughts?
Brian,
I’m quite certain this was working correctly at one point. But I did just try it again and it is doing what you said. Let me know if you figure out what’s happening.
I’ve reproduced the problem using multiprocessing.Process & Pipe intead of popen as well as using a completed form of the example given on the official Tornado documentation:
http://www.tornadoweb.org/documentation#non-blocking-asynchronous-requests
I’ve been asking questions on the Tornado mailing list lately, though this recent discovery has been unanswered so far.
Make sure you have epoll working. I did at one time, but it may have broken since. Check here:
https://github.com/facebook/tornado/blob/master/tornado/ioloop.py#L36
It was the browser! Try using two different browsers or two instances of cURL.
This was answered by Ben Darnell in my e-mail to the Tornado mailing list:
http://groups.google.com/group/python-tornado/browse_thread/thread/fa695c938d5092dc
Brian,
Thanks for following up on that.
This implementation worked for me using the “subprocess” module:
—————————————————————–
@tornado.web.asynchronous
def post(self):
#…
#… get your inputs… like self.get_argument(‘whatever blah blah’)
#…
#spawn process, feed it, and then move on
self.ioloop = tornado.ioloop.IOLoop.instance()
self.sp = subprocess.Popen([command,args,pamplemousse],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
#my application has a lot of input, too much for the command line
#so we feed in the data via stdin. You may not need to do this part,
#depending on your application
self.sp.stdin.write(‘here is my input la la la’)
#make the callback!
self.ioloop.add_handler(self.sp.stdout.fileno(),
self.async_callback(self.on_response),
self.ioloop.READ)
def on_response(self):
response = self.sp.stdout.read()
self.write(response)
self.ioloop.remove_handler(fd)
self.finish()
I’ve created an updated version of a previous gist which can be found here: https://gist.github.com/1370533 and includes an example using the gen.engine yield way of doing things.
Small explanation on my personal blog here: http://cerealkillers.co.uk/2011/async-shell-commands-from-tornado/