Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
Appearance settings

gh-109564: AddNone check forasyncio.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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
ordinary-jamie wants to merge8 commits intopython:main
base:main
Choose a base branch
Loading
fromordinary-jamie:asyncio-server-wakeup-none-check

Conversation

ordinary-jamie
Copy link
Contributor

@ordinary-jamieordinary-jamie commentedNov 11, 2024
edited by bedevere-appbot
Loading

Adds aNone check forasyncio.Server._wakeup to prevent crashing when it is called multiple times.

Because the method clearsself._waiters the next call to this method will fail when it attempts to iterate onNone.

This can happen when the publicasyncio.Server.close API and internalasyncio.Server._detach are both called, and this is demonstrated in#109564, when the server is closed after a socket connection is accepted but before it is handled.

Example error of missingNone check

Repro:

# repro.pyimportasyncioimportsysimportsocketasyncdefserver():server=awaitasyncio.start_server(lambda*_:None,host="127.0.0.1",port=4445)awaitasyncio.sleep(0)server.close()awaitserver.wait_closed()defclient():whileTrue:withsocket.socket(socket.AF_INET,socket.SOCK_STREAM)assock:try:sock.connect(("127.0.0.1",4445))exceptIOError:passfinally:sock.close()if__name__=='__main__':iflen(sys.argv)>1andsys.argv[1]=='server':asyncio.run(server())else:client()

Usage:

python repro.py&# May have to call multiple times before observing errorpython repro.py server

Retracted stack trace:

/cpython/Lib/asyncio/selector_events.py:869: ResourceWarning: unclosed transport <_SelectorSocketTransport fd=10>  _warn(f"unclosed transport {self!r}", ResourceWarning, source=self)ResourceWarning: Enable tracemalloc to get the object allocation tracebackException ignored in: <function _SelectorTransport.__del__ at 0x10194b650>Traceback (most recent call last):  File "/cpython/Lib/asyncio/selector_events.py", line 872, in __del__    self._server._detach(self)  File "/cpython/Lib/asyncio/base_events.py", line 303, in _detach    self._wakeup()  File "/cpython/Lib/asyncio/base_events.py", line 308, in _wakeup    for waiter in waiters:TypeError: 'NoneType' object is not iterable

Copy link
Member

@ZeroIntensityZeroIntensity left a comment

Choose a reason for hiding this comment

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

This needs a test, too. Please add one to theserver tests.

ordinary-jamie reacted with eyes emoji
@@ -303,6 +303,8 @@ def _detach(self, transport):
self._wakeup()

def _wakeup(self):
if self._waiters is None:
Copy link
Contributor

@graingertgraingertNov 12, 2024
edited
Loading

Choose a reason for hiding this comment

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

Might as well do the None check on thewaiters local after it's been assigned fromself._waiters

ordinary-jamie reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed ind963237

…f7W0v.rstCo-authored-by: Peter Bierma <zintensitydev@gmail.com>
@@ -303,6 +303,8 @@ def _detach(self, transport):
self._wakeup()

def _wakeup(self):
if self._waiters is None:
return
Copy link
Member

Choose a reason for hiding this comment

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

Please add a detailed comment on how a double call can happen (sort of a shorter version of your main PR message)

ordinary-jamie reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed ind963237

@ordinary-jamie
Copy link
ContributorAuthor

Sorry for the late reply,@ZeroIntensity!

I'm having a hard time creating a unit test for this scenario. Would appreciate some pointers on how to approach a unit test for this.

Below is a snippet that can reproduce the error in a unit-test, however the_accept_connection2 task is not easily accessible from this test. And, even if I did filter it out fromasyncio.tasks.all_tasks() then I can't do much with this since the error is caught and the future result is simply set toNone.

classTestServer2():# ...asyncdeftest_close_race(self):srv=awaitasyncio.start_server(lambda*_:None,socket_helper.HOSTv4,0)# Do not await connection so that we can recreate the race condition by closing# the server after the connection is accepted, but before it is handled.addr=srv.sockets[0].getsockname()conn_task=asyncio.create_task(asyncio.open_connection(addr[0],addr[1]))for_inrange(3):# 1st yield: client socket connect# 2nd yield: selector reader# 3rd yield: server accept connectionawaitasyncio.sleep(0)srv.close()# server accept connection 2awaitasyncio.sleep(0)# Complete the client connection to close the socket_,wr=awaitconn_taskself.addCleanup(wr.close)awaitsrv.wait_closed()

