diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-13 15:40:50 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-13 15:40:50 +0200 |
| commit | f2d9a026db453899e5bb50f101b1f5f1a9f91ddf (patch) | |
| tree | ed3a33a20f340f18f917a30f1d73877a14a1c3cf /packages/meshbay-node/src/meshbay_node/ui/app.py | |
| parent | d917bb61e42336c38782b22da604d7ca923d484a (diff) | |
| download | meshbay-f2d9a026db453899e5bb50f101b1f5f1a9f91ddf.tar.gz | |
fix: hold every background task, in both codebases
asyncio keeps only a weak reference to a task, so a coroutine started with
`asyncio.ensure_future(...)` whose result is discarded can be collected while
it is still running: the loop logs "Task was destroyed but it is pending!" and
the work simply does not happen. No error reaches the caller, and what is lost
is whatever that coroutine was in the middle of.
The node already had a guard for this, written after an abandoned stream task
lost a transcode slot for good — and it read one file, `webrtc_server.py`,
because that is where the defect was found. Outside that file there were
nineteen sites: the hub's `chat_notify` (a notification for every member of a
group), the indexer's debounce (every real-time index update), eleven in
`daemon.py` including the SIGHUP reload and each enrichment pass, two in
`ops.py`, and five in the loopback API.
`meshbay_common.background.spawn()` is the one door. It holds the task, drops
it when it finishes, and logs what it raised under the coroutine's own name —
an exception in a task nobody awaits was otherwise reported by asyncio at
collection time, out of context or not at all. A peer session's `_spawn` stays
as it is: that one can also *cancel* what it holds, which a module-level holder
cannot, because a session ends and a process does not.
`test_background_tasks.py` walks every package's source and refuses a discarded
handle. It parses rather than greps, so an assignment, a comprehension or an
await is not mistaken for one, and it was checked against a deliberate
reintroduction. A guard that stops at the edge of the file where the bug was
found is a guard against that bug, not against its class.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui/app.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 16 |
1 files changed, 8 insertions, 8 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 4ad3787..de2542e 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -12,12 +12,12 @@ no server-rendered UI: the Node page ships in the desktop client (see `docs/refactor-node-ui.md`). """ -import asyncio import logging from fastapi import FastAPI, HTTPException, Query from fastapi.responses import JSONResponse +from meshbay_common.background import spawn from meshbay_node import __version__, ops from meshbay_node.indexer.indexer import DirectoryIndexer @@ -179,7 +179,7 @@ def create_ui_app(state: dict) -> FastAPI: )) reload_fn = state.get("reload_fn") if reload_fn: - asyncio.ensure_future(reload_fn()) + spawn(reload_fn()) return result @app.post("/api/groups/detach") @@ -190,7 +190,7 @@ def create_ui_app(state: dict) -> FastAPI: )) reload_fn = state.get("reload_fn") if reload_fn: - asyncio.ensure_future(reload_fn()) + spawn(reload_fn()) return result @app.delete("/api/groups/{group_id}/files/{file_id}") @@ -373,7 +373,7 @@ def create_ui_app(state: dict) -> FastAPI: )) reload_fn = state.get("reload_fn") if reload_fn: - asyncio.ensure_future(reload_fn()) + spawn(reload_fn()) return result @app.patch("/api/groups/{group_id}/roots/{root_name}") @@ -385,7 +385,7 @@ def create_ui_app(state: dict) -> FastAPI: )) reload_fn = state.get("reload_fn") if reload_fn: - asyncio.ensure_future(reload_fn()) + spawn(reload_fn()) return result @app.put("/api/groups/{group_id}/roots/{root_name}/eject") @@ -401,11 +401,11 @@ def create_ui_app(state: dict) -> FastAPI: result = await _op(lambda: ops.remove_root(state, group_id, root_name)) reload_fn = state.get("reload_fn") if reload_fn: - asyncio.ensure_future(reload_fn()) + spawn(reload_fn()) return result - # asyncio.ensure_future above schedules the reload (and whatever initial - # scan it triggers) on the daemon's own event loop — it has no link to + # `spawn` above schedules the reload (and whatever initial scan it + # triggers) on the daemon's own event loop — it has no link to # this HTTP request or to any browser tab. Closing the client that made # this call does not cancel it: the scan is the node's own background # work, not something borrowed from the request that started it. |