Repository navigation
refactor: non-blocking remote job dispatch (HARN-4) - #61
Merged
Merged
Conversation
Async compute routes now await their bodies on a dedicated bounded pool
(status.run_compute_async / run_task_async, 8 slots) instead of holding a
shared request-threadpool token for the whole response — a remote job's
result() wait used to pin one of ~40 worker tokens for up to 1800 s, so a
few concurrent traces could starve /health and every other sync endpoint.
Routes converted by extracting their sync bodies (_replay_bundle-style
names) and shelling to the pool: /replay/{id}, /replay/{id}/psi/{i},
/replay/{id}/fieldlines/{i}, /eq_surrogate, /surrogate, /export/{id},
/export, POST /compute/benchmark, POST /virtual/{id}. Bounded sync routes
(/compute probe, /compute/cancel, USD export) deliberately stay sync.
Provider selection, depth-1 queue, local fallback, RemoteComputeError
typing, provenance labels, and HARN-1/HARN-2 progress + cancel routes are
unchanged; agent tools now call the sync bodies directly.
New tests: run_task_async semantics (remote success + fallback), router-
level remote coverage incl. fallback labeling (audit gap 1), /health
responsiveness under 4 parallel slow-remote replays (fails on the old
sync dispatch), and cancel/progress SSE through the awaited dispatch.
Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
obvious-autobuild Bot
added a commit
that referenced
this pull request
Oct 12, 2026
Task-mode IA: Replay / Compare / Explore workspaces behind one hash grammar; shared shot-load state machine; anchored what-if as a Replay drawer; command palette; catalog absorbed into Compare with MJ units and catalog naming; a11y tokens; integrated main (#61 #62 #64) + post-audit fixes (CSV-export error surfacing 02c16c3, mobile compare-bar wrap 73b4048). V1-V10 sweep PASS at 73b4048; V8 capability parity PASS (72 controls, art_A29dHIEm). Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human author: Tapan Ganatma Nakkina ([email protected])
Why
A remote compute job used to pin one of FastAPI's shared request-threadpool tokens for its entire life.
SshProvider.result()polls every 0.25 s withtime.sleepfor up to 1800 s (ssh.py:50,323-348), and every compute router was a syncdef— so a few concurrent traces could hold the ~40-token pool anyio gives all sync endpoints and starve/health(and/db/search,/simulate, ...) until the traces finished. HARN-4's fix: async routes that await compute on a dedicated bounded pool, leaving the shared pool to the endpoints that only need it for milliseconds.What
status.run_compute_async(fn)+status.run_task_async(task, inputs)— the dispatch seam. Bodies run on a plain loop-agnosticThreadPoolExecutor(COMPUTE_SLOTS = 8, module-level instatus.py) vialoop.run_in_executor. Chosen over an anyioCapacityLimiterbecause anyio limiters bind to the first event loop that touches them — unsafe across uvicorn/TestClient-portal loops — while threads and queues need no loop. Extra requests queue on the pool's own queue; they never consume shared tokens._replay_psi_slice,_replay_fieldlines_slice,_replay_bundles,_virtual_shot_body) and the route is now a two-line async shell awaiting it on the pool. Converted:/replay/{id},/replay/{id}/psi/{i},/replay/{id}/fieldlines/{i},/eq_surrogate,/surrogate,/export/{id},/export,POST /compute/benchmark,POST /virtual/{id}./compute(TTL-cached, single-flight 10 s probe cap),/compute/cancel(worker POST),POST /replay/{id}/usd(process-lifetime single-flight; sub-second work),/health,/db*.RemoteComputeError→ local re-run → fallback banner state), provenance labels, and HARN-1's progress SSE + HARN-2's cancel routes — they observe the same job recordsrun_taskregisters, from whichever thread it runs on. A cancelled HTTP request cannot stop a running body (a thread can't be interrupted); it completes and its caches/provenance land — the same semantics the abandoned sync-router thread had.agent_tools.pycalledreplay_shot/virtual_shotdirectly as sync code; it now imports the sync bodies (_replay_bundle,_virtual_shot_body).ssh.py's module docstring no longer claims sync routers.asyncio.to_threadinsiderun_task) was rejected — it rewrites the protocol contract HARN-1/HARN-2 just stabilized and risks the depth-1 + fallback invariants this PR must preserve exactly.run_taskitself is untouched; the dispatch wraps it.How to Review
fusionlab/compute/status.py(the dispatch section) — everything else is a shell + extraction.tests/test_nonblocking_compute.py::test_health_stays_responsive_while_replays_wait) is discriminating: with the shared limiter shrunk to 2 tokens it fails against the old sync dispatch (verified by stashing the fix) and passes here.learned_computed_on is None+ recorded reason + local provenance).tests/test_api.py::test_concurrent_requests_share_one_fetchupdated consciously: it called the route function directly; it now calls the body (_replay_bundle) the async route shells to — same three-threads-one-fetch assertion.web/replay.jsis untouched (its polling and SSE consumption are unchanged), so substring tripwires hold.Test Evidence
uv run ruff checkanduv run mypy fusionlabclean.tests/test_nonblocking_compute.py:run_task_asyncremote-success + fallback semantics; router-level remote coverage (remote answer served with ssh provenance; replay bundle labels remote host; dying remote → local bundle with honest labels); the/healthconcurrency property; HARN-2 cancel and HARN-1 progress SSE through the awaited dispatch.web/).Closes #42
🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact