# 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() 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) 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) @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 def cancel(self): """ If the timeout is pending, cancel it. Otherwise, do nothing. The timeout object can be :meth:`started ` again. If you will not start the timeout again, you should use :meth:`close` instead. """ self.timer.stop() if self._one_shot: self.close() 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) 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 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 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()