├── README.rst └── src ├── .gitignore ├── gtornado ├── __init__.py └── monkey.py ├── thelloworld.py └── tsockhttp.py /README.rst: -------------------------------------------------------------------------------- 1 | gtornado = gevent + Tornado 2 | =========================== 3 | 4 | 5 | Introduction 6 | ------------ 7 | 8 | Tornado_ is a high performance web server and framework. It operates in a non-blocking fashion, 9 | utilizing Linux's epoll_ facility when available. It also comes bundled with several niceties 10 | such as authentication via OpenID, OAuth, secure cookies, templates, CSRF protection and UI modules. 11 | 12 | Unfortunately, some of its features ties the developer into its own asynchronous API implementation. 13 | 14 | This module is an experiment to monkey patch it just enough to make it run under gevent_. 15 | One advantage of doing so is that one can use a coroutine-style and code in a blocking fashion 16 | while being able to use the tornado framework. For example, one could use Tornado's OpenID mixins, together with 17 | other libraries (perhaps AMQP or XMPP clients?) that may not otherwise be written to Tornado's asynchronous API and therefore would block the entire process. 18 | 19 | .. _Tornado: http://www.tornadoweb.org/ 20 | .. _epoll: http://www.kernel.org/doc/man-pages/online/pages/man4/epoll.4.html 21 | .. _gevent: http://www.gevent.org/ 22 | 23 | 24 | Monkey Patching 25 | --------------- 26 | 27 | gtornado currently includes patches to two different Tornado modules: ``ioloop`` and ``httpserver``. 28 | 29 | The ``ioloop`` patch uses gevent's internal pyevent implementation, mapping ``ioloop``'s concepts 30 | into libevent's. 31 | 32 | The ``httpserver`` patch uses gevent's libevent_http wrapper, which *should* be blazing fast. 33 | However, due to the way tornado's httpserver is structured, the monkey patching code has to do some, 34 | well, monkeying around (parsing the headers from tornado and translating them into libevent_http calls.) 35 | It tries to be fairly efficient, but if your application is doesn't do much (most benchmarks), 36 | the parsing overhead can be a significant chunk of your CPU time. 37 | 38 | There are two ways to monkey patch your tornado application: 39 | 40 | - by importing the ``gtornado.monkey`` module and calling the ``patch_*`` functions in your tornado application source before importing any tornado modules. 41 | 42 | :: 43 | 44 | from gtornado.monkey import patch_all; patch_all() 45 | # now import your usual stuff 46 | from tornado import ioloop 47 | 48 | - by running the gtornado.monkey module as a script, to let it patch tornado before running your tornado application. 49 | 50 | :: 51 | 52 | $ python -m gtornado.monkey my_tornado_app.py 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /src/gtornado/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wil/gtornado/4774fd4bdcef05e0490f8940092e03b91e98948c/src/gtornado/__init__.py -------------------------------------------------------------------------------- /src/gtornado/monkey.py: -------------------------------------------------------------------------------- 1 | import time 2 | import cgi 3 | import gevent 4 | import gevent.hub 5 | import gevent.http 6 | 7 | def patch_ioloop(): 8 | _tornado_iol = __import__('tornado.ioloop', fromlist=['fromlist_has_to_be_non_empty']) 9 | _IOLoop = _tornado_iol.IOLoop 10 | 11 | class IOLoop: 12 | READ = _IOLoop.READ 13 | WRITE = _IOLoop.WRITE 14 | ERROR = _IOLoop.ERROR 15 | 16 | def __init__(self): 17 | self._handlers = {} # by fd 18 | self._events = {} # by fd 19 | 20 | def start(self): 21 | gevent.hub.get_hub().switch() 22 | 23 | def stop(self): 24 | for e,fd in list(self._events.iteritems()): 25 | self.remove_handler(e) 26 | 27 | gevent.hub.shutdown() 28 | 29 | def remove_handler(self, fd): 30 | self._handlers.pop(fd, None) 31 | ev = self._events.pop(fd, None) 32 | ev.cancel() 33 | 34 | def update_handler(self, fd, events): 35 | handler = self._handlers.pop(fd, None) 36 | self.remove_handler(fd) 37 | self.add_handler(fd, handler, events) 38 | 39 | def add_handler(self, fd, handler, events): 40 | type = gevent.core.EV_PERSIST 41 | if events & _IOLoop.READ: 42 | type = type | gevent.core.EV_READ 43 | if events & _IOLoop.WRITE: 44 | type = type | gevent.core.EV_WRITE 45 | if events & _IOLoop.ERROR: 46 | type = type | gevent.core.EV_READ 47 | 48 | def callback(ev, type): 49 | #print "ev=%r type=%r firing" % (ev, type) 50 | tornado_events = 0 51 | if type & gevent.core.EV_READ: 52 | tornado_events |= _IOLoop.READ 53 | if type & gevent.core.EV_WRITE: 54 | tornado_events |= _IOLoop.WRITE 55 | if type & gevent.core.EV_SIGNAL: 56 | tornado_events |= _IOLoop.ERROR 57 | return handler(ev.fd, tornado_events) 58 | 59 | #print "add_handler(fd=%r, handler=%r, events=%r)" % (fd, handler, events) 60 | #print "type => %r" % type 61 | e = gevent.core.event(type, fd, callback) 62 | e.add() 63 | self._events[fd] = e 64 | self._handlers[fd] = handler 65 | 66 | 67 | def add_callback(self, callback): 68 | print "adding callback" 69 | gevent.spawn(callback) 70 | 71 | def add_timeout(self, deadline, callback): 72 | print "adding callback" 73 | gevent.spawn_later(int(deadline - time.time()), callback) 74 | 75 | @classmethod 76 | def instance(cls): 77 | if not hasattr(cls, "_instance"): 78 | print "new instance?" 79 | cls._instance = cls() 80 | return cls._instance 81 | 82 | #print "orig ioloop = ", dir(_tornado_iol) 83 | _tornado_iol.IOLoop = IOLoop 84 | #print "iol = ", id(_tornado_iol.IOLoop) 85 | 86 | 87 | 88 | def patch_httpserver(): 89 | from tornado.httpserver import HTTPRequest 90 | 91 | def parse_t_http_output(buf): 92 | headers, body = buf.split("\r\n\r\n", 1) 93 | headers = headers.split("\r\n") 94 | ver, code, msg = headers[0].split(" ", 2) 95 | code = int(code) 96 | chunked = False 97 | 98 | headers_out = [] 99 | for h in headers[1:]: 100 | k, v = h.split(":", 1) 101 | if k == "Transfer-Encoding" and v == "chunked": 102 | chunked = True 103 | headers_out.append((k, v.lstrip())) 104 | 105 | return code, msg, headers_out, body, chunked 106 | 107 | def parse_post_body(req, body): 108 | content_type = req.headers.get("Content-Type", "") 109 | if req.method == "POST": 110 | if content_type.startswith("application/x-www-form-urlencoded"): 111 | arguments = cgi.parse_qs(req.body) 112 | for name, values in arguments.iteritems(): 113 | values = [v for v in values if v] 114 | if values: 115 | req.arguments.setdefault(name, []).extend(values) 116 | elif content_type.startswith("multipart/form-data"): 117 | boundary = content_type[30:] 118 | if boundary: 119 | self._parse_mime_body(boundary, data) 120 | # from HTTPConnection._parse_mime_body 121 | if data.endswith("\r\n"): 122 | footer_length = len(boundary) + 6 123 | else: 124 | footer_length = len(boundary) + 4 125 | parts = data[:-footer_length].split("--" + boundary + "\r\n") 126 | for part in parts: 127 | if not part: continue 128 | eoh = part.find("\r\n\r\n") 129 | if eoh == -1: 130 | logging.warning("multipart/form-data missing headers") 131 | continue 132 | headers = HTTPHeaders.parse(part[:eoh]) 133 | name_header = headers.get("Content-Disposition", "") 134 | if not name_header.startswith("form-data;") or \ 135 | not part.endswith("\r\n"): 136 | logging.warning("Invalid multipart/form-data") 137 | continue 138 | value = part[eoh + 4:-2] 139 | 140 | 141 | name_values = {} 142 | for name_part in name_header[10:].split(";"): 143 | name, name_value = name_part.strip().split("=", 1) 144 | name_values[name] = name_value.strip('"').decode("utf-8") 145 | if not name_values.get("name"): 146 | logging.warning("multipart/form-data value missing name") 147 | continue 148 | name = name_values["name"] 149 | if name_values.get("filename"): 150 | ctype = headers.get("Content-Type", "application/unknown") 151 | req.files.setdefault(name, []).append(dict( 152 | filename=name_values["filename"], body=value, 153 | content_type=ctype)) 154 | else: 155 | req.arguments.setdefault(name, []).append(value) 156 | 157 | 158 | class FakeStream(): 159 | def __init__(self): 160 | self._closed = False 161 | 162 | def closed(self): 163 | print "stream closed = ", self._closed 164 | return self._closed 165 | 166 | 167 | class FakeConnection(): 168 | def __init__(self, r): 169 | self._r = r 170 | self.xheaders = False 171 | self.reply_started = False 172 | self.stream = FakeStream() 173 | #r.connection.set_closecb(self) 174 | 175 | def _cb_connection_close(self, conn): 176 | print "connection %r closed!!!!" % (conn,) 177 | print "stream = %r" % self.stream 178 | self.stream._closed = True 179 | print "flagged stream as closed" 180 | 181 | def write(self, chunk): 182 | if not self.reply_started: 183 | #print "starting reply..." 184 | # need to parse the first line as RequestHandler actually writes the response line 185 | code, msg, headers, body, chunked = parse_t_http_output(chunk) 186 | 187 | for k, v in headers: 188 | #print "header[%s] = %s" % (k, v) 189 | self._r.add_output_header(k, v) 190 | 191 | if chunked: 192 | self._r.send_reply_start(code, msg) 193 | self._r.send_reply_chunk(body) 194 | else: 195 | self._r.send_reply(code, msg, body) 196 | self.reply_started = True 197 | else: 198 | print "writing %s" % chunk 199 | self._r.send_reply_chunk(chunk) 200 | 201 | def finish(self): 202 | self._r.send_reply_end() 203 | 204 | 205 | class GHttpServer: 206 | def __init__(self, t_app): 207 | def debug_http_cb(r): 208 | print "http request = ", r 209 | for m in dir(r): 210 | o = eval("r." + m) 211 | if type(o) in (str, list, int, tuple): 212 | print "r.%s = %r" % (m, o) 213 | r.add_output_header("X-Awesomeness", "100%") 214 | r.send_reply(200, "OK", 'hello') 215 | 216 | 217 | def http_cb(r): 218 | body = r.input_buffer.read() 219 | treq = HTTPRequest( 220 | r.typestr, # method 221 | r.uri, # uri 222 | headers=dict(r.get_input_headers()), # need to transform from list of tuples to dict 223 | body=body, 224 | remote_ip=r.remote_host, 225 | protocol="http", # or https 226 | host=None, # 127.0.0.1? 227 | files=None, # ?? 228 | connection=FakeConnection(r)) 229 | 230 | parse_post_body(treq, body) 231 | 232 | """ 233 | print "http request = ", r 234 | for m in dir(r): 235 | o = eval("r." + m) 236 | if type(o) in (str, list, int, tuple): 237 | print "r.%s = %r" % (m, o) 238 | """ 239 | t_app(treq) 240 | 241 | self._httpserver = gevent.http.HTTPServer(http_cb) 242 | 243 | def listen(self, port): 244 | self._httpserver.serve_forever(('0.0.0.0', port), backlog=128) 245 | 246 | @classmethod 247 | def instance(cls): 248 | if not hasattr(cls, "_instance"): 249 | cls._instance = cls() 250 | return cls._instance 251 | 252 | _httpserver = __import__('tornado.httpserver', fromlist=['fromlist_has_to_be_non_empty']) 253 | _httpserver.HTTPServer = GHttpServer 254 | 255 | 256 | def patch_all(ioloop=True, httpserver=True): 257 | if ioloop: 258 | print "Patching ioloop" 259 | patch_ioloop() 260 | 261 | if httpserver: 262 | print "Patching httpserver" 263 | patch_httpserver() 264 | 265 | 266 | # code below shamelessly stolen from gevent.monkey 267 | if __name__=='__main__': 268 | import sys 269 | modules = [x.replace('patch_', '') for x in globals().keys() if x.startswith('patch_') and x!='patch_all'] 270 | script_help = """gtornado.monkey - monkey patch the tornado modules to use gevent. 271 | 272 | USAGE: python -m gtornado.monkey [MONKEY OPTIONS] script [SCRIPT OPTIONS] 273 | 274 | If no OPTIONS present, monkey patches all the modules it can patch. 275 | You can exclude a module with --no-module, e.g. --no-thread. You can 276 | specify a module to patch with --module, e.g. --socket. In the latter 277 | case only the modules specified on the command line will be patched. 278 | 279 | MONKEY OPTIONS: --verbose %s""" % ', '.join('--[no-]%s' % m for m in modules) 280 | args = {} 281 | argv = sys.argv[1:] 282 | verbose = False 283 | default_yesno = True 284 | while argv and argv[0].startswith('--'): 285 | option = argv[0][2:] 286 | if option == 'verbose': 287 | verbose = True 288 | elif option.startswith('no-') and option.replace('no-', '') in modules: 289 | args[option[3:]] = False 290 | elif option in modules: 291 | args[option] = True 292 | default_yesno = False 293 | else: 294 | sys.exit(script_help + '\n\n' + 'Cannot patch %r' % option) 295 | del argv[0] 296 | 297 | for m in modules: 298 | if args and m not in args: 299 | args[m] = default_yesno 300 | 301 | if verbose: 302 | import pprint, os 303 | print 'gtornado.monkey.patch_all(%s)' % ', '.join('%s=%s' % item for item in args.items()) 304 | print 'sys.version=%s' % (sys.version.strip().replace('\n', ' '), ) 305 | print 'sys.path=%s' % pprint.pformat(sys.path) 306 | print 'sys.modules=%s' % pprint.pformat(sorted(sys.modules.keys())) 307 | print 'cwd=%s' % os.getcwd() 308 | 309 | patch_all(**args) 310 | if argv: 311 | sys.argv = argv 312 | __package__ = None 313 | execfile(sys.argv[0]) 314 | else: 315 | print script_help 316 | 317 | -------------------------------------------------------------------------------- /src/thelloworld.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2009 Facebook 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import tornado.httpserver 18 | import tornado.ioloop 19 | import tornado.options 20 | import tornado.web 21 | 22 | from tornado.options import define, options 23 | 24 | define("port", default=8888, help="run on the given port", type=int) 25 | 26 | 27 | class MainHandler(tornado.web.RequestHandler): 28 | def get(self): 29 | self.write("Hello, world") 30 | 31 | 32 | def main(): 33 | tornado.options.parse_command_line() 34 | application = tornado.web.Application([ 35 | (r"/", MainHandler), 36 | ]) 37 | http_server = tornado.httpserver.HTTPServer(application) 38 | http_server.listen(options.port) 39 | tornado.ioloop.IOLoop.instance().start() 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /src/tsockhttp.py: -------------------------------------------------------------------------------- 1 | # taken from http://nichol.as/asynchronous-servers-in-python 2 | 3 | import errno 4 | import functools 5 | import socket 6 | from tornado import ioloop, iostream 7 | 8 | 9 | def connection_ready(sock, fd, events): 10 | while True: 11 | try: 12 | connection, address = sock.accept() 13 | except socket.error, e: 14 | if e[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): 15 | raise 16 | return 17 | connection.setblocking(0) 18 | stream = iostream.IOStream(connection) 19 | stream.write("HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nPong!\r\n", stream.close) 20 | 21 | if __name__ == '__main__': 22 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 23 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 24 | sock.setblocking(0) 25 | sock.bind(("", 8010)) 26 | sock.listen(5000) 27 | 28 | io_loop = ioloop.IOLoop.instance() 29 | callback = functools.partial(connection_ready, sock) 30 | io_loop.add_handler(sock.fileno(), callback, io_loop.READ) 31 | try: 32 | io_loop.start() 33 | except KeyboardInterrupt: 34 | io_loop.stop() 35 | print "exited cleanly" 36 | --------------------------------------------------------------------------------