在某些情况下,asyncio.wait_for
可能会导致套接字泄漏。
简明示例:
async def _create_connection(timeout=60, ssl_obj):
loop = asyncio.get_event_loop()
connector = loop.create_connection(MyEchoClientProtocol, '127.0.0.1', 5000, ssl=ssl_obj)
connector = asyncio.ensure_future(connector)
tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
return tr, pr
async def main():
...
res = await asyncio.wait_for(_acquire_impl(), timeout=timeout, loop=loop)
如果我的理解正确,wait_for
应该以两种方式起作用
我提供了客户端和服务器的源代码,因此可以在您的系统上轻松重现该问题。
可以使用minica
我发现,如果我抓到CancelledError
并将done_callback
添加到内部任务中,就像这样:
try:
tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
return tr, pr
except asyncio.CancelledError as e:
connector.add_done_callback(_done_callback)
raise e
然后在_done_callback
内部,我可以访问传输和协议对象并手动关闭传输以防止泄漏:
客户
import asyncio
import io
import struct
import functools
import ssl as ssl_module
import socket
import collections
import time
import traceback
class MyEchoClientProtocol(asyncio.Protocol):
def connection_made(self, transport):
print('connection_made', transport)
query = 'hello world'
transport.write(query.encode('latin-1'))
def data_received(self, data):
print('data_received', data)
def connection_lost(self, exc):
print('connection_lost', exc)
async def create_connection(ssl_obj, timeout=60):
loop = asyncio.get_event_loop()
connector = loop.create_connection(MyEchoClientProtocol, '127.0.0.1', 5000, ssl=ssl_obj)
connector = asyncio.ensure_future(connector)
tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
return tr, pr
async def main(timeout, ssl_obj):
async def _acquire_impl():
try:
proxy = await create_connection(ssl_obj)
except Exception:
raise
else:
return proxy
res = await asyncio.wait_for(_acquire_impl(), timeout=timeout, loop=loop)
return res
async def test_cancel():
sc = ssl_module.create_default_context(ssl_module.Purpose.SERVER_AUTH, cafile='localhostc.crt')
sc.check_hostname = False
sc.verify_mode = ssl_module.CERT_NONE
for i in range(10): # try 50 times
timeout = 0.003
try:
tr, pr = await main(
timeout=timeout, ssl_obj=sc
)
tr.close()
except asyncio.TimeoutError as e:
print('timeouterror', repr(e))
await asyncio.sleep(600)
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(test_cancel())
loop.run_forever()
loop.close()
服务器
import asyncio
import ssl
async def handle_echo(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print("Received %r from %r" % (message, addr))
print("Send: %r" % message)
writer.write(data)
await writer.drain()
loop = asyncio.get_event_loop()
sc = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
sc.load_cert_chain('localhost.crt', 'localhost.key')
coro = asyncio.start_server(handle_echo, '127.0.0.1', 5000, loop=loop, ssl=sc)
server = loop.run_until_complete(coro)
try:
loop.run_forever()
except KeyboardInterrupt:
pass
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
脚本完成后,我运行netstat -a | grep 5000 | grep ESTAB | awk '{ print $5 }' | sort | uniq -c | grep 5000
,并且有许多未关闭的连接。
输出取决于您的硬件,因此您可能需要调整超时参数
问题
环境 操作系统:Ubuntu 16.04 LTS python:Python 3.6.6或Python 3.7.3