Source code for gevent.timeout

# Copyright (c) 2009-2010 Denis Bilenko. See LICENSE for details.
"""
Timeouts.

Many functions in :mod:`gevent` have a *timeout* argument that allows
limiting the time the function will block. When that is not available,
the :class:`Timeout` class and :func:`with_timeout` function in this
module add timeouts to arbitrary code.

.. warning::

    Timeouts can only work when the greenlet switches to the hub.
    If a blocking function is called or an intense calculation is ongoing during
    which no switches occur, :class:`Timeout` is powerless.
"""
from __future__ import absolute_import, print_function, division

from gevent._compat import string_types
from gevent._util import _NONE

from greenlet import getcurrent
from gevent._hub_local import get_hub_noargs as get_hub

__all__ = [
    'Timeout',
    'with_timeout',
]


class _FakeTimer(object):
    # An object that mimics the API of get_hub().loop.timer, but
    # without allocating any native resources. This is useful for timeouts
    # that will never expire.
    # Also partially mimics the API of Timeout itself for use in _start_new_or_dummy

    # This object is used as a singleton, so it should be
    # immutable.
    __slots__ = ()

    @property
    def pending(self):
        return False

    active = pending

    @property
    def seconds(self):
        "Always returns None"

    timer = exception = seconds

    def start(self, *args, **kwargs):
        # pylint:disable=unused-argument
        raise AssertionError("non-expiring timer cannot be started")

    def stop(self):
        return

    cancel = stop

    stop = close = cancel

    def __enter__(self):
        return self

    def __exit__(self, _t, _v, _tb):
        return

_FakeTimer = _FakeTimer()


