From: Ben Darnell Date: Tue, 17 Jan 2012 07:23:21 +0000 (-0800) Subject: Major update to tornado.platform.twisted. X-Git-Tag: v2.2.0~33 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=af940f4e0bfd458230a4f6fdc1205e6bd019458b;p=thirdparty%2Ftornado.git Major update to tornado.platform.twisted. Significantly improved compatibility (most important changes are in TornadoReactor._invoke_callback) and expanded test coverage. --- diff --git a/tornado/platform/twisted.py b/tornado/platform/twisted.py index 25fe36f35..5d406d347 100644 --- a/tornado/platform/twisted.py +++ b/tornado/platform/twisted.py @@ -13,14 +13,35 @@ # License for the specific language governing permissions and limitations # under the License. -""" -A twisted-style reactor for the Tornado IOLoop. +# Note: This module's docs are not currently extracted automatically, +# so changes must be made manually to twisted.rst +# TODO: refactor doc build process to use an appropriate virtualenv +"""A Twisted reactor built on the Tornado IOLoop. + +This module lets you run applications and libraries written for +Twisted in a Tornado application. To use it, simply call `install` at +the beginning of the application:: + + import tornado.platform.twisted + tornado.platform.twisted.install() + from twisted.internet import reactor + +When the app is ready to start, call `IOLoop.instance().start()` +instead of `reactor.run()`. This will allow you to use a mixture of +Twisted and Tornado code in the same process. + +It is also possible to create a non-global reactor by calling +`tornado.platform.twisted.TornadoReactor(io_loop)`. However, if +the `IOLoop` and reactor are to be short-lived (such as those used in +unit tests), additional cleanup may be required. Specifically, it is +recommended to call:: + + reactor.fireSystemEvent('shutdown') + reactor.disconnectAll() -To use it, add the following to your twisted application: +before closing the `IOLoop`. -import tornado.platform.twisted -tornado.platform.twisted.install() -from twisted.internet import reactor +This module has been tested with Twisted versions 11.0.0 and 11.1.0. """ from __future__ import with_statement, absolute_import @@ -32,7 +53,7 @@ import time from twisted.internet.posixbase import PosixReactorBase from twisted.internet.interfaces import \ IReactorFDSet, IDelayedCall, IReactorTime -from twisted.python import failure +from twisted.python import failure, log from twisted.internet import error from zope.interface import implements @@ -44,9 +65,7 @@ from tornado.ioloop import IOLoop class TornadoDelayedCall(object): - """ - DelayedCall object for Tornado. - """ + """DelayedCall object for Tornado.""" implements(IDelayedCall) def __init__(self, reactor, seconds, f, *args, **kw): @@ -89,8 +108,14 @@ class TornadoDelayedCall(object): return self._active class TornadoReactor(PosixReactorBase): - """ - Twisted style reactor for Tornado. + """Twisted reactor built on the Tornado IOLoop. + + Since it is intented to be used in applications where the top-level + event loop is ``io_loop.start()`` rather than ``reactor.run()``, + it is implemented a little differently than other Twisted reactors. + We override `mainLoop` instead of `doIteration` and must implement + timed call functionality on top of `IOLoop.add_timeout` rather than + using the implementation in `PosixReactorBase`. """ implements(IReactorTime, IReactorFDSet) @@ -98,14 +123,20 @@ class TornadoReactor(PosixReactorBase): if not io_loop: io_loop = tornado.ioloop.IOLoop.instance() self._io_loop = io_loop - self._readers = {} - self._writers = {} + self._readers = {} # map of reader objects to fd + self._writers = {} # map of writer objects to fd self._fds = {} # a map of fd to a (reader, writer) tuple self._delayedCalls = {} - self._running = False - self._closed = False PosixReactorBase.__init__(self) + # IOLoop.start() bypasses some of the reactor initialization. + # Fire off the necessary events if they weren't already triggered + # by reactor.run(). + def start_if_necessary(): + if not self._started: + self.fireSystemEvent('startup') + self._io_loop.add_callback(start_if_necessary) + # IReactorTime def seconds(self): return time.time() @@ -124,9 +155,7 @@ class TornadoReactor(PosixReactorBase): # IReactorThreads def callFromThread(self, f, *args, **kw): - """ - See L{twisted.internet.interfaces.IReactorThreads.callFromThread}. - """ + """See `twisted.internet.interfaces.IReactorThreads.callFromThread`""" assert callable(f), "%s is not callable" % f p = functools.partial(f, *args, **kw) self._io_loop.add_callback(p) @@ -142,25 +171,36 @@ class TornadoReactor(PosixReactorBase): # IReactorFDSet def _invoke_callback(self, fd, events): (reader, writer) = self._fds[fd] - if events & IOLoop.READ and reader: - reader.doRead() - if events & IOLoop.WRITE and writer: - writer.doWrite() - if events & IOLoop.ERROR: - if reader: - reader.readConnectionLost(failure.Failure(error.ConnectionLost())) - if writer: - writer.connectionLost(failure.Failure(error.ConnectionLost())) + if reader: + err = None + if reader.fileno() == -1: + err = error.ConnectionLost() + elif events & IOLoop.READ: + err = log.callWithLogger(reader, reader.doRead) + if err is None and events & IOLoop.ERROR: + err = error.ConnectionLost() + if err is not None: + self.removeReader(reader) + reader.readConnectionLost(failure.Failure(err)) + if writer: + err = None + if writer.fileno() == -1: + err = error.ConnectionLost() + elif events & IOLoop.WRITE: + err = log.callWithLogger(writer, writer.doWrite) + if err is None and events & IOLoop.ERROR: + err = error.ConnectionLost() + if err is not None: + self.removeWriter(writer) + writer.writeConnectionLost(failure.Failure(err)) def addReader(self, reader): - """ - Add a FileDescriptor for notification of data available to read. - """ + """Add a FileDescriptor for notification of data available to read.""" if reader in self._readers: # Don't add the reader if it's already there return - self._readers[reader] = True fd = reader.fileno() + self._readers[reader] = fd if fd in self._fds: (_, writer) = self._fds[fd] self._fds[fd] = (reader, writer) @@ -175,13 +215,11 @@ class TornadoReactor(PosixReactorBase): IOLoop.READ) def addWriter(self, writer): - """ - Add a FileDescriptor for notification of data available to write. - """ + """Add a FileDescriptor for notification of data available to write.""" if writer in self._writers: return - self._writers[writer] = True fd = writer.fileno() + self._writers[writer] = fd if fd in self._fds: (reader, _) = self._fds[fd] self._fds[fd] = (reader, writer) @@ -196,13 +234,9 @@ class TornadoReactor(PosixReactorBase): IOLoop.WRITE) def removeReader(self, reader): - """ - Remove a Selectable for notification of data available to read. - """ - fd = reader.fileno() + """Remove a Selectable for notification of data available to read.""" if reader in self._readers: - del self._readers[reader] - if self._closed: return + fd = self._readers.pop(reader) (_, writer) = self._fds[fd] if writer: # We have a writer so we need to update the IOLoop for @@ -217,13 +251,9 @@ class TornadoReactor(PosixReactorBase): self._io_loop.remove_handler(fd) def removeWriter(self, writer): - """ - Remove a Selectable for notification of data available to write. - """ - fd = writer.fileno() + """Remove a Selectable for notification of data available to write.""" if writer in self._writers: - del self._writers[writer] - if self._closed: return + fd = self._writers.pop(writer) (reader, _) = self._fds[fd] if reader: # We have a reader so we need to update the IOLoop for @@ -246,47 +276,30 @@ class TornadoReactor(PosixReactorBase): def getWriters(self): return self._writers.keys() + # The following functions are mainly used in twisted-style test cases; + # it is expected that most users of the TornadoReactor will call + # IOLoop.start() instead of Reactor.run(). def stop(self): - """ - Implement L{IReactorCore.stop}. - """ - self._running = False PosixReactorBase.stop(self) - self.runUntilCurrent() - try: - self._io_loop.stop() - self._io_loop.close() - except: - # Ignore any exceptions thrown by IOLoop - pass - self._closed = True + self._io_loop.stop() def crash(self): - if not self._running: - return - self._running = False PosixReactorBase.crash(self) - self.runUntilCurrent() - try: - self._io_loop.stop() - self._io_loop.close() - except: - # Ignore any exceptions thrown by IOLoop - pass - self._closed = True + self._io_loop.stop() def doIteration(self, delay): raise NotImplementedError("doIteration") def mainLoop(self): - self._running = True self._io_loop.start() + if self._stopped: + self.fireSystemEvent("shutdown") class _TestReactor(TornadoReactor): """Subclass of TornadoReactor for use in unittests. This can't go in the test.py file because of import-order dependencies - with the twisted reactor test builder. + with the Twisted reactor test builder. """ def __init__(self): # always use a new ioloop @@ -299,12 +312,16 @@ class _TestReactor(TornadoReactor): return super(_TestReactor, self).listenTCP( port, factory, backlog=backlog, interface=interface) + def listenUDP(self, port, protocol, interface='', maxPacketSize=8192): + if not interface: + interface = '127.0.0.1' + return super(_TestReactor, self).listenUDP( + port, protocol, interface=interface, maxPacketSize=maxPacketSize) + def install(io_loop=None): - """ - Install the Tornado reactor. - """ + """Install this package as the default Twisted reactor.""" if not io_loop: io_loop = tornado.ioloop.IOLoop.instance() reactor = TornadoReactor(io_loop) diff --git a/tornado/test/twisted_test.py b/tornado/test/twisted_test.py index 56312458f..ba53c7897 100644 --- a/tornado/test/twisted_test.py +++ b/tornado/test/twisted_test.py @@ -25,7 +25,13 @@ import unittest try: import fcntl import twisted + from twisted.internet.defer import Deferred from twisted.internet.interfaces import IReadDescriptor, IWriteDescriptor + from twisted.internet.protocol import Protocol + from twisted.web.client import Agent + from twisted.web.resource import Resource + from twisted.web.server import Site + from twisted.python import log from tornado.platform.twisted import TornadoReactor from zope.interface import implements except ImportError: @@ -34,14 +40,22 @@ except ImportError: IReadDescriptor = IWriteDescriptor = None def implements(f): pass +from tornado.httpclient import AsyncHTTPClient from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec +from tornado.testing import get_unused_port from tornado.util import import_object +from tornado.web import RequestHandler, Application -class ReactorWhenRunningTest(unittest.TestCase): +class ReactorTestCase(unittest.TestCase): def setUp(self): - self._reactor = TornadoReactor(IOLoop()) + self._io_loop = IOLoop() + self._reactor = TornadoReactor(self._io_loop) + def tearDown(self): + self._io_loop.close(all_fds=True) + +class ReactorWhenRunningTest(ReactorTestCase): def test_whenRunning(self): self._whenRunningCalled = False self._anotherWhenRunningCalled = False @@ -58,10 +72,7 @@ class ReactorWhenRunningTest(unittest.TestCase): def anotherWhenRunningCallback(self): self._anotherWhenRunningCalled = True -class ReactorCallLaterTest(unittest.TestCase): - def setUp(self): - self._reactor = TornadoReactor(IOLoop()) - +class ReactorCallLaterTest(ReactorTestCase): def test_callLater(self): self._laterCalled = False self._now = self._reactor.seconds() @@ -78,10 +89,7 @@ class ReactorCallLaterTest(unittest.TestCase): self._called = self._reactor.seconds() self._reactor.stop() -class ReactorTwoCallLaterTest(unittest.TestCase): - def setUp(self): - self._reactor = TornadoReactor(IOLoop()) - +class ReactorTwoCallLaterTest(ReactorTestCase): def test_callLater(self): self._later1Called = False self._later2Called = False @@ -108,13 +116,14 @@ class ReactorTwoCallLaterTest(unittest.TestCase): self._called2 = self._reactor.seconds() self._reactor.stop() -class ReactorCallFromThreadTest(unittest.TestCase): +class ReactorCallFromThreadTest(ReactorTestCase): def setUp(self): - self._reactor = TornadoReactor(IOLoop()) + super(ReactorCallFromThreadTest, self).setUp() self._mainThread = thread.get_ident() def tearDown(self): self._thread.join() + super(ReactorCallFromThreadTest, self).tearDown() def _newThreadRun(self): self.assertNotEqual(self._mainThread, thread.get_ident()) @@ -134,9 +143,9 @@ class ReactorCallFromThreadTest(unittest.TestCase): self._reactor.callWhenRunning(self._whenRunningCallback) self._reactor.run() -class ReactorCallInThread(unittest.TestCase): +class ReactorCallInThread(ReactorTestCase): def setUp(self): - self._reactor = TornadoReactor(IOLoop()) + super(ReactorCallInThread, self).setUp() self._mainThread = thread.get_ident() def _fnCalledInThread(self, *args, **kwargs): @@ -192,13 +201,13 @@ class Writer: def doWrite(self): self._callback(self._fd) -class ReactorReaderWriterTest(unittest.TestCase): +class ReactorReaderWriterTest(ReactorTestCase): def _set_nonblocking(self, fd): flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) def setUp(self): - self._reactor = TornadoReactor(IOLoop()) + super(ReactorReaderWriterTest, self).setUp() r, w = os.pipe() self._set_nonblocking(r) self._set_nonblocking(w) @@ -207,6 +216,11 @@ class ReactorReaderWriterTest(unittest.TestCase): self._p1 = os.fdopen(r, "rb", 0) self._p2 = os.fdopen(w, "wb", 0) + def tearDown(self): + super(ReactorReaderWriterTest, self).tearDown() + self._p1.close() + self._p2.close() + def _testReadWrite(self): """ In this test the writer writes an 'x' to its fd. The reader @@ -267,6 +281,106 @@ class ReactorReaderWriterTest(unittest.TestCase): self._reactor.callWhenRunning(self._testNoWriter) self._reactor.run() +# Test various combinations of twisted and tornado http servers, +# http clients, and event loop interfaces. +class CompatibilityTests(unittest.TestCase): + def setUp(self): + self.io_loop = IOLoop() + self.reactor = TornadoReactor(self.io_loop) + + def tearDown(self): + self.reactor.disconnectAll() + self.io_loop.close(all_fds=True) + + def start_twisted_server(self): + class HelloResource(Resource): + isLeaf = True + def render_GET(self, request): + return "Hello from twisted!" + site = Site(HelloResource()) + self.twisted_port = get_unused_port() + self.reactor.listenTCP(self.twisted_port, site, interface='127.0.0.1') + + def start_tornado_server(self): + class HelloHandler(RequestHandler): + def get(self): + self.write("Hello from tornado!") + app = Application([('/', HelloHandler)], + log_function=lambda x: None) + self.tornado_port = get_unused_port() + app.listen(self.tornado_port, address='127.0.0.1', io_loop=self.io_loop) + + def run_ioloop(self): + self.stop_loop = self.io_loop.stop + self.io_loop.start() + self.reactor.fireSystemEvent('shutdown') + + def run_reactor(self): + self.stop_loop = self.reactor.stop + self.stop = self.reactor.stop + self.reactor.run() + + def tornado_fetch(self, url, runner): + responses = [] + client = AsyncHTTPClient(self.io_loop) + def callback(response): + responses.append(response) + self.stop_loop() + client.fetch(url, callback=callback) + runner() + self.assertEqual(len(responses), 1) + responses[0].rethrow() + return responses[0] + + def twisted_fetch(self, url, runner): + # http://twistedmatrix.com/documents/current/web/howto/client.html + chunks = [] + client = Agent(self.reactor) + d = client.request('GET', url) + class Accumulator(Protocol): + def __init__(self, finished): + self.finished = finished + def dataReceived(self, data): + chunks.append(data) + def connectionLost(self, reason): + self.finished.callback(None) + def callback(response): + finished = Deferred() + response.deliverBody(Accumulator(finished)) + return finished + d.addCallback(callback) + def shutdown(ignored): + self.stop_loop() + d.addBoth(shutdown) + runner() + self.assertTrue(chunks) + return ''.join(chunks) + + def testTwistedServerTornadoClientIOLoop(self): + self.start_twisted_server() + response = self.tornado_fetch( + 'http://localhost:%d' % self.twisted_port, self.run_ioloop) + self.assertEqual(response.body, 'Hello from twisted!') + + def testTwistedServerTornadoClientReactor(self): + self.start_twisted_server() + response = self.tornado_fetch( + 'http://localhost:%d' % self.twisted_port, self.run_reactor) + self.assertEqual(response.body, 'Hello from twisted!') + + def testTornadoServerTwistedClientIOLoop(self): + self.start_tornado_server() + response = self.twisted_fetch( + 'http://localhost:%d' % self.tornado_port, self.run_ioloop) + self.assertEqual(response, 'Hello from tornado!') + + def testTornadoServerTwistedClientReactor(self): + self.start_tornado_server() + response = self.twisted_fetch( + 'http://localhost:%d' % self.tornado_port, self.run_reactor) + self.assertEqual(response, 'Hello from tornado!') + + if twisted is None: del ReactorWhenRunningTest del ReactorCallLaterTest @@ -274,39 +388,87 @@ if twisted is None: del ReactorCallFromThreadTest del ReactorCallInThread del ReactorReaderWriterTest + del CompatibilityTests else: # Import and run as much of twisted's test suite as possible. # This is unfortunately rather dependent on implementation details, # but there doesn't appear to be a clean all-in-one conformance test # suite for reactors. + # # This is a list of all test suites using the ReactorBuilder - # available in Twisted 11.0.0. Tests that do not currently pass - # with the TornadoReactor are commented out. - twisted_tests = [ - 'twisted.internet.test.test_core.ObjectModelIntegrationTest', - #'twisted.internet.test.test_core.SystemEventTestsBuilder', - 'twisted.internet.test.test_fdset.ReactorFDSetTestsBuilder', - #'twisted.internet.test.test_process.ProcessTestsBuilder', - #'twisted.internet.test.test_process.PTYProcessTestsBuilder', - #'twisted.internet.test.test_tcp.TCPClientTestsBuilder', - 'twisted.internet.test.test_tcp.TCPPortTestsBuilder', - 'twisted.internet.test.test_tcp.TCPConnectionTestsBuilder', - 'twisted.internet.test.test_threads.ThreadTestsBuilder', - 'twisted.internet.test.test_time.TimeTestsBuilder', - #'twisted.internet.test.test_tls.SSLClientTestsMixin', - 'twisted.internet.test.test_udp.UDPServerTestsBuilder', - #'twisted.internet.test.test_unix.UNIXTestsBuilder', - #'twisted.internet.test.test_unix.UNIXDatagramTestsBuilder', - ] - for test_name in twisted_tests: + # available in Twisted 11.0.0 and 11.1.0 (and a blacklist of + # specific test methods to be disabled). + twisted_tests = { + 'twisted.internet.test.test_core.ObjectModelIntegrationTest': [], + 'twisted.internet.test.test_core.SystemEventTestsBuilder': [ + 'test_iterate', # deliberately not supported + ], + 'twisted.internet.test.test_fdset.ReactorFDSetTestsBuilder': [ + "test_lostFileDescriptor", # incompatible with epoll and kqueue + ], + 'twisted.internet.test.test_process.ProcessTestsBuilder': [ + # Doesn't work on python 2.5 + 'test_systemCallUninterruptedByChildExit', + # Doesn't clean up its temp files + 'test_shebang', + ], + 'twisted.internet.test.test_process.PTYProcessTestsBuilder': [ + 'test_systemCallUninterruptedByChildExit', + ], + 'twisted.internet.test.test_tcp.TCPClientTestsBuilder': [], + 'twisted.internet.test.test_tcp.TCPPortTestsBuilder': [], + 'twisted.internet.test.test_tcp.TCPConnectionTestsBuilder': [], + 'twisted.internet.test.test_tcp.WriteSequenceTests': [], + 'twisted.internet.test.test_tcp.AbortConnectionTestCase': [], + 'twisted.internet.test.test_threads.ThreadTestsBuilder': [], + 'twisted.internet.test.test_time.TimeTestsBuilder': [], + # Extra third-party dependencies (pyOpenSSL) + #'twisted.internet.test.test_tls.SSLClientTestsMixin': [], + 'twisted.internet.test.test_udp.UDPServerTestsBuilder': [], + 'twisted.internet.test.test_unix.UNIXTestsBuilder': [ + # Platform-specific. These tests would be skipped automatically + # if we were running twisted's own test runner. + 'test_connectToLinuxAbstractNamespace', + 'test_listenOnLinuxAbstractNamespace', + ], + 'twisted.internet.test.test_unix.UNIXDatagramTestsBuilder': [ + 'test_listenOnLinuxAbstractNamespace', + ], + 'twisted.internet.test.test_unix.UNIXPortTestsBuilder': [], + } + for test_name, blacklist in twisted_tests.iteritems(): try: - test = import_object(test_name) + test_class = import_object(test_name) except (ImportError, AttributeError): continue - class TornadoTest(test): - _reactors = ["tornado.platform.twisted._TestReactor"] - TornadoTest.__name__ = test.__name__ - globals().update(TornadoTest.makeTestCaseClasses()) + for test_func in blacklist: + if hasattr(test_class, test_func): + # The test_func may be defined in a mixin, so clobber + # it instead of delattr() + setattr(test_class, test_func, lambda self: None) + def make_test_subclass(test_class): + class TornadoTest(test_class): + _reactors = ["tornado.platform.twisted._TestReactor"] + def unbuildReactor(self, reactor): + test_class.unbuildReactor(self, reactor) + # Clean up file descriptors (especially epoll/kqueue + # objects) eagerly instead of leaving them for the + # GC. Unfortunately we can't do this in reactor.stop + # since twisted expects to be able to unregister + # connections in a post-shutdown hook. + reactor._io_loop.close(all_fds=True) + TornadoTest.__name__ = test_class.__name__ + return TornadoTest + test_subclass = make_test_subclass(test_class) + globals().update(test_subclass.makeTestCaseClasses()) + + # Since we're not using twisted's test runner, it's tricky to get + # logging set up well. Most of the time it's easiest to just + # leave it turned off, but while working on these tests you may want + # to uncomment one of the other lines instead. + log.defaultObserver.stop() + #import sys; log.startLogging(sys.stderr, setStdout=0) + #log.startLoggingWithObserver(log.PythonLoggingObserver().emit, setStdout=0) if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index 30a78088d..88c8c3757 100644 --- a/tox.ini +++ b/tox.ini @@ -36,8 +36,10 @@ deps = MySQL-python pycurl simplejson - twisted==11.0.0 + twisted>=11.1.0 +# py26-full deliberately runs an older version of twisted to ensure +# we're still compatible with the oldest version we support. [testenv:py26-full] basepython = python2.6 deps = @@ -50,7 +52,7 @@ basepython = python2.7 deps = MySQL-python pycurl - twisted==11.0.0 + twisted>=11.1.0 [testenv:py27-curl] # Same as py27-full, but runs the tests with curl_httpclient by default. @@ -60,7 +62,7 @@ basepython = python2.7 deps = MySQL-python pycurl - twisted==11.0.0 + twisted>=11.0.0 commands = python -m tornado.test.runtests --httpclient=tornado.curl_httpclient.CurlAsyncHTTPClient {posargs:} # No pypy-full yet: pycurl doesn't build with pypy, and installing diff --git a/website/sphinx/releases/next.rst b/website/sphinx/releases/next.rst index aec6654b8..6fbbf1eef 100644 --- a/website/sphinx/releases/next.rst +++ b/website/sphinx/releases/next.rst @@ -65,8 +65,8 @@ Other modules * `SimpleAsyncHTTPClient` no longer hangs on ``HEAD`` requests, responses with no content, or empty ``POST``/``PUT`` response bodies. -* `tornado.platform.twisted` compatibility has been improved. However, - only Twisted version 11.0.0 is supported (and not 11.1.0). +* `tornado.platform.twisted` compatibility has been significantly improved. + Twisted version 11.1.0 is now supported in addition to 11.0.0. * `tornado.web` now behaves better when given malformed ``Cookie`` headers * `RequestHandler.redirect` now has a ``status`` argument to send status codes other than 301 and 302. diff --git a/website/sphinx/twisted.rst b/website/sphinx/twisted.rst index 5b107d46c..6a4d85416 100644 --- a/website/sphinx/twisted.rst +++ b/website/sphinx/twisted.rst @@ -3,17 +3,43 @@ .. module:: tornado.platform.twisted -This module contains an implementation of the Twisted Reactor built -on the Tornado IOLoop. This lets you run applications and libraries -written for Twisted in a Tornado application. To use it, simply call -`install` at the beginnging of the application:: +This module contains a Twisted reactor build on the Tornado IOLoop, +which lets you run applications and libraries written for Twisted in a +Tornado application. To use it, simply call `install` at the +beginning of the application:: import tornado.platform.twisted tornado.platform.twisted.install() - from twisted.internet import reactor - ... + +When the app is ready to start, call `IOLoop.instance().start()` +instead of `reactor.run()`. This will allow you to use a mixture of +Twisted and Tornado code in the same process. + +It is also possible to create a non-global reactor by calling +`tornado.platform.twisted.TornadoReactor(io_loop)`. However, if +the `IOLoop` and reactor are to be short-lived (such as those used in +unit tests), additional cleanup may be required. Specifically, it is +recommended to call:: + + reactor.fireSystemEvent('shutdown') + reactor.disconnectAll() + +before closing the `IOLoop`. + +This module has been tested with Twisted versions 11.0.0 and 11.1.0. .. function:: install(io_loop=None) - Installs this package as the default Twisted reactor. +Install this package as the default Twisted reactor. + +.. class:: TornadoReactor(io_loop=None) + +Twisted reactor built on the Tornado IOLoop. + +Since it is intented to be used in applications where the top-level +event loop is ``io_loop.start()`` rather than ``reactor.run()``, +it is implemented a little differently than other Twisted reactors. +We override `mainLoop` instead of `doIteration` and must implement +timed call functionality on top of `IOLoop.add_timeout` rather than +using the implementation in `PosixReactorBase`.