diff options
| -rw-r--r-- | .gitignore | 57 | ||||
| -rw-r--r-- | .travis.yml | 19 | ||||
| -rw-r--r-- | CHANGES.rst | 55 | ||||
| -rw-r--r-- | LICENSE | 22 | ||||
| -rw-r--r-- | MANIFEST.in | 3 | ||||
| -rw-r--r-- | Makefile | 25 | ||||
| -rw-r--r-- | README.rst | 32 | ||||
| -rw-r--r-- | debian/changelog | 11 | ||||
| -rw-r--r-- | debian/control | 34 | ||||
| -rw-r--r-- | debian/copyright | 30 | ||||
| -rwxr-xr-x | debian/rules | 7 | ||||
| -rw-r--r-- | debian/source/format | 1 | ||||
| -rw-r--r-- | debian/source/options | 1 | ||||
| -rw-r--r-- | debian/watch | 3 | ||||
| -rw-r--r-- | requirements-test.txt | 7 | ||||
| -rw-r--r-- | setup.cfg | 2 | ||||
| -rw-r--r-- | setup.py | 32 | ||||
| -rw-r--r-- | tests/__init__.py | 0 | ||||
| -rw-r--r-- | tests/threadloop/__init__.py | 0 | ||||
| -rw-r--r-- | tests/threadloop/test_threadloop.py | 305 | ||||
| -rw-r--r-- | threadloop/__init__.py | 3 | ||||
| -rw-r--r-- | threadloop/exceptions.py | 11 | ||||
| -rw-r--r-- | threadloop/threadloop.py | 160 |
23 files changed, 820 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f66d097 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.xml +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..207fd61 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python + +python: + - 2.7 + - pypy + - 3.3 + - 3.4 + - 3.5 + - pypy3 + +install: + - make install + +script: + - make test + - make lint + +after_success: + - coveralls diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..6a6df36 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,55 @@ +Changelog +========= + +1.0.2 (2016-04-01) +------------------ + +- Fix ``concurrent.Future`` exceptions not being propagated. + + +1.0.1 (2015-12-17) +------------------ + +- Fixed a bug where instantiating ``ThreadLoop`` would change the ``IOLoop`` + for the current thread. + + +1.0.0 (2015-10-05) +------------------ + +- Commit to current API. +- Better documentation. +- Dropped support for Python 2.6. +- Dropped support for Python 3.2. + + +0.5.0 (2015-08-17) +------------------ + +- Synchronous functions (i.e., those not returning futures) can be submitted to + the loop. + + +0.4.0 (2015-07-19) +------------------ + +- Added ``Thread.is_ready()``. +- Switched blocking mechanism to use a ``threading.Event``. + + +0.3.3 (2015-07-17) +------------------ + +- Fix race condition where submit can be called before Thread is fully started. + + +0.3.2 (2015-07-16) +------------------ + +- Fix exception traceback not propagating in python2. + + +0.3.1 (2015-07-13) +------------------ + +- Use a new IOLoop for each ThreadLoop. @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Grayson Koonce + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2795cce --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include threadloop * +include README.rst +include CHANGES.rst diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ffd67d --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +project := threadloop + +flake8 := flake8 +pytest := py.test -s --tb short --cov $(project) tests + +test_args := --cov-report term-missing --cov-report xml + +.DEFAULT_GOAL := test + +.PHONY:install +install: + pip install -r requirements-test.txt + python setup.py develop + +.PHONY: clean +clean: + @find $(project) tests -name "*.pyc" -delete + +.PHONY:test +test: clean lint + PYTHONDONTWRITEBYTECODE=1 $(pytest) $(test_args) + +.PHONY: lint +lint: + $(flake8) $(project) tests diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..293c6d9 --- /dev/null +++ b/README.rst @@ -0,0 +1,32 @@ +ThreadLoop +========== + +|build-status| |coverage| |pypi| + + Run Tornado Coroutines from Synchronous Python. + +.. code:: python + + + from threadloop import ThreadLoop + from tornado import gen + + @gen.coroutine + def coroutine(greeting="Goodbye"): + yield gen.sleep(1) + raise gen.Return("%s World" % greeting) + + with ThreadLoop() as threadloop: + future = threadloop.submit(coroutine, "Hello") + + print future.result() # Hello World + + +.. |build-status| image:: https://travis-ci.org/breerly/threadloop.svg?branch=master + :target: https://travis-ci.org/breerly/threadloop + +.. |coverage| image:: https://coveralls.io/repos/breerly/threadloop/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/breerly/threadloop?branch=master + +.. |pypi| image:: https://badge.fury.io/py/threadloop.svg + :target: http://badge.fury.io/py/threadloop diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..269ae93 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,11 @@ +python-threadloop (1.0.2-2) unstable; urgency=medium + + * Re-upload source-only. + + -- Thomas Goirand <zigo@debian.org> Tue, 10 Sep 2024 15:00:21 +0200 + +python-threadloop (1.0.2-1) unstable; urgency=medium + + * Initial release. (Closes: #1081203) + + -- Thomas Goirand <zigo@debian.org> Wed, 28 Aug 2024 11:24:34 +0200 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..eb69cd4 --- /dev/null +++ b/debian/control @@ -0,0 +1,34 @@ +Source: python-threadloop +Section: python +Priority: optional +Maintainer: Debian OpenStack <team+openstack@tracker.debian.org> +Uploaders: + Thomas Goirand <zigo@debian.org>, +Build-Depends: + debhelper-compat (= 11), + dh-python, + openstack-pkg-tools, + pybuild-plugin-pyproject, + python3-all, + python3-setuptools, +Build-Depends-Indep: + python3-tornado, +Standards-Version: 4.6.1 +Vcs-Browser: https://salsa.debian.org/openstack-team/libs/python-threadloop +Vcs-Git: https://salsa.debian.org/openstack-team/libs/python-threadloop.git +Homepage: https://github.com/TomerFi/aioswitcher + +Package: python3-threadloop +Architecture: all +Depends: + python3-tornado, + ${misc:Depends}, + ${python3:Depends}, +Description: Tornado IOLoop Backed Concurrent Futures + This package provides a way to run Tornado Coroutines from Synchronous Python. + . + Tornado is a Python web framework and asynchronous networking library, + originally developed at FriendFeed. By using non-blocking network I/O, + Tornado can scale to tens of thousands of open connections, making it ideal + for long polling, WebSockets, and other applications that require a + long-lived connection to each user. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..8c02e1c --- /dev/null +++ b/debian/copyright @@ -0,0 +1,30 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: threadloop +Source: https://github.com/TomerFi/aioswitcher + +Files: * +Copyright: (c) 2015-2016, Grayson Koonce <breerly@gmail.com> +License: Expat + +Files: debian/* +Copyright: (c) 2024, Thomas Goirand <zigo@debian.org> +License: Expat + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..c00f2a1 --- /dev/null +++ b/debian/rules @@ -0,0 +1,7 @@ +#!/usr/bin/make -f + +UPSTREAM_GIT := https://github.com/TomerFi/aioswitcher.git +include /usr/share/openstack-pkg-tools/pkgos.make + +%: + dh $@ --buildsystem=pybuild --with python3 diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..cb61fa5 --- /dev/null +++ b/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..fb203bb --- /dev/null +++ b/debian/watch @@ -0,0 +1,3 @@ +version=4 +opts="mode=git,uversionmangle=s/\.0rc/~rc/;s/\.0b1/~b1/;s/\.0b2/~b2/;s/\.0b3/~b3/" https://github.com/TomerFi/aioswitcher refs/tags/(\d[brc\d\.]+) + diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..f27ee02 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,7 @@ +flake8 +ipdb +pytest +pytest-cov +twine +zest.releaser +coveralls diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..81049c3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[zest.releaser] +create-wheel = yes diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7b48e25 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup, find_packages + +install_requires = ['tornado'] + +# if python < 3, we need futures backport +try: + import concurrent.futures # noqa +except ImportError: + install_requires.append('futures') + +setup( + name='threadloop', + version='1.0.2', + author='Grayson Koonce', + author_email='breerly@gmail.com', + description='Tornado IOLoop Backed Concurrent Futures', + license='MIT', + url='https://github.com/breerly/threadloop', + packages=find_packages(exclude=['tests']), + install_requires=install_requires, + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ] +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/threadloop/__init__.py b/tests/threadloop/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/threadloop/__init__.py diff --git a/tests/threadloop/test_threadloop.py b/tests/threadloop/test_threadloop.py new file mode 100644 index 0000000..39e38ac --- /dev/null +++ b/tests/threadloop/test_threadloop.py @@ -0,0 +1,305 @@ +from __future__ import absolute_import + +import time +from types import TracebackType +from decimal import Decimal + +import pytest +from tornado import gen, ioloop +from concurrent.futures import Future, as_completed, TimeoutError + +from threadloop import ThreadLoop +from threadloop.exceptions import ThreadNotStartedError + + +@pytest.fixture(autouse=True) +def clear_io_loop(): + # clear the current IOLoop before all tests + ioloop.IOLoop.clear_current() + + +@pytest.yield_fixture +def threadloop(): + with ThreadLoop() as threadloop: + yield threadloop + + +class TestException(Exception): + pass + + +def test_coroutine_returns_future(threadloop): + + @gen.coroutine + def coroutine(): + raise gen.Return("Hello World") + + future = threadloop.submit(coroutine) + + assert isinstance(future, Future), "expected a concurrent.futures.Future" + + assert future.result() == "Hello World" + + +def test_propogates_arguments(threadloop): + + @gen.coroutine + def coroutine(message, adjective="Shady"): + raise gen.Return("Hello %s %s" % (adjective, message)) + + future = threadloop.submit(coroutine, "World") + assert future.result() == "Hello Shady World" + + future = threadloop.submit(coroutine, "World", adjective="Cloudy") + assert future.result() == "Hello Cloudy World" + + +def test_coroutine_exception_propagates(threadloop): + + @gen.coroutine + def coroutine(): + raise TestException() + + with pytest.raises(TestException): + future = threadloop.submit(coroutine) + future.result() + + +def test_coroutine_exception_contains_exc_info(threadloop): + + @gen.coroutine + def coroutine(): + raise TestException('something went wrong') + + with pytest.raises(Exception) as exc_info: + threadloop.submit(coroutine).result() + + assert 'something went wrong' in str(exc_info.value) + assert isinstance(exc_info.value, TestException) + assert isinstance(exc_info.tb, TracebackType) + assert ( + "raise TestException('something went wrong')" + in str(exc_info.traceback[-1]) + ) + + +def test_propagate_concurrent_future_exception(threadloop): + def func(): + future = Future() + future.set_exception(AttributeError()) + return future + + with pytest.raises(AttributeError): + threadloop.submit(func).result() + + +def test_plain_function(threadloop): + + def not_a_coroutine(): + return "Hello World" + + future = threadloop.submit(not_a_coroutine) + + assert isinstance(future, Future), "expected a concurrent.futures.Future" + + assert future.result() == "Hello World" + + +def test_plain_function_exception_propagates(threadloop): + + def not_a_coroutine(): + raise TestException() + + future = threadloop.submit(not_a_coroutine) + + with pytest.raises(TestException): + future = threadloop.submit(not_a_coroutine) + future.result() + + +def test_plain_function_exception_contains_exc_info(threadloop): + + def not_a_coroutine(): + raise TestException('something went wrong') + + with pytest.raises(Exception) as exc_info: + threadloop.submit(not_a_coroutine).result() + + assert 'something went wrong' in str(exc_info.value) + assert isinstance(exc_info.value, TestException) + assert isinstance(exc_info.tb, TracebackType) + assert ( + "raise TestException('something went wrong')" + in str(exc_info.traceback[-1]) + ) + + +def test_use_existing_ioloop(): + io_loop = ioloop.IOLoop.current() + threadloop = ThreadLoop(io_loop) + + assert threadloop._io_loop is io_loop + + @gen.coroutine + def coroutine(): + raise gen.Return("Hello World") + + with threadloop: + future = threadloop.submit(coroutine) + assert future.result() == "Hello World" + + +def test_start_must_be_called_before_submit(): + threadloop = ThreadLoop() + + @gen.coroutine + def coroutine(): + raise gen.Return("Hello World") + + with pytest.raises(ThreadNotStartedError): + threadloop.submit(coroutine) + + +def test_submits_coroutines_concurrently(threadloop): + + @gen.coroutine + def coroutine1(): + yield gen.sleep(.1) + raise gen.Return('coroutine1') + + @gen.coroutine + def coroutine2(): + yield gen.sleep(.1) + raise gen.Return('coroutine2') + + @gen.coroutine + def coroutine3(): + yield gen.sleep(.1) + raise gen.Return('coroutine3') + + start = time.time() + + future1 = threadloop.submit(coroutine1) + future2 = threadloop.submit(coroutine2) + future3 = threadloop.submit(coroutine3) + + result1 = future1.result() + result2 = future2.result() + result3 = future3.result() + + end = time.time() - start + + # round to float with precision of 1, eg 0.3 + took = float(round(Decimal(str(end)), 1)) + + # should only take ~100 ms to finish both + # instead of ~300ms if they were executed serially + assert took == .1 + + assert result1 == 'coroutine1' + assert result2 == 'coroutine2' + assert result3 == 'coroutine3' + + +def test_as_completed(threadloop): + + @gen.coroutine + def coroutine1(): + yield gen.sleep(.02) + raise gen.Return('coroutine1') + + @gen.coroutine + def coroutine2(): + yield gen.sleep(.03) + raise gen.Return('coroutine2') + + @gen.coroutine + def coroutine3(): + yield gen.sleep(.01) + raise gen.Return('coroutine3') + + @gen.coroutine + def coroutine4(): + yield gen.sleep(.04) + raise gen.Return('coroutine4') + + futures = [] + futures.append(threadloop.submit(coroutine1)) + futures.append(threadloop.submit(coroutine2)) + futures.append(threadloop.submit(coroutine3)) + futures.append(threadloop.submit(coroutine4)) + + i = 0 + for future in as_completed(futures): + i = i + 1 + + # make sure futures finish in the expected order + if i == 1: + assert future.result() == "coroutine3" + elif i == 2: + assert future.result() == "coroutine1" + elif i == 3: + assert future.result() == "coroutine2" + elif i == 4: + assert future.result() == "coroutine4" + + assert i == 4, "expected 4 completed futures" + + +def test_timeout(threadloop): + + @gen.coroutine + def too_long(): + yield gen.sleep(5) # 5 sec task + raise gen.Return('that was too long') + + start = time.time() + future = threadloop.submit(too_long) + + with pytest.raises(TimeoutError): + future.result(timeout=.001) + + end = time.time() - start + took = float(round(Decimal(str(end)), 1)) + + assert took <= .002 + + +def test_block_until_thread_is_ready(): + + threadloop = ThreadLoop() + + assert not threadloop.is_ready() + + threadloop.start() + + assert threadloop.is_ready() + + +def test_is_not_ready_when_ready_hasnt_been_sent(): + + threadloop = ThreadLoop() + threadloop._thread = True # fake the Thread being set + + assert not threadloop.is_ready() + + +def test_main_io_loop_is_not_changed(): + threadloop = ThreadLoop() + threadloop.start() + + # The ThreadLoop's IOLoop should not be the 'current' IOLoop in the main + # thread. + tl_loop = threadloop.submit(ioloop.IOLoop.current).result() + assert ioloop.IOLoop.current() is not tl_loop + + +def test_ioloop_is_not_already_running(): + threadloop = ThreadLoop() + threadloop.start() + + @gen.coroutine + def f(): + yield threadloop.submit(gen.sleep, 0.1) + + ioloop.IOLoop.current().run_sync(f) diff --git a/threadloop/__init__.py b/threadloop/__init__.py new file mode 100644 index 0000000..a862cf0 --- /dev/null +++ b/threadloop/__init__.py @@ -0,0 +1,3 @@ +from __future__ import absolute_import + +from threadloop.threadloop import ThreadLoop # noqa diff --git a/threadloop/exceptions.py b/threadloop/exceptions.py new file mode 100644 index 0000000..29ccccb --- /dev/null +++ b/threadloop/exceptions.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import + + +class ThreadLoopException(Exception): + """Top-level library exception.""" + pass + + +class ThreadNotStartedError(ThreadLoopException): + """Raised when calling submit before ThreadLoop.start() was called.""" + pass diff --git a/threadloop/threadloop.py b/threadloop/threadloop.py new file mode 100644 index 0000000..8b584b6 --- /dev/null +++ b/threadloop/threadloop.py @@ -0,0 +1,160 @@ +from __future__ import absolute_import + +import sys +from concurrent.futures import Future +from threading import Event, Thread + +from tornado import ioloop +from tornado import gen + +from .exceptions import ThreadNotStartedError + +# Python3's concurrent.futures.Future doesn't allow +# setting exc_info... but exc_info works w/o setting explicitly +_FUTURE_HAS_EXC_INFO = hasattr(Future, "set_exception_info") + + +class ThreadLoop(object): + """Tornado IOLoop Backed Concurrent Futures. + + Run Tornado Coroutines from Synchronous Python. + + This is made possible by starting the IOLoop in another thread. When + coroutines are submitted, they are ran against that loop, and their + responses are bound to Concurrent Futures. + + .. code-block:: python + + from threadloop import ThreadLoop + from tornado import gen + + @gen.coroutine + def coroutine(greeting="Goodbye"): + yield gen.sleep(1) + raise gen.Return("%s World" % greeting) + + with ThreadLoop() as threadloop: + + future = threadloop.submit(coroutine, "Hello") + + print future.result() # Hello World + + """ + def __init__(self, io_loop=None): + + self._thread = None + self._ready = Event() + self._io_loop = io_loop + + def __enter__(self): + self.start() + return self + + def __exit__(self, *args): + self.stop() + + def start(self): + """Start IOLoop in daemonized thread.""" + assert self._thread is None, 'thread already started' + + # configure thread + self._thread = Thread(target=self._start_io_loop) + self._thread.daemon = True + + # begin thread and block until ready + self._thread.start() + self._ready.wait() + + def _start_io_loop(self): + """Start IOLoop then set ready threading.Event.""" + + def mark_as_ready(): + self._ready.set() + + if not self._io_loop: + self._io_loop = ioloop.IOLoop() + + self._io_loop.add_callback(mark_as_ready) + self._io_loop.start() + + def is_ready(self): + """Is thread & ioloop ready. + + :returns bool: + """ + if not self._thread: + return False + + if not self._ready.is_set(): + return False + + return True + + def stop(self): + """Stop IOLoop & close daemonized thread.""" + self._io_loop.stop() + self._thread.join() + + def submit(self, fn, *args, **kwargs): + """Submit Tornado Coroutine to IOLoop in daemonized thread. + + :param fn: Tornado Coroutine to execute + :param args: Args to pass to coroutine + :param kwargs: Kwargs to pass to coroutine + :returns concurrent.futures.Future: future result of coroutine + """ + if not self.is_ready(): + raise ThreadNotStartedError( + "The thread has not been started yet, " + "make sure you call start() first" + ) + + future = Future() + + def execute(): + """Executes fn on the IOLoop.""" + try: + result = gen.maybe_future(fn(*args, **kwargs)) + except Exception: + # The function we ran didn't return a future and instead raised + # an exception. Let's pretend that it returned this dummy + # future with our stack trace. + f = gen.Future() + f.set_exc_info(sys.exc_info()) + on_done(f) + else: + result.add_done_callback(on_done) + + def on_done(f): + """Sets tornado.Future results to the concurrent.Future.""" + + if not f.exception(): + future.set_result(f.result()) + return + + # if f is a tornado future, then it has exc_info() + if hasattr(f, 'exc_info'): + exception, traceback = f.exc_info()[1:] + + # else it's a concurrent.future + else: + # python2's concurrent.future has exception_info() + if hasattr(f, 'exception_info'): + exception, traceback = f.exception_info() + + # python3's concurrent.future just has exception() + else: + exception = f.exception() + traceback = None + + # python2 needs exc_info set explicitly + if _FUTURE_HAS_EXC_INFO: + future.set_exception_info(exception, traceback) + return + + # python3 just needs the exception, exc_info works fine + future.set_exception(exception) + + self._io_loop.add_callback(execute) + + return future |
