Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork32k
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
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 nameSHUTDOWN
here, but needed something more "definitive" thanclosed
.
If we do useSHUTDOWN
should I also renameServer._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 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
.
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 in07129e5
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 636match-case
statements are permitted; can change toif-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 usematch
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 in8e409b7
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 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
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 usematch
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!
Uh oh!
There was an error while loading.Please reload this page.
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 atry
/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 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" |
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 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
.
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 isSERVING
orINITIALIZED
.
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 theSERVING
state, nor theINITIALIZED
/NOT_STARTED
state)
Made a fix in:7f3481b
Uh oh!
There was an error while loading.Please reload this page.
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 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.
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
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, withoutResourceWarning
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 inGH-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
: