diff --git a/tests/test_dns.py b/tests/test_dns.py index 106ef580..1ff6d267 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -1,4 +1,5 @@ import asyncio +import gc import socket import unittest @@ -238,6 +239,47 @@ def test_getnameinfo_4(self): def test_getnameinfo_5(self): self._test_getnameinfo(('localhost', 8080), 0) + def test_create_connection_cancel_during_getaddrinfo(self): + # Regression test for https://github.com/MagicStack/uvloop/pull/590 + # + # When create_connection() is cancelled (e.g. by a wait_for() + # timeout) while its DNS lookup is still in flight, the lookup + # must be cancelled as well. Otherwise, once the lookup fails, + # nobody retrieves its exception and the loop logs + # "Future exception was never retrieved". + + host = 'no-such-host.invalid' # reserved TLD, see RFC 2606 + + messages = [] + self.loop.set_exception_handler( + lambda loop, ctx: messages.append(ctx)) + + async def run(): + task = self.loop.create_task( + self.loop.create_connection(asyncio.Protocol, host, 80)) + # Let create_connection() start the DNS lookup, then cancel + # it before the lookup result is delivered to the loop. + await asyncio.sleep(0) + task.cancel() + with self.assertRaises(asyncio.CancelledError): + await task + del task + + # Wait for the lookup to fail. The resolver answers requests + # roughly in order, so a second lookup of the same host is a + # good proxy for the first one being done. + try: + await self.loop.getaddrinfo(host, 80) + except socket.gaierror: + pass + else: + raise unittest.SkipTest(f'{host!r} unexpectedly resolves') + await asyncio.sleep(0.1) + gc.collect() + + self.loop.run_until_complete(run()) + self.assertEqual(messages, []) + class Test_UV_DNS(BaseTestDNS, tb.UVTestCase): diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index 802cdb69..6038b3ff 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -1972,7 +1972,19 @@ cdef class Loop: lai = &lai_static if len(fs): - await aio_wait(fs) + try: + await aio_wait(fs) + except asyncio.CancelledError: + # The caller gave up (e.g. a wait_for() timeout) while + # the DNS lookups were still in progress. Cancel them + # so that a lookup failing later doesn't get reported + # as "Future exception was never retrieved". A lookup + # that has already finished can't be cancelled, so mark + # its exception (if any) as retrieved instead. + for fut in fs: + if not fut.cancel() and not fut.cancelled(): + fut.exception() + raise if rai is NULL: ai_remote = f1.result()