Skip to content

Commit cc5d4e8

Browse files
committed
extmod/asyncio: Support gather of tasks that finish early.
Adds support to asyncio.gather() for the following cases: - one or more sub-tasks finish before the gather starts - one or more sub-tasks raise an exception before the gather starts Signed-off-by: Damien George <damien@micropython.org>
1 parent 16c6bc4 commit cc5d4e8

File tree

3 files changed

+102
-14
lines changed

3 files changed

+102
-14
lines changed

extmod/asyncio/core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ def run_until_complete(main_task=None):
219219
elif t.state is None:
220220
# Task is already finished and nothing await'ed on the task,
221221
# so call the exception handler.
222+
223+
# Save exception raised by the coro for later use.
224+
t.data = exc
225+
226+
# Create exception context and call the exception handler.
222227
_exc_context["exception"] = exc
223228
_exc_context["future"] = t
224229
Loop.call_exception_handler(_exc_context)

extmod/asyncio/funcs.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,26 +86,39 @@ def done(t, er):
8686
# Gather waiting is done, schedule the main gather task.
8787
core._task_queue.push(gather_task)
8888

89+
# Prepare the sub-tasks for the gather.
90+
# The `state` variable counts the number of tasks to wait for.
8991
ts = [core._promote_to_task(aw) for aw in aws]
92+
wait_for_sub_tasks = True
93+
state = 0
9094
for i in range(len(ts)):
91-
if ts[i].state is not True:
92-
# Task is not running, gather not currently supported for this case.
95+
if ts[i].state is True:
96+
# Task is running, register the callback to call when the task is done.
97+
ts[i].state = done
98+
state += 1
99+
elif not ts[i].state:
100+
# Task finished already.
101+
if not isinstance(ts[i].data, StopIteration):
102+
# Task finished by raising an exception.
103+
if not return_exceptions:
104+
# Finish this gather immediately.
105+
wait_for_sub_tasks = False
106+
else:
107+
# Task being waited on, gather not currently supported for this case.
93108
raise RuntimeError("can't gather")
94-
# Register the callback to call when the task is done.
95-
ts[i].state = done
96109

97110
# Set the state for execution of the gather.
98111
gather_task = core.cur_task
99-
state = len(ts)
100112
cancel_all = False
101113

102-
# Wait for the a sub-task to need attention.
103-
gather_task.data = _Remove
104-
try:
105-
yield
106-
except core.CancelledError as er:
107-
cancel_all = True
108-
state = er
114+
# Wait for a sub-task to need attention.
115+
if wait_for_sub_tasks:
116+
gather_task.data = _Remove
117+
try:
118+
yield
119+
except core.CancelledError as er:
120+
cancel_all = True
121+
state = er
109122

110123
# Clean up tasks.
111124
for i in range(len(ts)):
@@ -118,8 +131,13 @@ def done(t, er):
118131
# Sub-task ran to completion, get its return value.
119132
ts[i] = ts[i].data.value
120133
else:
121-
# Sub-task had an exception with return_exceptions==True, so get its exception.
122-
ts[i] = ts[i].data
134+
# Sub-task had an exception.
135+
if return_exceptions:
136+
# Get the sub-task exception to return in the list of return values.
137+
ts[i] = ts[i].data
138+
elif isinstance(state, int):
139+
# Raise the sub-task exception, if there is not already an exception to raise.
140+
state = ts[i].data
123141

124142
# Either this gather was cancelled, or one of the sub-tasks raised an exception with
125143
# return_exceptions==False, so reraise the exception here.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Test asyncio.gather() when a task is already finished before the gather starts.
2+
3+
try:
4+
import asyncio
5+
except ImportError:
6+
print("SKIP")
7+
raise SystemExit
8+
9+
10+
# CPython and MicroPython differ in when they signal (and print) that a task raised an
11+
# uncaught exception. So define an empty custom_handler() to suppress this output.
12+
def custom_handler(loop, context):
13+
pass
14+
15+
16+
async def task_that_finishes_early(id, event, fail):
17+
print("task_that_finishes_early", id)
18+
event.set()
19+
if fail:
20+
raise ValueError("intentional exception ", id)
21+
22+
23+
async def task_that_runs():
24+
for i in range(5):
25+
print("task_that_runs", i)
26+
await asyncio.sleep(0)
27+
28+
29+
async def main(task_fail, return_exceptions):
30+
print("== start", task_fail, return_exceptions)
31+
32+
# Set exception handler to suppress exception output.
33+
loop = asyncio.get_event_loop()
34+
loop.set_exception_handler(custom_handler)
35+
36+
# Create tasks.
37+
event_a = asyncio.Event()
38+
event_b = asyncio.Event()
39+
tasks = [
40+
asyncio.create_task(task_that_finishes_early("a", event_a, task_fail)),
41+
asyncio.create_task(task_that_finishes_early("b", event_b, task_fail)),
42+
asyncio.create_task(task_that_runs()),
43+
]
44+
45+
# Make sure task_that_finishes_early() are both done, before calling gather().
46+
await event_a.wait()
47+
await event_b.wait()
48+
49+
# Gather the tasks.
50+
try:
51+
result = "complete", await asyncio.gather(*tasks, return_exceptions=return_exceptions)
52+
except Exception as er:
53+
result = "exception", er, tasks[-1].done()
54+
55+
# Wait for the final task to finish.
56+
await tasks[-1]
57+
58+
# Print results.
59+
print(result)
60+
61+
62+
asyncio.run(main(False, False))
63+
asyncio.run(main(False, True))
64+
asyncio.run(main(True, False))
65+
asyncio.run(main(True, True))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy