Commit b5a218a7 authored by Jason Madden's avatar Jason Madden

Python 2: socket.sendall on a non-blocking socket no longer immediately fails with a timeout

Instead, we only start checking the timer after the first attempted
send. This is what the C implementation in CPython does.

Fixes benoitc/gunicorn#1282.
parent da6f214b
...@@ -74,6 +74,8 @@ Stdlib Compatibility ...@@ -74,6 +74,8 @@ Stdlib Compatibility
would tend to report both read and write events. would tend to report both read and write events.
- Python 2: ``reload(site)`` no longer fails with a ``TypeError`` if - Python 2: ``reload(site)`` no longer fails with a ``TypeError`` if
gevent has been imported. Reported in :issue:`805` by Jake Hilton. gevent has been imported. Reported in :issue:`805` by Jake Hilton.
- Python 2: ``sendall`` on a non-blocking socket could spuriously fail
with a timeout.
Other Changes Other Changes
------------- -------------
......
...@@ -348,17 +348,21 @@ class socket(object): ...@@ -348,17 +348,21 @@ class socket(object):
""" """
data_sent = 0 data_sent = 0
len_data_memory = len(data_memory) len_data_memory = len(data_memory)
started_timer = 0
while data_sent < len_data_memory: while data_sent < len_data_memory:
chunk = data_memory[data_sent:] chunk = data_memory[data_sent:]
if timeleft is None: if timeleft is None:
data_sent += self.send(chunk, flags) data_sent += self.send(chunk, flags)
elif timeleft <= 0: elif started_timer and timeleft <= 0:
# Check before sending to guarantee a check # Check before sending to guarantee a check
# happens even if each chunk successfully sends its data # happens even if each chunk successfully sends its data
# (especially important for SSL sockets since they have large # (especially important for SSL sockets since they have large
# buffers) # buffers). But only do this if we've actually tried to
# send something once to avoid spurious timeouts on non-blocking
# sockets.
raise timeout('timed out') raise timeout('timed out')
else: else:
started_timer = 1
data_sent += self.send(chunk, flags, timeout=timeleft) data_sent += self.send(chunk, flags, timeout=timeleft)
timeleft = end - time.time() timeleft = end - time.time()
......
...@@ -58,17 +58,21 @@ class TestTCP(greentest.TestCase): ...@@ -58,17 +58,21 @@ class TestTCP(greentest.TestCase):
pass pass
del self.listener del self.listener
def create_connection(self, host='127.0.0.1', port=None, timeout=None): def create_connection(self, host='127.0.0.1', port=None, timeout=None,
blocking=None):
sock = socket.socket() sock = socket.socket()
sock.connect((host, port or self.port)) sock.connect((host, port or self.port))
if timeout is not None: if timeout is not None:
sock.settimeout(timeout) sock.settimeout(timeout)
if blocking is not None:
sock.setblocking(blocking)
return self._close_on_teardown(sock) return self._close_on_teardown(sock)
def _test_sendall(self, data, match_data=None, client_method='sendall', def _test_sendall(self, data, match_data=None, client_method='sendall',
**client_args): **client_args):
read_data = [] read_data = []
server_exc_info = []
def accept_and_read(): def accept_and_read():
try: try:
...@@ -78,8 +82,7 @@ class TestTCP(greentest.TestCase): ...@@ -78,8 +82,7 @@ class TestTCP(greentest.TestCase):
r.close() r.close()
conn.close() conn.close()
except: except:
traceback.print_exc() server_exc_info.append(sys.exc_info())
os._exit(1)
server = Thread(target=accept_and_read) server = Thread(target=accept_and_read)
client = self.create_connection(**client_args) client = self.create_connection(**client_args)
...@@ -95,6 +98,9 @@ class TestTCP(greentest.TestCase): ...@@ -95,6 +98,9 @@ class TestTCP(greentest.TestCase):
match_data = self.long_data match_data = self.long_data
self.assertEqual(read_data[0], match_data) self.assertEqual(read_data[0], match_data)
if server_exc_info:
six.reraise(*server_exc_info[0])
def test_sendall_str(self): def test_sendall_str(self):
self._test_sendall(self.long_data) self._test_sendall(self.long_data)
...@@ -115,6 +121,14 @@ class TestTCP(greentest.TestCase): ...@@ -115,6 +121,14 @@ class TestTCP(greentest.TestCase):
data = b'' data = b''
self._test_sendall(data, data, timeout=10) self._test_sendall(data, data, timeout=10)
def test_sendall_nonblocking(self):
# https://github.com/benoitc/gunicorn/issues/1282
# Even if the socket is non-blocking, we make at least
# one attempt to send data. Under Py2 before this fix, we
# would incorrectly immediately raise a timeout error
data = b'hi\n'
self._test_sendall(data, data, blocking=False)
def test_empty_send(self): def test_empty_send(self):
# Issue 719 # Issue 719
data = b'' data = b''
......
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