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: add asyncio.Server state machine#131009

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 merge9 commits intopython:main
base:main
Choose a base branch
Loading
fromordinary-jamie:asyncio-server-state-machine

Conversation

ordinary-jamie
Copy link
Contributor

Supersedes#126660 and implements a state machine as suggested by this comment:#126660 (review)

Analysis

image

Addressed issues

This addresses two issues raised inGH-109564:

  1. A transport will first check if a server is closed before attempting to attach. This will prevent raising anAssertionError (changed to aRuntimeError here)
  2. When a connection is accepted byBaseSelectorEventLoop._accept_connection but is not handled inBaseSelectorEventLoop._accept_connection2, then it will fail to correctly construct and raise an"unclosed transport"ResourceWarning on__del__; this will raise an error as it will callServer._detach ->Server._wakeup after close and attempt to access theServer._waiters=None attribute.

These errors can be seen in the new testtest_close_before_transport_attach againstmain:

0:00:00 load avg: 2.05 [1/1] test_asyncio.test_serverError on transport creation for incoming connectionhandle_traceback: Handle created at (most recent call last):  File "cpython/Lib/unittest/async_case.py", line 120, in _callMaybeAsync    return self._asyncioRunner.run(  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 706, in run_until_complete    self.run_forever()  File "cpython/Lib/asyncio/base_events.py", line 677, in run_forever    self._run_once()  File "cpython/Lib/asyncio/base_events.py", line 2029, in _run_once    handle._run()  File "cpython/Lib/asyncio/events.py", line 98, in _run    self._context.run(self._callback, *self._args)  File "cpython/Lib/asyncio/selector_events.py", line 215, in _accept_connection    self.create_task(accept)  File "cpython/Lib/asyncio/base_events.py", line 470, in create_task    task = tasks.Task(coro, loop=self, **kwargs)protocol: <Mock id='4354243424'>Traceback (most recent call last):  File "cpython/Lib/asyncio/selector_events.py", line 234, in _accept_connection2    transport = self._make_socket_transport(        conn, protocol, waiter=waiter, extra=extra,        server=server)  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 941, in __init__    super().__init__(loop, sock, protocol, extra, server)    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "cpython/Lib/asyncio/selector_events.py", line 799, in __init__    self._server._attach(self)    ~~~~~~~~~~~~~~~~~~~~^^^^^^  File "cpython/Lib/asyncio/base_events.py", line 297, in _attach    assert self._sockets is not None           ^^^^^^^^^^^^^^^^^^^^^^^^^AssertionErrorWarning -- Unraisable exceptionException ignored while calling deallocator <function _SelectorTransport.__del__ at 0x1037d1900>:Traceback (most recent call last):  File "cpython/Lib/asyncio/selector_events.py", line 881, 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 iterabletest test_asyncio.test_server failed -- 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 719, in run_until_complete    return future.result()           ~~~~~~~~~~~~~^^  File "cpython/Lib/test/test_asyncio/test_server.py", line 293, in test_close_before_transport_attach    proto.connection_lost.assert_called()    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^  File "cpython/Lib/unittest/mock.py", line 949, in assert_called    raise AssertionError(msg)AssertionError: Expected 'connection_lost' to have been called.0:00:00 load avg: 2.05 [1/1/1] test_asyncio.test_server failed (1 failure)

@ordinary-jamie
Copy link
ContributorAuthor

Hi@1st1 and@ZeroIntensity !

Very sorry for the major delay on this! I had to go on leave for personal reasons.

I've created a new PR for#126660 which implements a state machine as@1st1 suggested.

This change is a bit bigger than what I'm used to (and qualified for 😅 ) so please let me know if there needs to be any changes. There was some areas I wasn't quite sure about, which I will mark up with comments

INITIALIZED = "initialized"
SERVING = "serving"
CLOSED = "closed"
SHUTDOWN = "shutdown"
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Not sure about the nameSHUTDOWN here, but needed something more "definitive" thanclosed.

If we do useSHUTDOWN should I also renameServer._wakeup ->Server._shutdown?

Choose a reason for hiding this comment

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

If we do use SHUTDOWN should I also rename Server._wakeup -> Server._shutdown?

I think that makes sense.

I'm more worried aboutINITIALIZED here; nothing is really initialized, other than the Python object, which isn't particularly useful knowledge. Let's call it something likeNOT_SERVING orNOT_YET_STARTED.

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 in07129e5

self._wakeup()

def _wakeup(self):
match self._state:
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Not sure if PEP 636match-case statements are permitted; can change toif-else statement if needed.

Choose a reason for hiding this comment

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

They're permitted, but it's not particularly useful to usematch here (no pattern is being matched).

ordinary-jamie reacted with thumbs up emoji
Copy link
Contributor

Choose a reason for hiding this comment

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

let's stick to if else as i am not so used to match case.

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 in8e409b7

Comment on lines 797 to 802
if self._server is not None:
self._server._attach(self)
if self._server.is_serving():
self._server._attach(self)
else:
self.abort()
return
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

This is a functional change overmain.. In my head, if a transport was constructed withserver != None, then the transport should not be created/serviced if its corresponding server was shutdown.

Usedtransport.abort overtransport.close to force clear the buffer, since no server exists to service that.

A return statement was added to theelse block to prevent the transport getting tracked inloop._transports

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 is looking a lot better! I'll do a thorough review sometime soon :)

ordinary-jamie reacted with heart emoji
self._wakeup()

def _wakeup(self):
match self._state:

Choose a reason for hiding this comment

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

They're permitted, but it's not particularly useful to usematch here (no pattern is being matched).

ordinary-jamie reacted with thumbs up emoji
@ZeroIntensityZeroIntensity self-requested a reviewMarch 10, 2025 11:06
@kumaraditya303
Copy link
Contributor

I'll try to review by this week.

ordinary-jamie reacted with heart 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.

Sorry for the delay!

if self._state == _ServerState.CLOSED or self._state == _ServerState.SHUTDOWN:
return
else:
self._state = _ServerState.CLOSED

Choose a reason for hiding this comment

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

What happens if something magically goes wrong after this has been set? Is the server still alive while thinking it's closed? It might be worth adding atry/except to undo this.

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.

Ah great point! Although I'm not sure how that would look from a user's point of view. Should we give them a warning to let them know they must try to close the server again? Alternatively, should we expose something like aforce kwarg/flag?

I pushed an initial fix with a simpletry/except to recover the previous state on fail in48a3c0d

INITIALIZED = "initialized"
SERVING = "serving"
CLOSED = "closed"
SHUTDOWN = "shutdown"

Choose a reason for hiding this comment

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

If we do use SHUTDOWN should I also rename Server._wakeup -> Server._shutdown?

I think that makes sense.

I'm more worried aboutINITIALIZED here; nothing is really initialized, other than the Python object, which isn't particularly useful knowledge. Let's call it something likeNOT_SERVING orNOT_YET_STARTED.

ordinary-jamie reacted with thumbs up emoji
# Server._detach() by the last connected client.
return
else:
raise RuntimeError(f"server {self!r} can only wakeup waiters after closing")

Choose a reason for hiding this comment

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

This error message isn't making much sense to me. We'll never reach here "after closing," right? This can only be triggered when the state isSERVING orINITIALIZED.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry this is bad wording, I was trying to say that the Server must be closed before it can be shutdown (i.e. we cannot shutdown from theSERVING state, nor theINITIALIZED/NOT_STARTED state)

Made a fix in:7f3481b

if self._server.is_serving():
self._server._attach(self)
else:
self.abort()

Choose a reason for hiding this comment

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

Why abort the whole transport? This is seemingly a change in behavior.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Ah yup sorry, my misunderstanding! Addressed in5832655

I commentedhere my original thinking:

This is a functional change over main.. In my head, if a transport was constructed with server != None, then the transport should not be created/serviced if its corresponding server was shutdown.

But I re-read the docs forServer.close and see I was thinking about it incorrectly:

https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Server.close

The sockets that represent existing incoming client connections are left open.

@ordinary-jamie
Copy link
ContributorAuthor

Sorry for the delay!

No worries; I'm really grateful for the review!

Sorry for my delay on responding too, had a busy week and just got around to this 🙏

I've tried to address all your comments in each of their threads and linked the respective commits. Please let me know if there's anything else that needs addressing/fixing.

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 really happy to report that I can confirm this fixes the original reproducer reported in#109564, withoutResourceWarning spam.

Thanks,@ordinary-jamie!

ordinary-jamie reacted with heart emoji
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@ZeroIntensityZeroIntensityZeroIntensity approved these changes

@1st11st1Awaiting requested review from 1st11st1 is a code owner

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

3 participants
@ordinary-jamie@kumaraditya303@ZeroIntensity

[8]ページ先頭

©2009-2025 Movatter.jp