Source code for greenado.concurrent

#
# Copyright 2014-2016 Dustin Spicuzza
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from functools import wraps
import sys
import types

import greenlet

from tornado import gen
from tornado.stack_context import wrap as sc_wrap, NullContext
from tornado.ioloop import IOLoop

# Tornado 5.0 compatibility
try:
    from tornado.concurrent import TracebackFuture as _Future
except ImportError:
    from tornado.concurrent import Future as _Future

try:
    from tornado.concurrent import future_set_exc_info
except ImportError:
    def future_set_exc_info(future, exc_info):
        future.set_exc_info(exc_info)

import logging
logger = logging.getLogger('greenado')

[docs]class TimeoutError(Exception): """Exception raised by ``gyield`` in timeout."""
[docs]def gcall(f, *args, **kwargs): ''' Calls a function, makes it asynchronous, and returns the result of the function as a :class:`tornado.concurrent.Future`. The wrapped function may use :func:`gyield` to pseudo-synchronously wait for a future to resolve. This is the same code that :func:`@greenado.groutine <groutine>` uses to wrap functions. :param f: Function to call :param args: Function arguments :param kwargs: Function keyword arguments :returns: :class:`tornado.concurrent.Future` .. warning:: You should not discard the returned Future or exceptions may be silently discarded, similar to a tornado coroutine. See :func:`@gen.coroutine <tornado.gen.coroutine>` for details. ''' # When this function gets updated, update groutine.wrapper also! future = _Future() def greenlet_base(): try: result = f(*args, **kwargs) except Exception: future_set_exc_info(future, sys.exc_info()) else: future.set_result(result) gr = greenlet.greenlet(sc_wrap(greenlet_base)) with NullContext(): gr.switch() return future
[docs]def generator(f): ''' A decorator that allows you to use the 'yield' keyword in a function, without requiring callers to also yield this function. The yield keyword can be used inside a decorated function on any function call that returns a future object, such as functions decorated by :func:`@gen.coroutine <tornado.gen.coroutine>`, and most of the tornado API as of tornado 4.0. Similar to :func:`@gen.coroutine <tornado.gen.coroutine>`, in versions of Python before 3.3 you must raise :class:`tornado.gen.Return` to return a value from this function. This function must only be used by functions that either have a :func:`@greenado.groutine <groutine>` decorator, or functions that are children of functions that have the decorator applied. .. versionadded:: 0.1.7 .. warning:: You should not discard the returned Future or exceptions may be silently discarded, similar to a tornado coroutine. See :func:`@gen.coroutine <tornado.gen.coroutine>` for details. ''' # Note: this code is vaguely similar to tornado's gen.coroutine/Runner, # which is licensed under the Apache 2.0 license. @wraps(f) def wrapper(*args, **kwargs): assert greenlet.getcurrent().parent is not None, "functions decorated with generator() can only be called from functions that have the @greenado.groutine decorator in the call stack." try: result = f(*args, **kwargs) except (gen.Return, StopIteration) as e: result = getattr(e, 'value', None) else: if isinstance(result, types.GeneratorType): try: future = next(result) while True: try: value = gyield(future) except Exception: result.throw(*sys.exc_info()) else: future = result.send(value) except (gen.Return, StopIteration) as e: return getattr(e, 'value', None) return result return wrapper
[docs]def gmoment(): ''' Similar to :func:`tornado.gen.moment`, yields the IOLoop for a single iteration from inside a groutine. ''' gr = greenlet.getcurrent() assert gr.parent is not None, "gmoment() can only be called from functions that have the @greenado.groutine decorator in the call stack." io_loop = IOLoop.current() def _finish(): gr.switch() io_loop.add_callback(_finish) with NullContext(): gr.parent.switch()
[docs]def groutine(f): ''' A decorator that makes a function asynchronous and returns the result of the function as a :class:`tornado.concurrent.Future`. The wrapped function may use :func:`gyield` to pseudo-synchronously wait for a future to resolve. The primary advantage to using this decorator is that it allows *all* called functions and their children to use :func:`gyield`, and doesn't require the use of generators. If you are calling a groutine-wrapped function from a function with a :func:`@greenado.groutine <groutine>` decorator, you will need to use :func:`gyield` to wait for the returned future to resolve. From a caller's perspective, this decorator is functionally equivalent to the :func:`@gen.coroutine <tornado.gen.coroutine>` decorator. You should not use this decorator and the :func:`@gen.coroutine <tornado.gen.coroutine>` decorator on the same function. .. warning:: You should not discard the returned Future or exceptions may be silently discarded, similar to a tornado coroutine. See :func:`@gen.coroutine <tornado.gen.coroutine>` for details. ''' @wraps(f) def wrapper(*args, **kwargs): # When this function gets updated, update gcall also! future = _Future() def greenlet_base(): try: result = f(*args, **kwargs) except Exception: future_set_exc_info(future, sys.exc_info()) else: future.set_result(result) gr = greenlet.greenlet(sc_wrap(greenlet_base)) with NullContext(): gr.switch() return future return wrapper
[docs]def gsleep(timeout): ''' This will yield and allow other operations to occur in the background before returning. :param timeout: Number of seconds to wait .. versionadded:: 0.1.9 ''' gr = greenlet.getcurrent() assert gr.parent is not None, "gsleep() can only be called from functions that have the @greenado.groutine decorator in the call stack." if timeout <= 0: raise ValueError("Invalid timeout value '%s'" % timeout) io_loop = IOLoop.current() done = [False] def on_timeout(): done[0] = True gr.switch() io_loop.add_timeout(io_loop.time() + timeout, on_timeout) with NullContext(): while not done[0]: gr.parent.switch()
[docs]def gyield(future, timeout=None): ''' This is functionally equivalent to the 'yield' statements used in a :func:`@gen.coroutine <tornado.gen.coroutine>`, but doesn't require turning all your functions into generators -- so you can use the return statement normally, and exceptions won't be accidentally discarded. This can be used on any function that returns a future object, such as functions decorated by :func:`@gen.coroutine <tornado.gen.coroutine>`, and most of the tornado API as of tornado 4.0. This function must only be used by functions that either have a :func:`@greenado.groutine <groutine>` decorator, or functions that are children of functions that have the decorator applied. :param future: A :class:`tornado.concurrent.Future` object :param timeout: Number of seconds to wait before raising a :exc:`TimeoutError`. Default is no timeout. `Parameter added in version 0.1.8.` :returns: The result set on the future object :raises: * If an exception is set on the future, the exception will be thrown to the caller of gyield. * If the timeout expires, :exc:`TimeoutError` will be raised. .. versionchanged:: 0.1.8 Added timeout parameter .. versionchanged:: 0.2.0 If a timeout occurs, the :exc:`TimeoutError` will not be set on the future object, but will only be raised to the caller. .. note: This cannot be used with :func:`tornado.gen.moment`, use :func:`gmoment` instead ''' gr = greenlet.getcurrent() assert gr.parent is not None, "gyield() can only be called from functions that have the @greenado.groutine decorator in the call stack." # don't switch/wait if the future is already ready to go if not future.done(): io_loop = IOLoop.current() wait_future = future if timeout != None and timeout > 0: # optimization: only do timeout related work if a timeout is happening.. timeout_handle = None timeout_future = None def on_complete(result): if timeout_future.done(): # resolve the future so tornado doesn't complain try: result.result() except Exception: # If you don't want to see this error, then implement cancellation # in the thing that the future came from logger.warn("gyield() timeout expired, and this exception was ignored", exc_info=1) else: timeout_future.set_result(True) io_loop.remove_timeout(timeout_handle) gr.switch() def on_timeout(): timeout_future.set_exception(TimeoutError("Timeout after %s seconds" % timeout)) gr.switch() wait_future = timeout_future = _Future() timeout_handle = io_loop.add_timeout( io_loop.time() + timeout, on_timeout ) else: def on_complete(result): gr.switch() io_loop.add_future(future, on_complete) with NullContext(): gr.parent.switch() while not wait_future.done(): gr.parent.switch() wait_future.result() return future.result()