Skip to content

Commit 7b56550

Browse files
committed
handle: When warning about an unclosed recource, print its source tb
1 parent f79d4d7 commit 7b56550

File tree

4 files changed

+77
-9
lines changed

4 files changed

+77
-9
lines changed

tests/test_tcp.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio
2-
import logging
2+
import gc
33
import socket
44
import unittest.mock
55
import uvloop
@@ -584,6 +584,42 @@ async def run():
584584

585585
self.loop.run_until_complete(run())
586586

587+
def test_tcp_handle_unclosed_gc(self):
588+
fut = self.loop.create_future()
589+
590+
async def server(reader, writer):
591+
writer.transport.abort()
592+
fut.set_result(True)
593+
594+
async def run():
595+
addr = srv.sockets[0].getsockname()
596+
await asyncio.open_connection(*addr, loop=self.loop)
597+
await fut
598+
srv.close()
599+
await srv.wait_closed()
600+
601+
srv = self.loop.run_until_complete(asyncio.start_server(
602+
server,
603+
'127.0.0.1', 0,
604+
family=socket.AF_INET,
605+
loop=self.loop))
606+
607+
if self.loop.get_debug():
608+
rx = r'unclosed resource <TCP.*; ' \
609+
r'object created at(.|\n)*test_tcp_handle_unclosed_gc'
610+
else:
611+
rx = r'unclosed resource <TCP.*'
612+
613+
with self.assertWarnsRegex(ResourceWarning, rx):
614+
self.loop.create_task(run())
615+
self.loop.run_until_complete(srv.wait_closed())
616+
gc.collect()
617+
self.loop.run_until_complete(asyncio.sleep(0.1))
618+
619+
# Since one TCPTransport handle wasn't closed correctly,
620+
# we need to disable this check:
621+
self.skip_unclosed_handles_check()
622+
587623

588624
class Test_AIO_TCP(_TestTCP, tb.AIOTestCase):
589625
pass

uvloop/_testbase.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,14 @@ def is_asyncio_loop(self):
6969
def setUp(self):
7070
self.loop = self.new_loop()
7171
asyncio.set_event_loop(self.loop)
72+
self._check_unclosed_resources_in_debug = True
7273

7374
def tearDown(self):
7475
self.loop.close()
7576

77+
if not self._check_unclosed_resources_in_debug:
78+
return
79+
7680
# GC to show any resource warnings as the test completes
7781
gc.collect()
7882
gc.collect()
@@ -83,24 +87,37 @@ def tearDown(self):
8387
gc.collect()
8488
gc.collect()
8589

86-
self.assertEqual(self.loop._debug_cb_handles_count, 0)
87-
self.assertEqual(self.loop._debug_cb_timer_handles_count, 0)
88-
self.assertEqual(self.loop._debug_stream_write_ctx_cnt, 0)
90+
self.assertEqual(
91+
self.loop._debug_cb_handles_count, 0,
92+
'not all callbacks (call_soon) are GCed')
93+
94+
self.assertEqual(
95+
self.loop._debug_cb_timer_handles_count, 0,
96+
'not all timer callbacks (call_later) are GCed')
97+
98+
self.assertEqual(
99+
self.loop._debug_stream_write_ctx_cnt, 0,
100+
'not all stream write contexts are GCed')
89101

90102
for h_name, h_cnt in self.loop._debug_handles_current.items():
91103
with self.subTest('Alive handle after test',
92104
handle_name=h_name):
93-
self.assertEqual(h_cnt, 0)
105+
self.assertEqual(
106+
h_cnt, 0,
107+
'alive {} after test'.format(h_name))
94108

95109
for h_name, h_cnt in self.loop._debug_handles_total.items():
96110
with self.subTest('Total/closed handles',
97111
handle_name=h_name):
98112
self.assertEqual(
99-
h_cnt, self.loop._debug_handles_closed[h_name])
113+
h_cnt, self.loop._debug_handles_closed[h_name],
114+
'total != closed for {}'.format(h_name))
100115

101116
asyncio.set_event_loop(None)
102117
self.loop = None
103118

119+
def skip_unclosed_handles_check(self):
120+
self._check_unclosed_resources_in_debug = False
104121

105122
def _cert_fullname(name):
106123
fullname = os.path.join(

uvloop/handles/handle.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ cdef class UVHandle:
44
bint _closed
55
bint _inited
66
Loop _loop
7+
readonly _source_traceback
78

89
# All "inline" methods are final
910

@@ -17,6 +18,8 @@ cdef class UVHandle:
1718
cdef _error(self, exc, throw)
1819
cdef _fatal_error(self, exc, throw, reason=?)
1920

21+
cdef _warn_unclosed(self)
22+
2023
cdef inline _free(self)
2124
cdef _close(self)
2225

uvloop/handles/handle.pyx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ cdef class UVHandle:
1717
self._inited = 0
1818
self._handle = NULL
1919
self._loop = None
20+
self._source_traceback = None
2021

2122
def __init__(self):
2223
raise TypeError(
@@ -60,8 +61,7 @@ cdef class UVHandle:
6061
self._handle.data = NULL
6162
uv.uv_close(self._handle, __uv_close_handle_cb) # void; no errors
6263
self._handle = NULL
63-
warnings_warn("unclosed resource {!r}".format(self),
64-
ResourceWarning)
64+
self._warn_unclosed()
6565
else:
6666
# The handle was allocated, but not initialized
6767
self._closed = 1
@@ -71,6 +71,16 @@ cdef class UVHandle:
7171
PyMem_Free(self._handle)
7272
self._handle = NULL
7373

74+
cdef _warn_unclosed(self):
75+
if self._source_traceback is not None:
76+
tb = ''.join(tb_format_list(self._source_traceback))
77+
tb = 'object created at (most recent call last):\n{}'.format(
78+
tb.rstrip())
79+
msg = 'unclosed resource {!r}; {}'.format(self, tb)
80+
else:
81+
msg = 'unclosed resource {!r}'.format(self)
82+
warnings_warn(msg, ResourceWarning)
83+
7484
cdef inline _abort_init(self):
7585
if self._handle is not NULL:
7686
self._free()
@@ -89,6 +99,8 @@ cdef class UVHandle:
8999
cdef inline _finish_init(self):
90100
self._inited = 1
91101
self._handle.data = <void*>self
102+
if self._loop._debug:
103+
self._source_traceback = tb_extract_stack(sys_getframe(0))
92104

93105
cdef inline _start_init(self, Loop loop):
94106
IF DEBUG:
@@ -322,5 +334,5 @@ cdef void __uv_walk_close_all_handles_cb(uv.uv_handle_t* handle, void* arg) with
322334

323335
h = <UVHandle>handle.data
324336
if not h._closed:
325-
warnings_warn("unclosed resource {!r}".format(h), ResourceWarning)
337+
h._warn_unclosed()
326338
h._close()

0 commit comments

Comments
 (0)