-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
GH-109564: add asyncio.Server state machine #131009
Conversation
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the name SHUTDOWN
here, but needed something more "definitive" than closed
.
If we do use SHUTDOWN
should I also rename Server._wakeup
-> Server._shutdown
?
There was a problem hiding this comment.
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 about INITIALIZED
here; nothing is really initialized, other than the Python object, which isn't particularly useful knowledge. Let's call it something like NOT_SERVING
or NOT_YET_STARTED
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 07129e5
Lib/asyncio/base_events.py
Outdated
self._wakeup() | ||
|
||
def _wakeup(self): | ||
match self._state: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if PEP 636 match-case
statements are permitted; can change to if-else
statement if needed.
There was a problem hiding this comment.
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 use match
here (no pattern is being matched).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 8e409b7
Lib/asyncio/selector_events.py
Outdated
if self._server is not None: | ||
self._server._attach(self) | ||
if self._server.is_serving(): | ||
self._server._attach(self) | ||
else: | ||
self.abort() | ||
return |
There was a problem hiding this comment.
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 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.
Used transport.abort
over transport.close
to force clear the buffer, since no server exists to service that.
A return statement was added to the else
block to prevent the transport getting tracked in loop._transports
There was a problem hiding this 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 :)
Lib/asyncio/base_events.py
Outdated
self._wakeup() | ||
|
||
def _wakeup(self): | ||
match self._state: |
There was a problem hiding this comment.
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 use match
here (no pattern is being matched).
I'll try to review by this week. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 a try
/except
to undo this.
There was a problem hiding this comment.
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 a force
kwarg/flag?
I pushed an initial fix with a simple try
/except
to recover the previous state on fail in 48a3c0d
INITIALIZED = "initialized" | ||
SERVING = "serving" | ||
CLOSED = "closed" | ||
SHUTDOWN = "shutdown" |
There was a problem hiding this comment.
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 about INITIALIZED
here; nothing is really initialized, other than the Python object, which isn't particularly useful knowledge. Let's call it something like NOT_SERVING
or NOT_YET_STARTED
.
Lib/asyncio/base_events.py
Outdated
# Server._detach() by the last connected client. | ||
return | ||
else: | ||
raise RuntimeError(f"server {self!r} can only wakeup waiters after closing") |
There was a problem hiding this comment.
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 is SERVING
or INITIALIZED
.
There was a problem hiding this comment.
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 the SERVING
state, nor the INITIALIZED
/NOT_STARTED
state)
Made a fix in: 7f3481b
Lib/asyncio/proactor_events.py
Outdated
if self._server.is_serving(): | ||
self._server._attach(self) | ||
else: | ||
self.abort() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 in 5832655
I commented here 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 for Server.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.
Co-authored-by: Peter Bierma <[email protected]>
Co-authored-by: Peter Bierma <[email protected]>
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. |
There was a problem hiding this 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, without ResourceWarning
spam.
Thanks, @ordinary-jamie!
Supersedes #126660 and implements a state machine as suggested by this comment: #126660 (review)
Analysis
Addressed issues
This addresses two issues raised in GH-109564:
AssertionError
(changed to aRuntimeError
here)BaseSelectorEventLoop._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 test
test_close_before_transport_attach
againstmain
: