Skip to content

Commit 2ef3676

Browse files
authored
[3.12] gh-109538: Catch closed loop runtime error and issue warning (GH-111983) (#112142)
* [3.12] gh-109538: Avoid RuntimeError when StreamWriter is deleted with closed loop (GH-111983) Issue a ResourceWarning instead. (cherry picked from commit e0f5127) gh-109538: Avoid RuntimeError when StreamWriter is deleted with closed loop (#111983) Issue a ResourceWarning instead. Co-authored-by: Hugo van Kemenade <[email protected]> (cherry picked from commit e0f5127) * Fix missing warnings import
1 parent 3f2cdbe commit 2ef3676

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

Lib/asyncio/streams.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import collections
66
import socket
77
import sys
8+
import warnings
89
import weakref
910

1011
if hasattr(socket, 'AF_UNIX'):
@@ -405,8 +406,11 @@ async def start_tls(self, sslcontext, *,
405406

406407
def __del__(self):
407408
if not self._transport.is_closing():
408-
self.close()
409-
409+
if self._loop.is_closed():
410+
warnings.warn("loop is closed", ResourceWarning)
411+
else:
412+
self.close()
413+
warnings.warn(f"unclosed {self!r}", ResourceWarning)
410414

411415
class StreamReader:
412416

Lib/test/test_asyncio/test_streams.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,65 @@ def test_eof_feed_when_closing_writer(self):
10731073

10741074
self.assertEqual(messages, [])
10751075

1076+
def test_unclosed_resource_warnings(self):
1077+
async def inner(httpd):
1078+
rd, wr = await asyncio.open_connection(*httpd.address)
1079+
1080+
wr.write(b'GET / HTTP/1.0\r\n\r\n')
1081+
data = await rd.readline()
1082+
self.assertEqual(data, b'HTTP/1.0 200 OK\r\n')
1083+
data = await rd.read()
1084+
self.assertTrue(data.endswith(b'\r\n\r\nTest message'))
1085+
with self.assertWarns(ResourceWarning) as cm:
1086+
del wr
1087+
gc.collect()
1088+
self.assertEqual(len(cm.warnings), 1)
1089+
self.assertTrue(str(cm.warnings[0].message).startswith("unclosed <StreamWriter"))
1090+
1091+
messages = []
1092+
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
1093+
1094+
with test_utils.run_test_server() as httpd:
1095+
self.loop.run_until_complete(inner(httpd))
1096+
1097+
self.assertEqual(messages, [])
1098+
1099+
def test_loop_is_closed_resource_warnings(self):
1100+
async def inner(httpd):
1101+
rd, wr = await asyncio.open_connection(*httpd.address)
1102+
1103+
wr.write(b'GET / HTTP/1.0\r\n\r\n')
1104+
data = await rd.readline()
1105+
self.assertEqual(data, b'HTTP/1.0 200 OK\r\n')
1106+
data = await rd.read()
1107+
self.assertTrue(data.endswith(b'\r\n\r\nTest message'))
1108+
1109+
# Make "loop is closed" occur first before "del wr" for this test.
1110+
self.loop.stop()
1111+
wr.close()
1112+
while not self.loop.is_closed():
1113+
await asyncio.sleep(0.0)
1114+
1115+
with self.assertWarns(ResourceWarning) as cm:
1116+
del wr
1117+
gc.collect()
1118+
self.assertEqual(len(cm.warnings), 1)
1119+
self.assertEqual("loop is closed", str(cm.warnings[0].message))
1120+
1121+
messages = []
1122+
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
1123+
1124+
with test_utils.run_test_server() as httpd:
1125+
try:
1126+
self.loop.run_until_complete(inner(httpd))
1127+
# This exception is caused by `self.loop.stop()` as expected.
1128+
except RuntimeError:
1129+
pass
1130+
finally:
1131+
gc.collect()
1132+
1133+
self.assertEqual(messages, [])
1134+
10761135
def test_unhandled_exceptions(self) -> None:
10771136
port = socket_helper.find_unused_port()
10781137

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Issue warning message instead of having :class:`RuntimeError` be displayed when event loop has already been closed at :meth:`StreamWriter.__del__`.

0 commit comments

Comments
 (0)