I also considered trying to get a reference to the server transport to handle the accepted connection. However, this fails to construct completely (failing when it attempts to attach to the server that just closed) and the object is destroyed (calling_SelectorTransport.__del__ and then its wakeup which triggers the error this PR is patching).

Additional errors

I discovered an unrelated error on the client side as well;

This occurs when the client attempts to create a transport before the server is able to accept the connection. In our unit test above the timing of when the_accept_connection reader is called varies in relation to when the client starts creating the transport; I'm not sure why at this stage and will investigate in a separate issue.

File "/cpython/Lib/test/test_asyncio/test_server.py", line 301, in test_close_race    _, wr = await conn_task            ^^^^^^^^^^^^^^^  File "/cpython/Lib/asyncio/streams.py", line 48, in open_connection    transport, _ = await loop.create_connection(                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        lambda: protocol, host, port, **kwds)        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "/cpython/Lib/asyncio/base_events.py", line 1192, in create_connection    transport, protocol = await self._create_connection_transport(                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    ...<2 lines>...        ssl_shutdown_timeout=ssl_shutdown_timeout)        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "/cpython/Lib/asyncio/base_events.py", line 1223, in _create_connection_transport    transport = self._make_socket_transport(sock, protocol, waiter)  File "/cpython/Lib/asyncio/selector_events.py", line 72, in _make_socket_transport    return _SelectorSocketTransport(self, sock, protocol, waiter,                                    extra, server)  File "/cpython/Lib/asyncio/selector_events.py", line 946, in __init__    base_events._set_nodelay(self._sock)    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^  File "/cpython/Lib/asyncio/base_events.py", line 196, in _set_nodelay    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^OSError: [Errno 22] Invalid argument

It appears this issue is observed for Mac OS:envoyproxy/envoy#1446

Copy link
Member

@1st11st1 left a comment

Choose a reason for hiding this comment

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

Approving this, that said, I think we should do a broader change - instead of relying on "implicit" notion of the actual state the Server is in by checking some attributes if they are None or not, we should add an explicitself._state enum member with states likeINITIALIZED,SERVING,CLOSED, etc.

cc@ZeroIntensity

@1st1
Copy link
Member

The PR looks good to me, but I'll wait to hear from@ZeroIntensity before I hit the green button.

ZeroIntensity reacted with thumbs up emoji

Copy link
Member

@ZeroIntensityZeroIntensity left a comment

Choose a reason for hiding this comment

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

I'm not sure what's going on with this bug. On Linux, I wasn't able to reproduce the issue given the reproducer in the PR description, and the two reproducer files fromgh-109564 don't cause any errors on my end either. In fact, on this PR, the original reproducer causes a bunch ofResourceWarning spam.

Regarding the test--this PR got lost in notifications, sorry for not replying! I think as long as we havesomething that reproduces the error that should work. But, I ran that test locally and I don't see any error (and again, it instead spamsResourceWarning with this change).

@ordinary-jamie
Copy link
ContributorAuthor

I'm not sure what's going on with this bug. On Linux, I wasn't able to reproduce the issue given the reproducer in the PR description, and the two reproducer files from#109564 don't cause any errors on my end either. In fact, on this PR, the original reproducer causes a bunch of ResourceWarning spam.

Sorry@ZeroIntensity, the only thing this PR addresses is an unraisable exception raised byException ignored in: <function _SelectorTransport.__del__ at ...>

I've manage to write a unit-test that captures this exception in001c286. I also added aconn.close in_accept_connection2 to reduce the number ofResourceWarnings (I initially omitted this because I thought it was unrelated to the PR).

Let me know if the unraisable exception is a trivial matter that doesn't need to be cleaned-up. Happy to close the PR if that's the case.

I think we should do a broader change - instead of relying on "implicit" notion of the actual state the Server is in by checking some attributes if they are None or not, we should add an explicit self._state enum member with states like INITIALIZED, SERVING, CLOSED, etc.

If the team prefers, I'm happy to close this PR and instead work on a more robust change like@1st1 mentioned. LMK!

