aboutsummaryrefslogtreecommitdiffstats
path: root/python/gevent/timeout.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/gevent/timeout.py')
-rw-r--r--python/gevent/timeout.py261
1 files changed, 261 insertions, 0 deletions
diff --git a/python/gevent/timeout.py b/python/gevent/timeout.py
new file mode 100644
index 0000000..f08b81a
--- /dev/null
+++ b/python/gevent/timeout.py
@@ -0,0 +1,261 @@
+# 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 gevent._compat import string_types
+from gevent.hub import getcurrent, _NONE, 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
+ pending = False
+ active = False
+
+ def start(self, *args, **kwargs):
+ # pylint:disable=unused-argument
+ raise AssertionError("non-expiring timer cannot be started")
+
+ def stop(self):
+ return
+
+ def cancel(self):
+ return
+
+_FakeTimer = _FakeTimer()
+
+
+class Timeout(BaseException):
+ """
+ Raise *exception* in the current greenlet after given time period::
+
+ timeout = Timeout(seconds, exception)
+ timeout.start()
+ try:
+ ... # exception will be raised here, after *seconds* passed since start() call
+ finally:
+ timeout.cancel()
+
+ .. 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.
+
+ When *exception* is omitted or ``None``, the :class:`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
+
+ 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 :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.
+
+ 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
+
+ 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.
+ .. versionchanged:: 1.1
+ Add warning about negative *seconds* values.
+ """
+
+ def __init__(self, seconds=None, exception=None, ref=True, priority=-1, _use_timer=True):
+ BaseException.__init__(self)
+ self.seconds = seconds
+ self.exception = exception
+ if seconds is None or not _use_timer:
+ # 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:
+ 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)
+ 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)
+
+ @classmethod
+ def start_new(cls, timeout=None, exception=None, ref=True):
+ """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)
+ timeout.start()
+ return timeout
+
+ @staticmethod
+ def _start_new_or_dummy(timeout, exception=None):
+ # 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)
+
+ @property
+ def pending(self):
+ """Return 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."""
+ self.timer.stop()
+
+ 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):
+ if not self.pending:
+ self.start()
+ return self
+
+ def __exit__(self, typ, value, tb):
+ self.cancel()
+ if value is self and self.exception is False:
+ return True
+
+
+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)
+ 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()