Python 3.15 TaskGroup.cancel: stop at the first Sume job done
Python 3.15 adds TaskGroup.cancel. Watch several Sume jobs and stop the other watchers when the first one completes, without cancelling the paid jobs.

To stop watching a group of Sume jobs as soon as one completes, call tg.cancel() inside an asyncio.TaskGroup on Python 3.15, which adds that method. Each watcher polls GET /v1/jobs/{id}/status; the first to see completed records itself and cancels the group, so the other watchers stop polling. On Python 3.14 the same code fails with AttributeError: 'TaskGroup' object has no attribute 'cancel'.
Two things are easy to confuse here. Cancelling the task group stops your polling tasks. It does not cancel the Sume jobs, which keep running and keep billing. That is usually what you want when you submitted several variants and need whichever finishes first, but you should decide on the leftovers deliberately.
What changed in Python 3.15
The Python 3.15 What's New page describes TaskGroup.cancel as allowing early termination of a task group when the goal of the tasks has been achieved or their services are no longer needed. The page was read while 3.15 was still a release candidate, so check the final release before pinning it in production.
| Fact | Value | Source |
|---|---|---|
| TaskGroup.cancel | New in Python 3.15 (documented at 3.15.0rc2) | docs.python.org What's new |
| Job statuses | queued, processing, completed, failed, canceled | docs.sume.com Jobs and results |
| Cancel a job | Only before generation starts; then 409 job_generation_already_started | docs.sume.com Jobs and results |
| Queue headroom | Plan concurrency, plus queue capacity | docs.sume.com Generation admission |
The code
Standard library only, no top-level await. get is blocking urllib, so it runs in a thread through asyncio.to_thread.
import asyncio, json, os, urllib.request
BASE, KEY = os.environ["SUME_BASE_URL"], os.environ["SUME_API_KEY"]
def get(path):
req = urllib.request.Request(BASE + path, headers={"Authorization": f"Bearer {KEY}"})
with urllib.request.urlopen(req, timeout=20) as r:
return json.load(r)["data"]
async def watch(tg, job_id, winner):
while True:
s = await asyncio.to_thread(get, f"/v1/jobs/{job_id}/status")
if s["terminal"]:
if s["sume_status"] == "completed" and not winner:
winner.append(job_id)
tg.cancel() # 3.15: stops the sibling watchers
return
await asyncio.sleep(max(2, s.get("next_poll_after_seconds") or 2))
async def first_completed(job_ids):
winner = []
async with asyncio.TaskGroup() as tg:
for job_id in job_ids:
tg.create_task(watch(tg, job_id, winner))
return winner[0] if winner else None
print(asyncio.run(first_completed(["job_1", "job_2"])))What to do with the other jobs
After the group exits you hold one finished job and the ids of the rest. Three options:
- Keep them: read their results later with the same status loop. The spend is already reserved.
- Cancel queued ones with
POST /v1/jobs/{id}/cancel. This only succeeds before generation starts; a started job answers409 job_generation_already_startedand runs to the end. - Do not submit again. A new paid submit for the same intent bills a second job.
Limits
I ran this on Python 3.15.0rc2 against a local mock that returns the documented status shape, and confirmed the failure on 3.14.7. Cancelling a task group interrupts a thread-based urllib call only after the thread returns, so the last in-flight request still completes. With many jobs, a status call per job per interval adds up against your read budget; the list endpoint with a status filter is cheaper for large fleets.
Sources
Related posts
More in Developers
- Python asyncio.timeout around a Sume job poll: a hard budget
Wrap a Sume status loop in asyncio.timeout so it stops at a fixed budget and returns still_running, leaving the job alone. A short version, run against a mock.
- Python httpx and asyncio: submit and poll a Sume image job
A runnable Python recipe: submit a Sume image job in async mode with an Idempotency-Key, poll with httpx and asyncio, honor retry-after, fetch the artifacts.
- Python Sume webhook handler: stdlib verify and SQLite dedupe
A stdlib Python handler for Sume webhooks: verify that accepts rotation, then INSERT OR IGNORE on job_id so a retry runs once. Tested on 3.14 and 3.15.
- Verify a Sume webhook in Rails: raw_post, skip_forgery_protection
A Rails controller that verifies Sume's sume-v1 HMAC over the raw body, accepts the rotation header, refuses an empty secret and skips CSRF for that route only.
Written by Sume