Comment on lines +278 to +289
# 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
Copy link
ContributorAuthor

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 onif 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.

@ZeroIntensity
Copy link
Member

Thanks for adding a test! I've temporarily marked this asDO-NOT-MERGE while we figure things out :)

I'll look closer at this again tomorrow morning (it's 9:30 PM here). In the meantime, do you have a reproducer for the unraisable exception? Everything I've seen so far in this PR and the issue haven't emitted any warnings or errors on my end. (It's possible that my system is just dodging the race condition by being either too fast or too slow--I'll investigate on a few different machines later.)

If the team prefers, I'm happy to close this PR and instead work on a more robust change like@1st1 mentioned.

Assuming asyncio is just being weird on my end and this PR is really fixing a problem, then we can merge it as-is and backport it, and then put a broader/feature-ish fix on main only.

ordinary-jamie reacted with thumbs up emoji

@ordinary-jamie
Copy link
ContributorAuthor

ordinary-jamie commentedNov 18, 2024
edited
Loading

In the meantime, do you have a reproducer for the unraisable exception? Everything I've seen so far in this PR and the issue haven't emitted any warnings or errors on my end.

Sorry, I don't have anything other than the test onmain and the reproducer in the bug ticket show me the unraisable exception. I will try investigate where the variations in the task scheduling are happening.

Test on main (acbd5c9)
test_close_race (test.test_asyncio.test_server.TestServer2.test_close_race) ... FAIL======================================================================FAIL: test_close_race (test.test_asyncio.test_server.TestServer2.test_close_race)----------------------------------------------------------------------Traceback (most recent call last):  File "/cpython/Lib/asyncio/runners.py", line 127, in run    return self._loop.run_until_complete(task)           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^  File "/cpython/Lib/asyncio/base_events.py", line 720, in run_until_complete    return future.result()           ~~~~~~~~~~~~~^^  File "/cpython/Lib/test/test_asyncio/test_server.py", line 310, in test_close_race    self.assertIsNone(cm.unraisable)    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^AssertionError: UnraisableHookArgs(exc_type=<class 'TypeError'>, exc_value=TypeError("'NoneType' object is not iterable"), exc_traceback=<traceback object at 0x1072792c0>, err_msg=None, object=<function _SelectorTransport.__del__ at 0x10689e450>) is not None----------------------------------------------------------------------Ran 1 test in 0.024sFAILED (failures=1)
Reproducer from bug ticket
<sys>:0: ResourceWarning: unclosed<socket.socket fd=7, family=2, type=1, proto=0, laddr=('127.0.0.1', 4445), raddr=('127.0.0.1', 50638)>ResourceWarning: Enable tracemalloc to get the object allocation traceback<sys>:0: ResourceWarning: unclosed<socket.socket fd=8, family=2, type=1, proto=0, laddr=('127.0.0.1', 4445), raddr=('127.0.0.1', 50639)>ResourceWarning: Enable tracemalloc to get the object allocation traceback/cpython/Lib/asyncio/selector_events.py:869: ResourceWarning: unclosed transport<_SelectorSocketTransport fd=7>  _warn(f"unclosed transport {self!r}", ResourceWarning, source=self)ResourceWarning: Enable tracemalloc to get the object allocation tracebackException ignored in:<function _SelectorTransport.__del__ at 0x10415f710>Traceback (most recent call last):  File"/cpython/Lib/asyncio/selector_events.py", line 872,in __del__    self._server._detach(self)  File"/cpython/Lib/asyncio/base_events.py", line 303,in _detachself._wakeup()  File"/cpython/Lib/asyncio/base_events.py", line 308,in _wakeupforwaiterin waiters:TypeError:'NoneType' object is not iterable/cpython/Lib/asyncio/selector_events.py:869: ResourceWarning: unclosed transport<_SelectorSocketTransport fd=8>  _warn(f"unclosed transport {self!r}", ResourceWarning, source=self)ResourceWarning: Enable tracemalloc to get the object allocation tracebackException ignored in:<function _SelectorTransport.__del__ at 0x10415f710>Traceback (most recent call last):  File"/cpython/Lib/asyncio/selector_events.py", line 872,in __del__    self._server._detach(self)  File"/cpython/Lib/asyncio/base_events.py", line 303,in _detachself._wakeup()  File"/cpython/Lib/asyncio/base_events.py", line 308,in _wakeupforwaiterin waiters:TypeError:'NoneType' object is not iterable