[docs]class Timeout(BaseException): """ Timeout(seconds=None, exception=None, ref=True, priority=-1) Raise *exception* in the current greenlet after *seconds* have elapsed:: timeout = Timeout(seconds, exception) timeout.start() try: ... # exception will be raised here, after *seconds* passed since start() call finally: timeout.close() .. note:: If the code that the timeout was protecting finishes executing before the timeout elapses, be sure to ``close`` the timeout so it is not unexpectedly raised in the future. Even if it is raised, it is a best practice to close it. This ``try/finally`` construct or a ``with`` statement is a recommended pattern. (If the timeout object will be started again, use ``cancel`` instead of ``close``; this is rare.) When *exception* is omitted or ``None``, the ``Timeout`` instance itself is raised:: >>> import gevent >>> gevent.Timeout(0.1).start() >>> gevent.sleep(0.2) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... Timeout: 0.1 seconds If the *seconds* argument is not given or is ``None`` (e.g., ``Timeout()``), then the timeout will never expire and never raise *exception*. This is convenient for creating functions which take an optional timeout parameter of their own. (Note that this is **not** the same thing as a *seconds* value of ``0``.) :: def function(args, timeout=None): "A function with an optional timeout." timer = Timeout(timeout) with timer: ... .. caution:: A *seconds* value less than ``0.0`` (e.g., ``-1``) is poorly defined. In the future, support for negative values is likely to do the same thing as a value of ``None`` or ``0`` A *seconds* value of ``0`` requests that the event loop spin and poll for I/O; it will immediately expire as soon as control returns to the event loop. .. rubric:: Use As A Context Manager To simplify starting and canceling timeouts, the ``with`` statement can be used:: with gevent.Timeout(seconds, exception) as timeout: pass # ... code block ... This is equivalent to the try/finally block above with one additional feature: if *exception* is the literal ``False``, the timeout is still raised, but the context manager suppresses it, so the code outside the with-block won't see it. This is handy for adding a timeout to the functions that don't support a *timeout* parameter themselves:: data = None with gevent.Timeout(5, False): data = mysock.makefile().readline() if data is None: ... # 5 seconds passed without reading a line else: ... # a line was read within 5 seconds .. caution:: If ``readline()`` above catches and doesn't re-raise :exc:`BaseException` (for example, with a bare ``except:``), then your timeout will fail to function and control won't be returned to you when you expect. .. rubric:: Catching Timeouts When catching timeouts, keep in mind that the one you catch may not be the one you have set (a calling function may have set its own timeout); if you going to silence a timeout, always check that it's the instance you need:: timeout = Timeout(1) timeout.start() try: ... except Timeout as t: if t is not timeout: raise # not my timeout finally: timeout.close() .. versionchanged:: 1.1b2 If *seconds* is not given or is ``None``, no longer allocate a native timer object that will never be started. .. versionchanged:: 1.1 Add warning about negative *seconds* values. .. versionchanged:: 1.3a1 Timeout objects now have a :meth:`close` method that must be called when the timeout will no longer be used to properly clean up native resources. The ``with`` statement does this automatically. """ # We inherit a __dict__ from BaseException, so __slots__ actually # makes us larger. def __init__(self, seconds=None, exception=None, ref=True, priority=-1, _one_shot=False): BaseException.__init__(self) self.seconds = seconds self.exception = exception self._one_shot = _one_shot if seconds is None: # Avoid going through the timer codepath if no timeout is # desired; this avoids some CFFI interactions on PyPy that can lead to a # RuntimeError if this implementation is used during an `import` statement. See # https://bitbucket.org/pypy/pypy/issues/2089/crash-in-pypy-260-linux64-with-gevent-11b1 # and https://github.com/gevent/gevent/issues/618. # Plus, in general, it should be more efficient self.timer = _FakeTimer else: # XXX: A timer <= 0 could cause libuv to block the loop; we catch # that case in libuv/loop.py self.timer = get_hub().loop.timer(seconds or 0.0, ref=ref, priority=priority)
[docs] def start(self): """Schedule the timeout.""" if self.pending: raise AssertionError('%r is already started; to restart it, cancel it first' % self) if self.seconds is None: # "fake" timeout (never expires) return if self.exception is None or self.exception is False or isinstance(self.exception, string_types): # timeout that raises self throws = self else: # regular timeout with user-provided exception throws = self.exception # Make sure the timer updates the current time so that we don't # expire prematurely. self.timer.start(getcurrent().throw, throws, update=True)
[docs] @classmethod def start_new(cls, timeout=None, exception=None, ref=True, _one_shot=False): """Create a started :class:`Timeout`. This is a shortcut, the exact action depends on *timeout*'s type: * If *timeout* is a :class:`Timeout`, then call its :meth:`start` method if it's not already begun. * Otherwise, create a new :class:`Timeout` instance, passing (*timeout*, *exception*) as arguments, then call its :meth:`start` method. Returns the :class:`Timeout` instance. """ if isinstance(timeout, Timeout): if not timeout.pending: timeout.start() return timeout timeout = cls(timeout, exception, ref=ref, _one_shot=_one_shot) timeout.start() return timeout
@staticmethod def _start_new_or_dummy(timeout, exception=None, ref=True): # Internal use only in 1.1 # Return an object with a 'cancel' method; if timeout is None, # this will be a shared instance object that does nothing. Otherwise, # return an actual Timeout. Because negative values are hard to reason about, # and are often used as sentinels in Python APIs, in the future it's likely # that a negative timeout will also return the shared instance. # This saves the previously common idiom of 'timer = Timeout.start_new(t) if t is not None else None' # followed by 'if timer is not None: timer.cancel()'. # That idiom was used to avoid any object allocations. # A staticmethod is slightly faster under CPython, compared to a classmethod; # under PyPy in synthetic benchmarks it makes no difference. if timeout is None: return _FakeTimer return Timeout.start_new(timeout, exception, ref, _one_shot=True) @property def pending(self): """True if the timeout is scheduled to be raised.""" return self.timer.pending or self.timer.active
[docs] def cancel(self): """ If the timeout is pending, cancel it. Otherwise, do nothing. The timeout object can be :meth:`started <start>` again. If you will not start the timeout again, you should use :meth:`close` instead. """ self.timer.stop() if self._one_shot: self.close()
[docs] def close(self): """ Close the timeout and free resources. The timer cannot be started again after this method has been used. """ self.timer.stop() self.timer.close() self.timer = _FakeTimer
def __repr__(self): classname = type(self).__name__ if self.pending: pending = ' pending' else: pending = '' if self.exception is None: exception = '' else: exception = ' exception=%r' % self.exception return '<%s at %s seconds=%s%s%s>' % (classname, hex(id(self)), self.seconds, exception, pending) def __str__(self): """ >>> raise Timeout #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... Timeout """ if self.seconds is None: return '' suffix = '' if self.seconds == 1 else 's' if self.exception is None: return '%s second%s' % (self.seconds, suffix) if self.exception is False: return '%s second%s (silent)' % (self.seconds, suffix) return '%s second%s: %s' % (self.seconds, suffix, self.exception)
[docs] def __enter__(self): """ Start and return the timer. If the timer is already started, just return it. """ if not self.pending: self.start() return self
[docs] def __exit__(self, typ, value, tb): """ Stop the timer. .. versionchanged:: 1.3a1 The underlying native timer is also stopped. This object cannot be used again. """ self.close() if value is self and self.exception is False: return True # Suppress the exception
[docs]def with_timeout(seconds, function, *args, **kwds): """Wrap a call to *function* with a timeout; if the called function fails to return before the timeout, cancel it and return a flag value, provided by *timeout_value* keyword argument. If timeout expires but *timeout_value* is not provided, raise :class:`Timeout`. Keyword argument *timeout_value* is not passed to *function*. """ timeout_value = kwds.pop("timeout_value", _NONE) timeout = Timeout.start_new(seconds, _one_shot=True) try: try: return function(*args, **kwds) except Timeout as ex: if ex is timeout and timeout_value is not _NONE: return timeout_value raise finally: timeout.cancel()