-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Backport py3.11 asyncio's taskgroups. #8791
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: master
Are you sure you want to change the base?
Changes from 13 commits
441d54a
51f2494
ff9c669
fb5d591
e1c7829
4beafa4
2440fe4
8dc64b0
d0eed1e
1740d74
c4947ce
36ae50a
e82d9b7
d95f4b2
4e56794
12defaf
ce9ea8f
d1bdd99
e022a1c
8aadd43
1391bcf
b61cc3e
59eb4ba
f4f510b
6d448ed
32608f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
"funcs.py", | ||
"lock.py", | ||
"stream.py", | ||
"taskgroup.py", | ||
), | ||
base_path="..", | ||
opt=3, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,8 @@ async def __aenter__(self): | |
return self | ||
|
||
async def __aexit__(self, exc_type, exc, tb): | ||
await self.close() | ||
self.s.close() | ||
pass | ||
smurfix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def close(self): | ||
pass | ||
|
@@ -152,7 +153,7 @@ async def _serve(self, s, cb): | |
|
||
|
||
# Helper function to start a TCP stream server, running as a new task | ||
# TODO could use an accept-callback on socket read activity instead of creating a task | ||
# DOES NOT USE TASKGROUPS. Use run_server instead | ||
async def start_server(cb, host, port, backlog=5): | ||
import socket | ||
|
||
|
@@ -170,6 +171,47 @@ async def start_server(cb, host, port, backlog=5): | |
return srv | ||
|
||
|
||
# Helper task to run a TCP stream server. | ||
# Callbacks (i.e. connection handlers) may run in a different taskgroup. | ||
async def run_server(cb, host, port, backlog=5, taskgroup=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this function doesn't look like it's equivalent to any cpython function is it? Does cpython handle start_server differently when it's run from within a TaskGroup? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cpython doesn't have a taskgroup-aware way to run a server, it simply starts a new task for each client (for some value of "simply", its code is rather complex for historical reasons related to their protocol/transport split – in hindsight, that was a rather bad design decision).
Yes this is a MicroPython extension and thus something that we don't really want to do, but CPython doesn't yet have an equivalent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. … which reminds me that the whole taskgroup thing needs documentation. Duh. Working on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So on cpython, if you run start _server in a TaskGroup it'll run, but not get cleaned up if the TaskGroup exits? Do you know if this has raised as an issue for cpython? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no idea. I plan to investigate that (and submit a PR if necessary). The "problem" is that most likely the issue hasn't come up yet because people who use taskgroups tend to do that with anyio or even trio. Both have their own wrappers to address this problem, which unfortunately are not a good fit for micropython. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you end up bringing this up with CPython, @smurfix? It's probably the biggest remaining hurdle - to try and keep asyncio module contents close to CPython. I guess the alternative is to hide this in a different module somewhere (maybe in micropython-lib). However if CPython were up for solving this as well, that would be ideal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No I didn't yet, no free time to shepherd a feature like that, esp. since taskgroups still are an under-utilized (and poorly understood, among old-time asyncio adherents) feature. I'll drop it for now and move the code to |
||
import socket | ||
|
||
# Create and bind server socket. | ||
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking! | ||
s = socket.socket() | ||
s.setblocking(False) | ||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
s.bind(host[-1]) | ||
s.listen(backlog) | ||
|
||
if taskgroup is None: | ||
from . import TaskGroup | ||
|
||
async with TaskGroup() as tg: | ||
await _run_server(tg, s, cb) | ||
else: | ||
await _run_server(taskgroup, s, cb) | ||
|
||
|
||
async def _run_server(tg, s, cb): | ||
while True: | ||
try: | ||
yield core._io_queue.queue_read(s) | ||
except core.CancelledError: | ||
# Shutdown server | ||
s.close() | ||
return | ||
try: | ||
s2, addr = s.accept() | ||
except Exception: | ||
# Ignore a failed accept | ||
continue | ||
|
||
s2.setblocking(False) | ||
s2s = Stream(s2, {"peername": addr}) | ||
tg.create_task(cb(s2s, s2s)) | ||
|
||
|
||
################################################################################ | ||
# Legacy uasyncio compatibility | ||
|
||
|
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.
Could you give some details on when / how the singleton is already in use?
It seems like it'll be confusing that sometimes sleep will allocate and sometimes it won't.
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.
Of course. Simply start two tasks and let both of them sleep at the same time.
I don't think it's confusing, this is entirely internal to
sleep_ms
.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 very regularly have a large number of tasks with each of them sleeping for different lengths of time... I haven't noticed any issue.
What problem / symptoms do you see?
Uh oh!
There was an error while loading. Please reload this page.
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 initially found this bug via test 12 in
tests/extmod/asyncio_taskgroup.py
, according to my notes, though I don't remember details.Test 12 no longer errors out when the change we're discussing here is removed, but adding a
print
statement to theif
clause does show that the problem is still triggered by the test, and that it does cause the sleep time of some other task to be modified and/or ignored.The original code depends on the scheduler to iterate the "singleton" object that's yielded by
sleep_ms
immediately, i.e.. before any other task which also happens to be scheduled callssleep_ms
. This condition seems to hold most of the time, but not always.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, this is quite interesting. I had wondered how the Singleton could ever work to provide multiple different sleep function usages.... I can see how multiple tasks started back to back (like in a new taskgroup setup) would be more likely to trigger this situation of multiple definitions before the task loop makes it back around to the start to process the sleeps.
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 definitely needs to be understood better. It should be that the scheduler always iterates the singleton immediately. If that's no longer the case then we need to understand why and document it.
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 can only assume that the "always iterate immediately" assumption was never true; we only thought it was. After all, building a test that actually fails when it's not is a nontrivial exercise, hence the
print("XXX")
patch that demonstrates the issue.