This is my system information:

  • uname -a:Darwin MacBookPro.modem 24.0.0 Darwin Kernel Version 24.0.0: Tue Sep 24 23:39:07 PDT 2024; root:xnu-11215.1.12~1/RELEASE_ARM64_T6000 arm64
  • ./python.exe --version:Python 3.14.0a1+
  • git rev-parse HEAD:acbd5c9c6c62dac34d2ed1a789d36fe61841c16d

Copy link
Member

@ZeroIntensityZeroIntensity left a comment

Choose a reason for hiding this comment

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

Thanks for all your hard work and responsiveness! Since the reproducers don't seem to work reliably on Linux, and this PR actually causes somewhat of a regression in terms ofResourceWarning spam, I'd like to go for a broader change like Yury suggested. (I'm fine with implementing it in this PR instead of opening a new one, FWIW.)

Running the test locally on my end:

importasyncioimportgcasyncdeftest_close_race():srv=awaitasyncio.start_server(lambda*_:None,'0.0.0.0',5000)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 serverloop=asyncio.get_running_loop()srv_reader,_=loop._selector.get_key(srv_sock.fileno()).dataconn_task=asyncio.create_task(asyncio.open_connection(addr[0],addr[1]))for_inrange(10):awaitasyncio.sleep(0)ifsrv_readerinloop._ready:break# Ensure accepted connection task is scheduled by the server reader, but not# completed, before closing the server.awaitasyncio.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.try:_,wr=awaitconn_taskexceptOSErrorase:print(e)# Just to see something for insightawaitsrv.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.gc.collect()asyncio.run(test_close_race())

It seems that I've been wrong about the affected versions. Python 3.12.3 (a little bit outdated but I don't think it matters) outputs nothing, but 3.13.0 and the main branch do indeed cause an error:

Exception ignored in: <function _SelectorTransport.__del__ at 0x7b23f43aca40>Traceback (most recent call last):  File "/usr/lib/python3.13/asyncio/selector_events.py", line 872, in __del__    self._server._detach(self)  File "/usr/lib/python3.13/asyncio/base_events.py", line 304, in _detach    self._wakeup()  File "/usr/lib/python3.13/asyncio/base_events.py", line 309, in _wakeup    for waiter in waiters:TypeError: 'NoneType' object is not iterable

With this PR, I now see this:

/root/cpython/Lib/asyncio/selector_events.py:871: ResourceWarning: unclosed transport <_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>  _warn(f"unclosed transport {self!r}", ResourceWarning, source=self)ResourceWarning: Enable tracemalloc to get the object allocation traceback/root/cpython/Lib/asyncio/streams.py:410: ResourceWarning: unclosed <StreamWriter transport=<_SelectorSocketTransport closing fd=7 read=idle write=<idle, bufsize=0>> reader=<StreamReader eof transport=<_SelectorSocketTransport closing fd=7 read=idle write=<idle, bufsize=0>>>>  warnings.warn(f"unclosed {self!r}", ResourceWarning)

Which is better! But my main concern (and why I'm requesting changes) is because this causesResourceWarnings to be shown where there was no error or warning before (such as with the reproducer in the PR description--that doesn't emit anything on the current main, but itdoes with this change). Please ping me if you need help :)

@1st1 Were you able to get the reproducer inGH-109564 or the PR description to work locally?

ordinary-jamie reacted with thumbs up emoji
@1st1
Copy link
Member

No I haven't tried reproducing this bug. I'm away this week, can try doing that next week.

ZeroIntensity reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@graingertgraingertgraingert left review comments

@ZeroIntensityZeroIntensityZeroIntensity requested changes

@1st11st11st1 approved these changes

@asvetlovasvetlovAwaiting requested review from asvetlovasvetlov is a code owner

@kumaraditya303kumaraditya303Awaiting requested review from kumaraditya303kumaraditya303 is a code owner

@willingcwillingcAwaiting requested review from willingcwillingc is a code owner

Assignees
No one assigned
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

6 participants
@ordinary-jamie@1st1@ZeroIntensity@graingert@serhiy-storchaka@tomasr8

[8]ページ先頭

©2009-2025 Movatter.jp