Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

gh-109564: Add None check for asyncio.Server._wakeup #126660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions 9 Lib/asyncio/base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ def _detach(self, transport):

def _wakeup(self):
waiters = self._waiters
if waiters is None:
# gh109564: the wakeup method has two possible call-sites, through an
# explicit call to the server's close method, or indirectly after the last
# client disconnects and the corresponding transport detaches from the
# server. These two can be in a race-condition if the server closes between
# `BaseSelectorEventLoop._accept_connection` and
# `BaseSelectorEventLoop._accept_connection2`; in this scenario we must
# check the wakeup call hasn't already set the server waiters to None.
return
self._waiters = None
for waiter in waiters:
if not waiter.done():
Expand Down
2 changes: 2 additions & 0 deletions 2 Lib/asyncio/selector_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,10 @@ async def _accept_connection2(
# It's now up to the protocol to handle the connection.

except (SystemExit, KeyboardInterrupt):
conn.close()
raise
except BaseException as exc:
conn.close()
if self._debug:
context = {
'message':
Expand Down
46 changes: 46 additions & 0 deletions 46 Lib/test/test_asyncio/test_server.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import asyncio
import gc
import os
import socket
import time
import threading
import unittest

from test import support
from test.support import socket_helper
from test.support import warnings_helper
from test.test_asyncio import utils as test_utils
from test.test_asyncio import functional as func_tests

Expand Down Expand Up @@ -266,6 +269,49 @@ async def serve(rd, wr):
await asyncio.sleep(0)
self.assertTrue(task.done())

async def test_close_race(self):

srv = await asyncio.start_server(lambda *_: None, socket_helper.HOSTv4, 0)
srv_sock = srv.sockets[0]
addr = srv_sock.getsockname()

# When the server is closed before a connection is handled but after the
# connection is accepted, then a race-condition exists between the handler
# transport and the server, both which will attempt to wakeup the server to set
# any server waiters. We can recreate race-condition by opening a connection and
# waiting for the server reader callback before closing the server
loop = asyncio.get_running_loop()
srv_reader, _ = loop._selector.get_key(srv_sock.fileno()).data
conn_task = asyncio.create_task(asyncio.open_connection(addr[0], addr[1]))
for _ in range(10):
await asyncio.sleep(0)
if srv_reader in loop._ready:
break
Comment on lines +278 to +289
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, forgot about Windows here.

If I set this to a fixed 3 yields without relying on if srv_reader in loop._ready, then the test is flaky (sometimes the error won't be observed and the test will have a false-positive)

The first yield is to let the client connection to start the connection, the second for reader to be selected, and the last for the server to accept the connection. But, I'm not sure why the accept connection tasks sometimes happens much later, and hence the false-positive.


# Ensure accepted connection task is scheduled by the server reader, but not
# completed, before closing the server.
await asyncio.sleep(0)
srv.close()

# Complete the client connection to close the socket. Suppress errors in the
# handler transport due to failing to attach to closed server.
with support.captured_stderr():
try:
_, wr = await conn_task
self.addCleanup(wr.close)
except OSError:
pass

await srv.wait_closed()

# Verify the handler transport does not raise an error due to multiple calls to
# the server wakeup. Suppress expected ResourceWarnings from the handler
# transport failing to attach to the closed server.
with warnings_helper.check_warnings(("unclosed transport", ResourceWarning)), \
support.catch_unraisable_exception() as cm:
support.gc_collect()
self.assertIsNone(cm.unraisable)


# Test the various corner cases of Unix server socket removal
class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix race condition in :meth:`asyncio.Server.close`. Patch by Jamie Phan.
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.