We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency with Threads and asyncio step 26 of 56
Build an Owner for Background Tasks
Build BackgroundTasks, a small owner for background work. It keeps every task
alive until it finishes, and drain() waits for a batch of tasks and raises
the first failure.
background = BackgroundTasks()
background.spawn(send_email("a@example.com"))
background.spawn(send_email("b@example.com"))
background.pending() # 2
await background.drain() # waits for both, and raises if either failed
background.pending() # 0
What to write
spawn(coro) -> asyncio.Task[None]startscorowithasyncio.create_task, adds the task to the setself._tasks, and returns it. Register a done callback that removes the task fromself._taskswhen it finishes. Also append it to a list that records spawn order.pending() -> intcounts spawned tasks wheretask.done()isFalse.len(self._tasks)gives the wrong answer, because the done callback runs a loop turn after the task finishes. For that moment the set still holds a finished task.async def drain() -> Noneworks on one batch, the tasks spawned since the last drain:- If the batch is empty, return at once.
- Wait until every task in the batch has finished, even after one fails.
- Skip cancelled tasks, and call
exception()on every other task. - Remove the batch from
self._tasksand from your list. - If any task failed, raise the exception of the failing task that was spawned first, even if a later one failed sooner.
The driver reads self._tasks, so keep that name.
Edge cases
- A second
drain()with an empty batch returns at once, without raising the old failure again. - Tasks spawned while
drain()waits belong to the next batch. - If
drain()itself is cancelled while it waits, letCancelledErrorpropagate. Keep the batch, with its tasks still running, for a later drain. - You only need to handle one drain at a time.
What the driver reports
solve spawns one worker for each entry in hops. Worker i waits hops[i]
loop turns, adds i to a log, then raises ValueError(f"task {i} failed") if
i is in failing. The driver then runs settle loop turns, calling
gc.collect() before each one, and calls drain().
pending_startispending()right after spawning, andpending_partialispending()after the settle turns.survived_gcis how many workers had added themselves to the log by then. Despite its name, it counts finished workers rather than live task objects.loglists the workers in the order they finished.outcomeis"ok", or the name of the exceptiondrain()raised, andmessageis its text.pending_endandheld_endarepending()andlen(self._tasks)after the drain. Both must be0. The tests check only the set, but clear your list too, or a long-running service keeps every task it ever started.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.