You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

382 lines
13 KiB
Python

5 years ago
# 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(self._on_expiration, getcurrent(), throws, update=True)
def _on_expiration(self, prev_greenlet, ex):
# Hook for subclasses.
prev_greenlet.throw(ex)
@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. A 0 value is allowed and creates a real 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 <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__
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()