openlang

asyncio.TaskGroup when one fetch fails

Concurrency comparisons between languages usually start with syntax. I'd start with what the calculation needs when something fails.

A small Python application fetches measurements and weights concurrently and combines them into one score. One fetch raises an exception while the other is still running.

First stop. Does one failed fetch make the whole calculation unusable? If so, asyncio.TaskGroup fits. When a member fails with anything other than CancelledError, the group cancels the remaining members and waits for its tasks before its exit finishes. One thing is easy to miss. Calling a coroutine function on its own schedules nothing, so the fetches have to become tasks in the group.

Second stop, a different decision. Both tasks return normally, but the weights are unsuitable. Collection has finished. Now let's inspect what came back. Reject the inputs, request replacements, or produce a partial result? TaskGroup has no opinion on that.

Third stop, caller cancellation. Fetch cleanup should let CancelledError propagate, since swallowing it can break the group's cancellation behavior.

For this application the answer is TaskGroup, because either failure ends the combined request and the scope has to wait for its members. Validating the returned data comes next, before any calculation. That gives a comparison with other languages something concrete to preserve.

The conversation continues 4

Follow the reasoning
  1. Permalink

    Small experiment on your second stop. Keep the measurements as they are and let the weight fetch return [] instead of raising. TaskGroup sees two normal returns. No cancellation, nothing propagates, and whatever the combining step does with an empty list is what you get.

    That step gets fun fast if the score is a weighted mean. In Python the division by an empty weight sum raises ZeroDivisionError. In Julia the same arithmetic on empty Float64 vectors quietly gives NaN.

    Now empty the measurements too. Two empty collections might be a perfectly legitimate empty calculation, or a required input that went missing without a sound. Nothing in the task group can tell those apart. So pin down what empty means for each input first, and only then decide which unsuitable results deserve to be raised as failures.

  2. Permalink

    Two fetches. That's the whole population, so a worker pool here would be ceremony.

    The part worth tracing is the fetch that's still running. Its sibling raises, the group cancels it, and then the group waits. Cancelling is a request. The fetch might be halfway through a socket read or a cleanup block when it lands, and it's done only when the task actually finishes. Go has the same shape with errgroup.WithContext. The context gets cancelled on the first error, and Wait still blocks until every goroutine returns.

    So the requirement I care about is one line. The combined request doesn't leave its scope while any member is still running. If somebody wants the measurements that did arrive, write down whether they can be used after the group has reported failure. Right now nothing says.

  3. Permalink

    Same word, different handoff. In Lua 5.4, coroutine.create gives you a coroutine that hasn't run a single line yet. The host calls resume. It runs until it yields. Then it sits there until somebody resumes it again.

    Who resumes it next? Whatever code the host wrote. Nothing waits for a sibling or cancels one for you. So check each language's facility against what TaskGroup does in your calculation, waiting for its members and cancelling on failure, and don't let the shared word carry that over.

Other sides of the same question

  1. Swapping the config language won't fix drift

    The usual pitch for replacing HCL files with a general-purpose language is loops and functions. Sometimes that's a fair point…

    Replies 4hcl