diff options
Diffstat (limited to 'python/gevent/timeout.py')
-rw-r--r-- | python/gevent/timeout.py | 214 |
1 files changed, 163 insertions, 51 deletions
diff --git a/python/gevent/timeout.py b/python/gevent/timeout.py index f08b81a..1cc05e4 100644 --- a/python/gevent/timeout.py +++ b/python/gevent/timeout.py @@ -13,12 +13,18 @@ module add timeouts to arbitrary code. 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.hub import getcurrent, _NONE, get_hub +from gevent._util import _NONE -__all__ = ['Timeout', - 'with_timeout'] +from greenlet import getcurrent +from gevent._hub_local import get_hub_noargs as get_hub + +__all__ = [ + 'Timeout', + 'with_timeout', +] class _FakeTimer(object): @@ -26,8 +32,22 @@ class _FakeTimer(object): # 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 - pending = False - active = False + + # 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): + return None + + timer = exception = seconds def start(self, *args, **kwargs): # pylint:disable=unused-argument @@ -36,7 +56,14 @@ class _FakeTimer(object): def stop(self): return - def cancel(self): + cancel = stop + + stop = close = cancel + + def __enter__(self): + return self + + def __exit__(self, _t, _v, _tb): return _FakeTimer = _FakeTimer() @@ -44,23 +71,30 @@ _FakeTimer = _FakeTimer() class Timeout(BaseException): """ - Raise *exception* in the current greenlet after given time period:: + 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.cancel() + timeout.close() - .. note:: If the code that the timeout was protecting finishes - executing before the timeout elapses, be sure to ``cancel`` the - timeout so it is not unexpectedly raised in the future. Even if - it is raised, it is a best practice to cancel it. This - ``try/finally`` construct or a ``with`` statement is a - recommended pattern. + .. note:: - When *exception* is omitted or ``None``, the :class:`Timeout` instance itself is raised: + 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() @@ -69,14 +103,41 @@ class Timeout(BaseException): ... Timeout: 0.1 seconds - To simplify starting and canceling timeouts, the ``with`` statement can be used:: + 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 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:: @@ -89,9 +150,14 @@ class Timeout(BaseException): else: ... # a line was read within 5 seconds - .. caution:: If ``readline()`` above catches and doesn't re-raise :class:`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. + .. 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 @@ -105,54 +171,73 @@ class Timeout(BaseException): except Timeout as t: if t is not timeout: raise # not my timeout + finally: + timeout.close() - 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.) - - .. 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 - if ``None``. .. versionchanged:: 1.1b2 - If *seconds* is not given or is ``None``, no longer allocate a libev - timer that will never be started. + + 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. + + 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. + """ - def __init__(self, seconds=None, exception=None, ref=True, priority=-1, _use_timer=True): + # 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 - if seconds is None or not _use_timer: + 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.""" - assert not self.pending, '%r is already started; to restart it, cancel it first' % self - if self.seconds is None: # "fake" timeout (never expires) + 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 - self.timer.start(getcurrent().throw, self) - else: # regular timeout with user-provided exception - self.timer.start(getcurrent().throw, self.exception) + 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): + 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: @@ -168,12 +253,12 @@ class Timeout(BaseException): if not timeout.pending: timeout.start() return timeout - timeout = cls(timeout, exception, ref=ref) + timeout = cls(timeout, exception, ref=ref, _one_shot=_one_shot) timeout.start() return timeout @staticmethod - def _start_new_or_dummy(timeout, exception=None): + 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, @@ -187,16 +272,33 @@ class Timeout(BaseException): # under PyPy in synthetic benchmarks it makes no difference. if timeout is None: return _FakeTimer - return Timeout.start_new(timeout, exception) + return Timeout.start_new(timeout, exception, ref, _one_shot=True) @property def pending(self): - """Return True if the timeout is scheduled to be raised.""" + """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.""" + """ + 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() + + 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__ @@ -229,14 +331,24 @@ class Timeout(BaseException): 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): - self.cancel() + """ + 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 + return True # Suppress the exception def with_timeout(seconds, function, *args, **kwds): @@ -249,7 +361,7 @@ def with_timeout(seconds, function, *args, **kwds): Keyword argument *timeout_value* is not passed to *function*. """ timeout_value = kwds.pop("timeout_value", _NONE) - timeout = Timeout.start_new(seconds) + timeout = Timeout.start_new(seconds, _one_shot=True) try: try: return function(*args, **kwds) |