├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── examples ├── echo-server.py └── hello-world.py ├── requirements.txt ├── setup.py ├── tornaduv └── __init__.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | MANIFEST 3 | .tox/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: 2.7 6 | env: TOX_ENV=py27 7 | - python: 2.7 8 | env: TOX_ENV=py27-deps 9 | - python: 2.7 10 | env: TOX_ENV=py27-t3 11 | 12 | - python: 3.3 13 | env: TOX_ENV=py33 14 | - python: 3.3 15 | env: TOX_ENV=py33-deps 16 | - python: 3.3 17 | env: TOX_ENV=py33-t3 18 | 19 | - python: 3.4 20 | env: TOX_ENV=py34 21 | - python: 3.4 22 | env: TOX_ENV=py34-deps 23 | - python: 3.4 24 | env: TOX_ENV=py34-t3 25 | 26 | - python: 3.5 27 | env: TOX_ENV=py35 28 | - python: 3.5 29 | env: TOX_ENV=py35-deps 30 | 31 | - python: 3.6 32 | env: TOX_ENV=py36 33 | - python: 3.6 34 | env: TOX_ENV=py36-deps 35 | 36 | 37 | sudo: false 38 | 39 | install: 40 | - pip install tox 41 | 42 | script: 43 | - tox -e $TOX_ENV 44 | 45 | notifications: 46 | email: 47 | on_success: never 48 | on_failure: change 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Saúl Ibarra Corretgé 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE TODO requirements.txt 2 | recursive-include examples * 3 | recursive-exclude * __pycache__ 4 | recursive-exclude * *.py[co] 5 | 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | A Tornado IOLoop based on pyuv 3 | ============================== 4 | 5 | .. image:: https://travis-ci.org/saghul/tornaduv.svg?branch=master 6 | :target: https://travis-ci.org/saghul/tornaduv 7 | :alt: Build status 8 | 9 | .. image:: https://pypip.in/download/tornaduv/badge.png 10 | :target: https://pypi.python.org/pypi/tornaduv/ 11 | :alt: Downloads 12 | 13 | .. image:: https://pypip.in/version/tornaduv/badge.png 14 | :target: https://pypi.python.org/pypi/tornaduv/ 15 | :alt: Latest Version 16 | 17 | .. image:: https://pypip.in/license/tornaduv/badge.png 18 | :target: https://pypi.python.org/pypi/tornaduv/ 19 | :alt: License 20 | 21 | 22 | tornaduv is a `Tornado `_ IOLoop implementation 23 | which uses `pyuv `_ as the networking library instead 24 | of the builtin epoll and kqueue pollers included in Tornado. 25 | 26 | pyuv is a Python interface for libuv, a high performance asynchronous 27 | networking library used as the platform layer for NodeJS. 28 | 29 | 30 | Installation 31 | ============ 32 | 33 | tornaduv requires pyuv >= 1.0.0 and Tornado >= 3.0. 34 | 35 | :: 36 | 37 | pip install tornaduv 38 | 39 | 40 | Using it 41 | ======== 42 | 43 | In order to use tornaduv, Tornado needs to be instructed to use 44 | our IOLoop. In order to do that add the following lines at the beginning 45 | of your project: 46 | 47 | :: 48 | 49 | from tornado.ioloop import IOLoop 50 | from tornaduv import UVLoop 51 | IOLoop.configure(UVLoop) 52 | 53 | 54 | Testing 55 | ======= 56 | 57 | If you want to run the Tornado test suite using tornaduv run the following command: 58 | 59 | :: 60 | 61 | python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' --verbose 62 | 63 | 64 | Authors 65 | ======= 66 | 67 | Saúl Ibarra Corretgé 68 | Marc Schlaich 69 | 70 | 71 | License 72 | ======= 73 | 74 | tornaduv uses the MIT license, check LICENSE file. 75 | 76 | -------------------------------------------------------------------------------- /examples/echo-server.py: -------------------------------------------------------------------------------- 1 | 2 | import signal 3 | 4 | from tornado.ioloop import IOLoop 5 | from tornado.tcpserver import TCPServer 6 | 7 | from tornaduv import UVLoop 8 | IOLoop.configure(UVLoop) 9 | 10 | 11 | def handle_signal(sig, frame): 12 | IOLoop.instance().add_callback(IOLoop.instance().stop) 13 | 14 | 15 | class EchoServer(TCPServer): 16 | 17 | def handle_stream(self, stream, address): 18 | self._stream = stream 19 | self._read_line() 20 | 21 | def _read_line(self): 22 | self._stream.read_until('\n', self._handle_read) 23 | 24 | def _handle_read(self, data): 25 | self._stream.write(data) 26 | self._read_line() 27 | 28 | 29 | if __name__ == '__main__': 30 | signal.signal(signal.SIGINT, handle_signal) 31 | signal.signal(signal.SIGTERM, handle_signal) 32 | server = EchoServer() 33 | server.listen(8889) 34 | IOLoop.instance().start() 35 | IOLoop.instance().close() 36 | 37 | -------------------------------------------------------------------------------- /examples/hello-world.py: -------------------------------------------------------------------------------- 1 | 2 | import signal 3 | 4 | from tornado.ioloop import IOLoop 5 | from tornado.web import Application, RequestHandler 6 | 7 | from tornaduv import UVLoop 8 | IOLoop.configure(UVLoop) 9 | 10 | 11 | def handle_signal(sig, frame): 12 | loop = IOLoop.instance() 13 | loop.add_callback(loop.stop) 14 | 15 | class MainHandler(RequestHandler): 16 | def get(self): 17 | self.write("Hello, world") 18 | 19 | application = Application([ 20 | (r"/", MainHandler), 21 | ]) 22 | 23 | 24 | if __name__ == "__main__": 25 | signal.signal(signal.SIGINT, handle_signal) 26 | signal.signal(signal.SIGTERM, handle_signal) 27 | application.listen(8080) 28 | loop = IOLoop.instance() 29 | loop.start() 30 | loop.close() 31 | 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Tornado>=3.0 2 | pyuv>=1.0.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from distutils.core import setup 5 | 6 | 7 | def get_version(): 8 | return re.search(r"""__version__\s+=\s+(?P['"])(?P.+?)(?P=quote)""", open('tornaduv/__init__.py').read()).group('version') 9 | 10 | setup( 11 | name = 'tornaduv', 12 | version = get_version(), 13 | url = 'https://github.com/saghul/tornaduv', 14 | author = 'Saúl Ibarra Corretgé', 15 | author_email = 'saghul@gmail.com', 16 | license = 'MIT License', 17 | description = 'Tornado IOLoop implementation with pyuv', 18 | long_description = open('README.rst', 'r').read(), 19 | packages = ['tornaduv'], 20 | install_requires = [i.strip() for i in open("requirements.txt").readlines() if i.strip()], 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: POSIX', 26 | 'Operating System :: Microsoft :: Windows', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.3', 32 | 'Programming Language :: Python :: 3.4' 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /tornaduv/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import pyuv 3 | 4 | import datetime 5 | import errno 6 | import functools 7 | import logging 8 | import numbers 9 | import os 10 | 11 | try: 12 | import thread 13 | except ImportError: 14 | import _thread as thread # Python 3 15 | 16 | try: 17 | import signal 18 | except ImportError: 19 | signal = None 20 | 21 | from tornado import stack_context 22 | from tornado.ioloop import IOLoop 23 | from tornado.log import gen_log 24 | from tornado.platform.auto import Waker as FDWaker 25 | 26 | __all__ = ('UVLoop') 27 | 28 | __version__ = '0.4.0' 29 | 30 | 31 | class Waker(object): 32 | def __init__(self, loop): 33 | self._async = pyuv.Async(loop, lambda x: None) 34 | 35 | def wake(self): 36 | self._async.send() 37 | 38 | 39 | class UVLoop(IOLoop): 40 | 41 | def initialize(self, **kwargs): 42 | loop = kwargs.pop('loop', None) 43 | self._loop = loop or pyuv.Loop() 44 | self._handlers = {} 45 | self._callbacks = [] 46 | self._callback_lock = thread.allocate_lock() 47 | self._timeouts = set() 48 | self._stopped = False 49 | self._running = False 50 | self._closing = False 51 | self._thread_ident = None 52 | self._cb_handle = pyuv.Prepare(self._loop) 53 | self._cb_handle.start(self._prepare_cb) 54 | self._waker = Waker(self._loop) 55 | self._fdwaker = FDWaker() 56 | self._signal_checker = pyuv.util.SignalChecker( 57 | self._loop, self._fdwaker.reader.fileno()) 58 | super(UVLoop, self).initialize(**kwargs) 59 | 60 | def close(self, all_fds=False): 61 | with self._callback_lock: 62 | self._closing = True 63 | if all_fds: 64 | for fd in self._handlers: 65 | obj, _ = self._handlers[fd] 66 | if obj is not None and hasattr(obj, 'close'): 67 | try: 68 | obj.close() 69 | except Exception: 70 | gen_log.debug("error closing socket object %s", obj, 71 | exc_info=True) 72 | try: 73 | os.close(fd) 74 | except Exception: 75 | gen_log.debug("error closing fd %s", fd, exc_info=True) 76 | 77 | self._fdwaker.close() 78 | self._close_loop_handles() 79 | # Run the loop so the close callbacks are fired and memory is freed 80 | self._loop.run() 81 | self._loop = None 82 | 83 | def add_handler(self, fd, handler, events): 84 | obj = None 85 | if hasattr(self, 'split_fd'): 86 | fd, obj = self.split_fd(fd) 87 | if fd in self._handlers: 88 | raise IOError("fd %d already registered" % fd) 89 | poll = pyuv.Poll(self._loop, fd) 90 | poll.handler = stack_context.wrap(handler) 91 | self._handlers[fd] = (obj, poll) 92 | poll_events = 0 93 | if events & IOLoop.READ: 94 | poll_events |= pyuv.UV_READABLE 95 | if events & IOLoop.WRITE: 96 | poll_events |= pyuv.UV_WRITABLE 97 | poll.start(poll_events, self._handle_poll_events) 98 | 99 | def update_handler(self, fd, events): 100 | if hasattr(self, 'split_fd'): 101 | fd, _ = self.split_fd(fd) 102 | _, poll = self._handlers[fd] 103 | poll_events = 0 104 | if events & IOLoop.READ: 105 | poll_events |= pyuv.UV_READABLE 106 | if events & IOLoop.WRITE: 107 | poll_events |= pyuv.UV_WRITABLE 108 | poll.start(poll_events, self._handle_poll_events) 109 | 110 | def remove_handler(self, fd): 111 | if hasattr(self, 'split_fd'): 112 | fd, _ = self.split_fd(fd) 113 | data = self._handlers.pop(fd, None) 114 | if data is not None: 115 | _, poll = data 116 | poll.close() 117 | poll.handler = None 118 | 119 | def start(self): 120 | if self._running: 121 | raise RuntimeError('IOLoop is already running') 122 | if not logging.getLogger().handlers: 123 | # The IOLoop catches and logs exceptions, so it's 124 | # important that log output be visible. However, python's 125 | # default behavior for non-root loggers (prior to python 126 | # 3.2) is to print an unhelpful "no handlers could be 127 | # found" message rather than the actual log entry, so we 128 | # must explicitly configure logging if we've made it this 129 | # far without anything. 130 | logging.basicConfig() 131 | if self._stopped: 132 | self._stopped = False 133 | return 134 | old_current = getattr(IOLoop._current, "instance", None) 135 | IOLoop._current.instance = self 136 | self._thread_ident = thread.get_ident() 137 | 138 | # pyuv won't interate the loop if the poll is interrupted by 139 | # a signal, so make sure we can wake it up to catch signals 140 | # registered with the signal module 141 | # 142 | # If someone has already set a wakeup fd, we don't want to 143 | # disturb it. This is an issue for twisted, which does its 144 | # SIGCHILD processing in response to its own wakeup fd being 145 | # written to. As long as the wakeup fd is registered on the IOLoop, 146 | # the loop will still wake up and everything should work. 147 | old_wakeup_fd = None 148 | self._signal_checker.stop() 149 | if hasattr(signal, 'set_wakeup_fd') and os.name == 'posix': 150 | # requires python 2.6+, unix. set_wakeup_fd exists but crashes 151 | # the python process on windows. 152 | try: 153 | old_wakeup_fd = signal.set_wakeup_fd(self._fdwaker.writer.fileno()) 154 | if old_wakeup_fd != -1: 155 | # Already set, restore previous value. This is a little racy, 156 | # but there's no clean get_wakeup_fd and in real use the 157 | # IOLoop is just started once at the beginning. 158 | signal.set_wakeup_fd(old_wakeup_fd) 159 | old_wakeup_fd = None 160 | else: 161 | self._signal_checker.start() 162 | except ValueError: # non-main thread 163 | pass 164 | 165 | self._running = True 166 | self._loop.run(pyuv.UV_RUN_DEFAULT) 167 | 168 | # reset the stopped flag so another start/stop pair can be issued 169 | self._running = False 170 | self._stopped = False 171 | IOLoop._current.instance = old_current 172 | if old_wakeup_fd is not None: 173 | signal.set_wakeup_fd(old_wakeup_fd) 174 | 175 | def stop(self): 176 | self._stopped = True 177 | self._loop.stop() 178 | self._waker.wake() 179 | 180 | def add_timeout(self, deadline, callback, *args, **kwargs): 181 | callback = stack_context.wrap(callback) 182 | if callable(callback): 183 | callback = functools.partial(callback, *args, **kwargs) 184 | timeout = _Timeout(deadline, callback, self) 185 | self._timeouts.add(timeout) 186 | return timeout 187 | 188 | def remove_timeout(self, timeout): 189 | self._timeouts.discard(timeout) 190 | if timeout._timer: 191 | timeout._timer.stop() 192 | 193 | def add_callback(self, callback, *args, **kwargs): 194 | with self._callback_lock: 195 | if self._closing: 196 | raise RuntimeError("IOLoop is closing") 197 | empty = not self._callbacks 198 | self._callbacks.append(functools.partial(stack_context.wrap(callback), *args, **kwargs)) 199 | if empty or thread.get_ident() != self._thread_ident: 200 | self._waker.wake() 201 | 202 | def add_callback_from_signal(self, callback, *args, **kwargs): 203 | with stack_context.NullContext(): 204 | if thread.get_ident() != self._thread_ident: 205 | # if the signal is handled on another thread, we can add 206 | # it normally (modulo the NullContext) 207 | self.add_callback(callback, *args, **kwargs) 208 | else: 209 | # If we're on the IOLoop's thread, we cannot use 210 | # the regular add_callback because it may deadlock on 211 | # _callback_lock. Blindly insert into self._callbacks. 212 | # This is safe because the GIL makes list.append atomic. 213 | # One subtlety is that if the signal interrupted the 214 | # _callback_lock block in IOLoop.start, we may modify 215 | # either the old or new version of self._callbacks, 216 | # but either way will work. 217 | self._callbacks.append(functools.partial(stack_context.wrap(callback), *args, **kwargs)) 218 | self._waker.wake() 219 | 220 | def _handle_poll_events(self, handle, poll_events, error): 221 | events = 0 222 | if error is not None: 223 | # Some error was detected, signal readability and writability so that the 224 | # handler gets and handles the error 225 | events |= IOLoop.READ | IOLoop.WRITE 226 | else: 227 | if poll_events & pyuv.UV_READABLE: 228 | events |= IOLoop.READ 229 | if poll_events & pyuv.UV_WRITABLE: 230 | events |= IOLoop.WRITE 231 | fd = handle.fileno() 232 | try: 233 | obj, poll = self._handlers[fd] 234 | callback_fd = fd 235 | if obj is not None and hasattr(obj, 'fileno'): 236 | # socket object was passed to add_handler, 237 | # return it to the callback 238 | callback_fd = obj 239 | 240 | poll.handler(callback_fd, events) 241 | except (OSError, IOError) as e: 242 | if e.args[0] == errno.EPIPE: 243 | # Happens when the client closes the connection 244 | pass 245 | else: 246 | logging.error("Exception in I/O handler for fd %s", fd, exc_info=True) 247 | except Exception: 248 | logging.error("Exception in I/O handler for fd %s", fd, exc_info=True) 249 | 250 | def _prepare_cb(self, handle): 251 | with self._callback_lock: 252 | callbacks = self._callbacks 253 | self._callbacks = [] 254 | for callback in callbacks: 255 | self._run_callback(callback) 256 | 257 | def _close_loop_handles(self): 258 | for handle in self._loop.handles: 259 | if not handle.closed: 260 | handle.close() 261 | 262 | 263 | class _Timeout(object): 264 | """An IOLoop timeout, a UNIX timestamp and a callback""" 265 | 266 | __slots__ = ['deadline', 'callback', '_timer'] 267 | 268 | def __init__(self, deadline, callback, io_loop): 269 | now = io_loop.time() 270 | if isinstance(deadline, numbers.Real): 271 | self.deadline = deadline 272 | elif isinstance(deadline, datetime.timedelta): 273 | self.deadline = now + _Timeout.timedelta_to_seconds(deadline) 274 | else: 275 | raise TypeError("Unsupported deadline %r" % deadline) 276 | self.callback = callback 277 | timeout = max(self.deadline - now, 0) 278 | self._timer = pyuv.Timer(io_loop._loop) 279 | self._timer.start(self._timer_cb, timeout, 0.0) 280 | 281 | def _timer_cb(self, handle): 282 | self._timer.close() 283 | self._timer = None 284 | io_loop = IOLoop.current() 285 | io_loop._timeouts.remove(self) 286 | io_loop._run_callback(self.callback) 287 | 288 | @staticmethod 289 | def timedelta_to_seconds(td): 290 | """Equivalent to td.total_seconds() (introduced in python 2.7).""" 291 | return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6) 292 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | 2 | [tox] 3 | envlist = py27,py33,py34,py35,py36,py27-deps,py33-deps,py34-deps,py35-deps,py36-deps,py27-t3,py33-t3,py34-t3 4 | 5 | 6 | [testenv] 7 | passenv = TRAVIS 8 | setenv = 9 | PYTHONHASHSEED = random 10 | deps = 11 | tornado 12 | pyuv 13 | commands = python -X faulthandler -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs} 14 | 15 | 16 | [testenv:py27] 17 | commands = python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs} 18 | 19 | 20 | [testenv:py27-deps] 21 | deps = 22 | {[testenv]deps} 23 | mock 24 | pycurl 25 | futures 26 | # pycares 27 | commands = python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs} 28 | 29 | 30 | [testenv:py33-deps] 31 | deps = 32 | {[testenv]deps} 33 | pycurl 34 | # pycares 35 | 36 | 37 | 38 | [testenv:py34-deps] 39 | deps = 40 | {[testenv]deps} 41 | pycurl 42 | # pycares 43 | 44 | 45 | [testenv:py35-deps] 46 | deps = 47 | {[testenv]deps} 48 | pycurl 49 | # pycares 50 | 51 | 52 | [testenv:py36-deps] 53 | deps = 54 | {[testenv]deps} 55 | pycurl 56 | # pycares 57 | 58 | 59 | [testenv:py27-t3] 60 | deps = 61 | tornado>=3.0,<4.0 62 | pyuv 63 | commands = python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs} 64 | 65 | 66 | [testenv:py33-t3] 67 | deps = 68 | tornado>=3.0,<4.0 69 | pyuv 70 | 71 | 72 | [testenv:py34-t3] 73 | deps = 74 | tornado>=3.0,<4.0 75 | pyuv 76 | --------------------------------------------------------------------------------