Commit 40959196 authored by Jason Madden's avatar Jason Madden Committed by GitHub

Sleeping updates the loop's current time always. (#1228)

* Sleeping updates the loop's current time always.

That had been disabled for libuv, but now that we run timers in our own code that's not a concern anymore.

Fixes #1227.

* Try Python 3.5.5 because of the weird Illegal Instruction crash we suddenly see with 3.5.4

* Increase the value; test_sendall_timeout was often failing to actually timeout.

* Really python3.5
parent 9154efd7
...@@ -7,7 +7,10 @@ ...@@ -7,7 +7,10 @@
1.3.3 (unreleased) 1.3.3 (unreleased)
================== ==================
- Nothing changed yet. - :func:`gevent.sleep` updates the loop's notion of the current time
before sleeping so that sleep duration corresponds more closely to
elapsed (wall clock) time. :class:`gevent.Timeout` does the same.
Reported by champax and FoP in :issue:`1227`.
1.3.2.post0 (2018-05-30) 1.3.2.post0 (2018-05-30)
......
...@@ -129,7 +129,7 @@ BUILD_RUNTIMES?=$(PWD)/.runtimes ...@@ -129,7 +129,7 @@ BUILD_RUNTIMES?=$(PWD)/.runtimes
PY278=$(BUILD_RUNTIMES)/snakepit/python2.7.8 PY278=$(BUILD_RUNTIMES)/snakepit/python2.7.8
PY27=$(BUILD_RUNTIMES)/snakepit/python2.7.14 PY27=$(BUILD_RUNTIMES)/snakepit/python2.7.14
PY34=$(BUILD_RUNTIMES)/snakepit/python3.4.7 PY34=$(BUILD_RUNTIMES)/snakepit/python3.4.7
PY35=$(BUILD_RUNTIMES)/snakepit/python3.5.4 PY35=$(BUILD_RUNTIMES)/snakepit/python3.5.5
PY36=$(BUILD_RUNTIMES)/snakepit/python3.6.4 PY36=$(BUILD_RUNTIMES)/snakepit/python3.6.4
PY37=$(BUILD_RUNTIMES)/snakepit/python3.7.0b4 PY37=$(BUILD_RUNTIMES)/snakepit/python3.7.0b4
PYPY=$(BUILD_RUNTIMES)/snakepit/pypy5100 PYPY=$(BUILD_RUNTIMES)/snakepit/pypy5100
...@@ -191,7 +191,7 @@ test-py34: $(PY34) ...@@ -191,7 +191,7 @@ test-py34: $(PY34)
PYTHON=python3.4.7 PATH=$(BUILD_RUNTIMES)/versions/python3.4.7/bin:$(PATH) make develop basictest PYTHON=python3.4.7 PATH=$(BUILD_RUNTIMES)/versions/python3.4.7/bin:$(PATH) make develop basictest
test-py35: $(PY35) test-py35: $(PY35)
PYTHON=python3.5.4 PATH=$(BUILD_RUNTIMES)/versions/python3.5.4/bin:$(PATH) make develop basictest PYTHON=python3.5.5 PATH=$(BUILD_RUNTIMES)/versions/python3.5.5/bin:$(PATH) make develop basictest
test-py36: $(PY36) test-py36: $(PY36)
PYTHON=python3.6.4 PATH=$(BUILD_RUNTIMES)/versions/python3.6.4/bin:$(PATH) make develop allbackendtest PYTHON=python3.6.4 PATH=$(BUILD_RUNTIMES)/versions/python3.6.4/bin:$(PATH) make develop allbackendtest
......
...@@ -102,7 +102,7 @@ for var in "$@"; do ...@@ -102,7 +102,7 @@ for var in "$@"; do
install 3.4.7 python3.4.7 install 3.4.7 python3.4.7
;; ;;
3.5) 3.5)
install 3.5.4 python3.5.4 install 3.5.5 python3.5.5
;; ;;
3.6) 3.6)
install 3.6.4 python3.6.4 install 3.6.4 python3.6.4
......
...@@ -230,7 +230,7 @@ class ImportableSetting(object): ...@@ -230,7 +230,7 @@ class ImportableSetting(object):
if '.' not in path: if '.' not in path:
raise ImportError("Cannot import %r. " raise ImportError("Cannot import %r. "
"Required format: [path/][package.]module.class. " "Required format: [path/][package.]module.class. "
"Or choice from %r" "Or choose from %r"
% (path, list(self.shortname_map))) % (path, list(self.shortname_map)))
if '/' in path: if '/' in path:
......
...@@ -514,7 +514,7 @@ class TimerMixin(object): ...@@ -514,7 +514,7 @@ class TimerMixin(object):
# 1.3 changed the default for this to False *unless* the loop is # 1.3 changed the default for this to False *unless* the loop is
# running a callback; see libuv for details. Note that # running a callback; see libuv for details. Note that
# starting Timeout objects internally still sets this to true. # starting Timeout objects still sets this to true.
self.loop.update_now() self.loop.update_now()
super(TimerMixin, self).start(callback, *args) super(TimerMixin, self).start(callback, *args)
......
...@@ -152,6 +152,10 @@ def sleep(seconds=0, ref=True): ...@@ -152,6 +152,10 @@ def sleep(seconds=0, ref=True):
waiter.get() waiter.get()
else: else:
with loop.timer(seconds, ref=ref) as t: with loop.timer(seconds, ref=ref) as t:
# Sleeping is expected to be an "absolute" measure with
# respect to time.time(), not a relative measure, so it's
# important to update the loop's notion of now before we start
loop.update_now()
hub.wait(t) hub.wait(t)
......
...@@ -232,7 +232,9 @@ class Timeout(BaseException): ...@@ -232,7 +232,9 @@ class Timeout(BaseException):
# regular timeout with user-provided exception # regular timeout with user-provided exception
throws = self.exception throws = self.exception
self.timer.start(getcurrent().throw, throws) # Make sure the timer updates the current time so that we don't
# expire prematurely.
self.timer.start(getcurrent().throw, throws, update=True)
@classmethod @classmethod
def start_new(cls, timeout=None, exception=None, ref=True, _one_shot=False): def start_new(cls, timeout=None, exception=None, ref=True, _one_shot=False):
......
...@@ -24,61 +24,125 @@ import greentest ...@@ -24,61 +24,125 @@ import greentest
import weakref import weakref
import time import time
import gc import gc
from gevent import sleep, Timeout
DELAY = 0.04 from gevent import sleep
from gevent import Timeout
from gevent import get_hub
from greentest.timing import SMALL_TICK as DELAY
class Error(Exception): class Error(Exception):
pass pass
class _UpdateNowProxy(object):
update_now_calls = 0
def __init__(self, loop):
self.loop = loop
def __getattr__(self, name):
return getattr(self.loop, name)
def update_now(self):
self.update_now_calls += 1
self.loop.update_now()
class _UpdateNowWithTimerProxy(_UpdateNowProxy):
def timer(self, *_args, **_kwargs):
return _Timer(self)
class _Timer(object):
pending = False
active = False
def __init__(self, loop):
self.loop = loop
def start(self, *_args, **kwargs):
if kwargs.get("update"):
self.loop.update_now()
self.pending = self.active = True
def stop(self):
self.active = self.pending = False
def close(self):
"Does nothing"
class Test(greentest.TestCase): class Test(greentest.TestCase):
def test_timeout_calls_update_now(self):
hub = get_hub()
loop = hub.loop
proxy = _UpdateNowWithTimerProxy(loop)
hub.loop = proxy
try:
with Timeout(DELAY * 2) as t:
self.assertTrue(t.pending)
finally:
hub.loop = loop
self.assertEqual(1, proxy.update_now_calls)
def test_sleep_calls_update_now(self):
hub = get_hub()
loop = hub.loop
proxy = _UpdateNowProxy(loop)
hub.loop = proxy
try:
sleep(0.01)
finally:
hub.loop = loop
self.assertEqual(1, proxy.update_now_calls)
@greentest.skipOnAppVeyor("Timing is flaky, especially under Py 3.4/64-bit") @greentest.skipOnAppVeyor("Timing is flaky, especially under Py 3.4/64-bit")
def test_api(self): def test_api(self):
# Nothing happens if with-block finishes before the timeout expires # Nothing happens if with-block finishes before the timeout expires
t = Timeout(DELAY * 2) t = Timeout(DELAY * 2)
assert not t.pending, repr(t) self.assertFalse(t.pending, t)
with t: with t:
assert t.pending, repr(t) self.assertTrue(t.pending, t)
sleep(DELAY) sleep(DELAY)
# check if timer was actually cancelled # check if timer was actually cancelled
assert not t.pending, repr(t) self.assertFalse(t.pending, t)
sleep(DELAY * 2) sleep(DELAY * 2)
# An exception will be raised if it's not # An exception will be raised if it's not
try: with self.assertRaises(Timeout) as exc:
with Timeout(DELAY) as t: with Timeout(DELAY) as t:
sleep(DELAY * 10) sleep(DELAY * 10)
except Timeout as ex:
assert ex is t, (ex, t) self.assertIs(exc.exception, t)
else:
raise AssertionError('must raise Timeout')
# You can customize the exception raised: # You can customize the exception raised:
try: with self.assertRaises(IOError):
with Timeout(DELAY, IOError("Operation takes way too long")): with Timeout(DELAY, IOError("Operation takes way too long")):
sleep(DELAY * 10) sleep(DELAY * 10)
except IOError as ex:
assert str(ex) == "Operation takes way too long", repr(ex)
# Providing classes instead of values should be possible too: # Providing classes instead of values should be possible too:
try: with self.assertRaises(ValueError):
with Timeout(DELAY, ValueError): with Timeout(DELAY, ValueError):
sleep(DELAY * 10) sleep(DELAY * 10)
except ValueError:
pass
try: try:
1 / 0 1 / 0
except: except ZeroDivisionError:
try: with self.assertRaises(ZeroDivisionError):
with Timeout(DELAY, sys.exc_info()[0]): with Timeout(DELAY, sys.exc_info()[0]):
sleep(DELAY * 10) sleep(DELAY * 10)
raise AssertionError('should not get there') raise AssertionError('should not get there')
raise AssertionError('should not get there') raise AssertionError('should not get there')
except ZeroDivisionError:
pass
else: else:
raise AssertionError('should not get there') raise AssertionError('should not get there')
...@@ -107,7 +171,8 @@ class Test(greentest.TestCase): ...@@ -107,7 +171,8 @@ class Test(greentest.TestCase):
sleep(DELAY) sleep(DELAY)
del err del err
gc.collect() gc.collect()
assert not err_ref(), repr(err_ref()) self.assertFalse(err_ref(), err_ref)
def test_nested_timeout(self): def test_nested_timeout(self):
with Timeout(DELAY, False): with Timeout(DELAY, False):
...@@ -117,23 +182,25 @@ class Test(greentest.TestCase): ...@@ -117,23 +182,25 @@ class Test(greentest.TestCase):
with Timeout(DELAY) as t1: with Timeout(DELAY) as t1:
with Timeout(DELAY * 20) as t2: with Timeout(DELAY * 20) as t2:
try: with self.assertRaises(Timeout) as exc:
sleep(DELAY * 30) sleep(DELAY * 30)
except Timeout as ex: self.assertIs(exc.exception, t1)
assert ex is t1, (ex, t1)
assert not t1.pending, t1 self.assertFalse(t1.pending, t1)
assert t2.pending, t2 self.assertTrue(t2.pending, t2)
assert not t2.pending, t2
self.assertFalse(t2.pending)
with Timeout(DELAY * 20) as t1: with Timeout(DELAY * 20) as t1:
with Timeout(DELAY) as t2: with Timeout(DELAY) as t2:
try: with self.assertRaises(Timeout) as exc:
sleep(DELAY * 30) sleep(DELAY * 30)
except Timeout as ex: self.assertIs(exc.exception, t2)
assert ex is t2, (ex, t2)
assert t1.pending, t1 self.assertTrue(t1.pending, t1)
assert not t2.pending, t2 self.assertFalse(t2.pending, t2)
assert not t1.pending, t1
self.assertFalse(t1.pending)
if __name__ == '__main__': if __name__ == '__main__':
......
from gevent import monkey; monkey.patch_all() from gevent import monkey; monkey.patch_all()
import sys import sys
import os import os
import array import array
...@@ -200,32 +201,36 @@ class TestTCP(greentest.TestCase): ...@@ -200,32 +201,36 @@ class TestTCP(greentest.TestCase):
client.close() client.close()
client_sock[0][0].close() client_sock[0][0].close()
# On Windows send() accepts whatever is thrown at it # Subclasses can disable this
if sys.platform != 'win32': _test_sendall_timeout_check_time = True
_test_sendall_timeout_check_time = True # Travis-CI container infrastructure is configured with
# Travis-CI container infrastructure is configured with # large socket buffers, at least 2MB, as-of Jun 3, 2015,
# large socket buffers, at least 2MB, as-of Jun 3, 2015, # so we must be sure to send more data than that.
# so we must be sure to send more data than that. # In 2018, this needs to be increased *again* as a smaller value was
_test_sendall_data = b'hello' * 1000000 # still often being sent.
_test_sendall_data = b'hello' * 100000000
def test_sendall_timeout(self):
client_sock = [] # This doesn't make much sense...why are we really skipping this?
acceptor = Thread(target=lambda: client_sock.append(self.listener.accept())) @greentest.skipOnWindows("On Windows send() accepts whatever is thrown at it")
client = self.create_connection() def test_sendall_timeout(self):
time.sleep(0.1) client_sock = []
assert client_sock acceptor = Thread(target=lambda: client_sock.append(self.listener.accept()))
client.settimeout(0.1) client = self.create_connection()
start = time.time() time.sleep(0.1)
try: assert client_sock
self.assertRaises(self.TIMEOUT_ERROR, client.sendall, self._test_sendall_data) client.settimeout(0.1)
if self._test_sendall_timeout_check_time: start = time.time()
took = time.time() - start try:
assert 0.09 <= took <= 0.2, took with self.assertRaises(self.TIMEOUT_ERROR):
finally: client.sendall(self._test_sendall_data)
acceptor.join() if self._test_sendall_timeout_check_time:
client.close() took = time.time() - start
client_sock[0][0].close() self.assertTimeWithinRange(took, 0.09, 0.2)
finally:
acceptor.join()
client.close()
client_sock[0][0].close()
def test_makefile(self): def test_makefile(self):
def accept_once(): def accept_once():
......
from gevent import monkey; monkey.patch_all() from gevent import monkey; monkey.patch_all()
import os import os
import sys
import socket import socket
import greentest import greentest
# Be careful not to have TestTCP as a bare attribute in this module, # Be careful not to have TestTCP as a bare attribute in this module,
...@@ -31,43 +31,42 @@ class TestSSL(test__socket.TestTCP): ...@@ -31,43 +31,42 @@ class TestSSL(test__socket.TestTCP):
def create_connection(self, *args, **kwargs): def create_connection(self, *args, **kwargs):
return ssl.wrap_socket(super(TestSSL, self).create_connection(*args, **kwargs)) return ssl.wrap_socket(super(TestSSL, self).create_connection(*args, **kwargs))
if not sys.platform.startswith('win32'): # The SSL library can take a long time to buffer the large amount of data we're trying
# to send, so we can't compare to the timeout values
# The SSL library can take a long time to buffer the large amount of data we're trying _test_sendall_timeout_check_time = False
# to send, so we can't compare to the timeout values
_test_sendall_timeout_check_time = False # The SSL layer has extra buffering, so test_sendall needs
# to send a very large amount to make it timeout
# The SSL layer has extra buffering, so test_sendall needs _test_sendall_data = data_sent = b'hello' * 100000000
# to send a very large amount to make it timeout
_test_sendall_data = data_sent = b'hello' * 100000000 @greentest.skipOnWindows("Not clear why we're skipping")
def test_ssl_sendall_timeout0(self):
def test_ssl_sendall_timeout0(self): # Issue #317: SSL_WRITE_PENDING in some corner cases
# Issue #317: SSL_WRITE_PENDING in some corner cases
server_sock = []
server_sock = [] acceptor = test__socket.Thread(target=lambda: server_sock.append(self.listener.accept()))
acceptor = test__socket.Thread(target=lambda: server_sock.append(self.listener.accept())) client = self.create_connection()
client = self.create_connection() client.setblocking(False)
client.setblocking(False) try:
try: # Python 3 raises ssl.SSLWantWriteError; Python 2 simply *hangs*
# Python 3 raises ssl.SSLWantWriteError; Python 2 simply *hangs* # on non-blocking sockets because it's a simple loop around
# on non-blocking sockets because it's a simple loop around # send(). Python 2.6 doesn't have SSLWantWriteError
# send(). Python 2.6 doesn't have SSLWantWriteError expected = getattr(ssl, 'SSLWantWriteError', ssl.SSLError)
expected = getattr(ssl, 'SSLWantWriteError', ssl.SSLError) with self.assertRaises(expected):
with self.assertRaises(expected): client.sendall(self._test_sendall_data)
client.sendall(self._test_sendall_data) finally:
finally: acceptor.join()
acceptor.join() client.close()
client.close() server_sock[0][0].close()
server_sock[0][0].close()
def test_fullduplex(self):
elif greentest.LIBUV: try:
super(TestSSL, self).test_fullduplex()
def test_fullduplex(self): except LoopExit:
try: if greentest.LIBUV and greentest.WIN:
super(TestSSL, self).test_fullduplex()
except LoopExit:
# XXX: Unable to duplicate locally # XXX: Unable to duplicate locally
raise unittest.SkipTest("libuv on Windows sometimes raises LoopExit") raise unittest.SkipTest("libuv on Windows sometimes raises LoopExit")
raise
@greentest.ignores_leakcheck @greentest.ignores_leakcheck
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment