summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuido van Rossum <guido@python.org>2015-10-19 18:49:30 (GMT)
committerGuido van Rossum <guido@python.org>2015-10-19 18:49:30 (GMT)
commitc44ecdf687897a20f11d0c5212b51e8d31f6100a (patch)
treecd908b285d3249995ae3f34a0ae49cb093203253
parent2bf91bf46cf5bf9913a126092a8d08bcd7a6157c (diff)
downloadcpython-c44ecdf687897a20f11d0c5212b51e8d31f6100a.zip
cpython-c44ecdf687897a20f11d0c5212b51e8d31f6100a.tar.gz
cpython-c44ecdf687897a20f11d0c5212b51e8d31f6100a.tar.bz2
Issue #25441: asyncio: Raise error from drain() when socket is closed.
-rw-r--r--Lib/asyncio/streams.py9
-rw-r--r--Lib/test/test_asyncio/test_streams.py43
-rw-r--r--Misc/NEWS2
3 files changed, 54 insertions, 0 deletions
diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py
index bb9fb31..fb786ed 100644
--- a/Lib/asyncio/streams.py
+++ b/Lib/asyncio/streams.py
@@ -301,6 +301,15 @@ class StreamWriter:
exc = self._reader.exception()
if exc is not None:
raise exc
+ if self._transport is not None:
+ if self._transport._closing:
+ # Yield to the event loop so connection_lost() may be
+ # called. Without this, _drain_helper() would return
+ # immediately, and code that calls
+ # write(...); yield from drain()
+ # in a loop would never call connection_lost(), so it
+ # would not see an error when the socket is closed.
+ yield
yield from self._protocol._drain_helper()
diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py
index d4a7582..1f375f0 100644
--- a/Lib/test/test_asyncio/test_streams.py
+++ b/Lib/test/test_asyncio/test_streams.py
@@ -2,8 +2,10 @@
import gc
import os
+import queue
import socket
import sys
+import threading
import unittest
from unittest import mock
try:
@@ -632,6 +634,47 @@ os.close(fd)
protocol = asyncio.StreamReaderProtocol(reader)
self.assertIs(protocol._loop, self.loop)
+ def test_drain_raises(self):
+ # See http://bugs.python.org/issue25441
+
+ # This test should not use asyncio for the mock server; the
+ # whole point of the test is to test for a bug in drain()
+ # where it never gives up the event loop but the socket is
+ # closed on the server side.
+
+ q = queue.Queue()
+
+ def server():
+ # Runs in a separate thread.
+ sock = socket.socket()
+ sock.bind(('localhost', 0))
+ sock.listen(1)
+ addr = sock.getsockname()
+ q.put(addr)
+ clt, _ = sock.accept()
+ clt.close()
+
+ @asyncio.coroutine
+ def client(host, port):
+ reader, writer = yield from asyncio.open_connection(host, port, loop=self.loop)
+ while True:
+ writer.write(b"foo\n")
+ yield from writer.drain()
+
+ # Start the server thread and wait for it to be listening.
+ thread = threading.Thread(target=server)
+ thread.setDaemon(True)
+ thread.start()
+ addr = q.get()
+
+ # Should not be stuck in an infinite loop.
+ with self.assertRaises((ConnectionResetError, BrokenPipeError)):
+ self.loop.run_until_complete(client(*addr))
+
+ # Clean up the thread. (Only on success; on failure, it may
+ # be stuck in accept().)
+ thread.join()
+
def test___repr__(self):
stream = asyncio.StreamReader(loop=self.loop)
self.assertEqual("<StreamReader>", repr(stream))
diff --git a/Misc/NEWS b/Misc/NEWS
index 260ee3f..01fbf17 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -96,6 +96,8 @@ Core and Builtins
Library
-------
+- Issue #25441: asyncio: Raise error from drain() when socket is closed.
+
- Issue #25411: Improved Unicode support in SMTPHandler through better use of
the email package. Thanks to user simon04 for the patch.