TaskGroup and structured concurrency
A task started with create_task can outlive the code that started it, and if nobody awaits it, its exception goes unnoticed. asyncio.TaskGroup ties tasks to a block: the block does not end until every task in it has finished.
import asyncio
async def double(n):
await asyncio.sleep(0.01)
return n * 2
async def main():
async with asyncio.TaskGroup() as tg:
a = tg.create_task(double(1))
b = tg.create_task(double(2))
print(a.result(), b.result()) # 2 4
asyncio.run(main())If any task fails, the group cancels the others, waits for them, and raises an ExceptionGroup that holds every error. Handle it with except*, which matches error types inside the group:
import asyncio
async def fail():
raise ValueError("bad row")
async def run():
async with asyncio.TaskGroup() as tg:
tg.create_task(fail())
try:
asyncio.run(run())
except* ValueError as group:
print(group.exceptions)Your task
Write async def run_all(jobs) that runs every job in jobs, a dict from names to async functions with no arguments, at once in an asyncio.TaskGroup, and returns a dict from each name to its result. If a job fails, the others must be cancelled and the group's ExceptionGroup must reach the caller.
The explanation opens here once every check passes.