viewer: add worker and cold process timeouts and stream large assets - #305
Conversation
- Add bounded timeout to worker JSON-RPC readline calls so hung workers raise transport errors, get reaped, and cleanly respawn rather than blocking all builds under the worker lock. - Add timeout support and TimeoutExpired error handling to cold cadgen subprocess execution in cadgen_bridge. - Stream static files and assets in 64 KiB chunks via copyfileobj instead of buffering entire files in memory. - Add unit tests for worker timeout recovery, cold subprocess timeouts, and chunked streaming.
|
Sorry for the delayed response, and thanks for the contributions so far. The streaming work is good and stands alone. 64 KiB chunks with an explicit The timeouts have a problem no constant can fix. Real builds run long: a mid-size The signal that separates hung from slow is progress. Builds already write it One architecture note: If you want to chat further, message me on Discord: https://discord.gg/5FGB9DwJYU |
… clock Review rework of the timeout half. A wall-clock cap is the wrong signal for cadgen: real builds run tens of seconds to minutes, their first open pays the full cost, and with no cross-run cache a killed build restarts from zero and dies at the same wall -- so anyone who raises the cap to fit their largest model has switched the guard off. The signal that separates hung from slow is output. worker.py routes all build narration (phase lines, progress chatter, C-level prints) to stderr; the client now always drains that pipe -- which it must anyway, since an undrained pipe fills and blocks the worker mid-write, a self-inflicted hang -- and every line stamps a liveness time. The response wait slides its deadline while narration arrives and declares the worker dead only after a full budget of total silence (VIEWER_CADGEN_IDLE_TIMEOUT, default 300s, <= 0 disables). The cold subprocess gets the same rule by swapping capture_output for pumped pipes. The ping keeps its short bounded budget; a transport fault still reaps and respawns once before failing the request, so the module-global lock can no longer wedge forever on one hung read.
|
Reworked per review (dbeea4f), and the streaming half is split out into #333 so it can land on its own. Timeouts are now an idle watchdog, not a wall clock:
Tests: silent worker → dead at the budget; narrating worker that outlives a wall-clock cap → survives; drainer freshness pinned; same pair for the cold path. Mutation-checked both directions. |
The os.pipe/fdopen harness never delivered the line to the drainer on Windows; a subprocess producer is the shape production uses anyway.
|
The idle-watchdog redesign is right, and I verified the hard part against a real One bug blocks the merge: the rewrite of return {"ok": False, "exitCode": proc.returncode, "error": message}That path is routine, and the caller crashes on it. Reproduction: from server_py import cadgen_bridge
result = cadgen_bridge.run_cadgen_cold(
"cadgen.step_artifact_cli",
["--repo-root", root, "--source-path", target], # missing required --step
root,
)
# result is None; develop returns {"ok": False, "error": "...required: --step..."}On develop the argparse message reaches the client as a build error. On this branch The tests miss it because each one either times out (early return) or prints valid
Everything else is ready; with those two this merges. |
The idle-watchdog rewrite dropped the function's final return, so every subprocess that exited without printing a JSON line -- argparse usage errors are the routine case -- returned None and crashed the caller with AttributeError instead of reporting the build error. The terminal return is back, pinned by a test where the subprocess exits with a usage message and no JSON.
|
Both asks done in 24e9d28:
|
Source ref: develop Source commit: cce04de Target branch: main Previous target: 8f9a7d7 Release base: 8f9a7d7 Previous source: 96675ba Included commits since previous source: cce04de Merge pull request #337 from earthtojake/release/0.4.28 c3f3856 Release 0.4.28 c7e2a7c Merge pull request #305 from warun7/fix/viewer-worker-deadlock-and-timeouts 2b65d4f Merge branch 'develop' into fix/viewer-worker-deadlock-and-timeouts 6f0265d Merge pull request #335 from warun7/fix/skill-remediations-and-coverage 1e4aea1 Merge branch 'develop' into fix/skill-remediations-and-coverage 1f75ced Merge pull request #336 from earthtojake/claude/port-probe-bind 3236a5c viewer: probe port availability by binding, not connecting 99a806f tests: pick viewer-smoke ports outside the ephemeral range 5633b65 tests: call the module-level drain helper directly 788bb5d tests: retire a busy candidate port instead of failing the viewer smoke 7306fbe tests: skip the cadgen probe in the viewer start smoke, surface its output 603e812 tests: resolve npm through PATH for the viewer start smoke on Windows 0b64fa3 skills: point gcode at the real cad export CLI; cover cad-viewer; fix skill deps 24e9d28 viewer: restore run_cadgen_cold's terminal error return 3150457 tests: drive the stderr drainer from a real subprocess pipe dbeea4f viewer: kill the CAD worker and cold subprocess on idleness, not wall clock 06bf1b3 viewer: add worker and cold process timeouts and stream large assets
Summary
Prevents permanent server lockups on hung worker/subprocess calls and prevents high memory usage on large file transfers:
Warm Worker Request Timeout & Deadlock Prevention:
_read_linewith bounded timeout (VIEWER_CADGEN_TIMEOUT, default 300s;VIEWER_CAD_WORKER_PING_TIMEOUT, default 10s) inworker_client.py._WorkerTransportErroris raised, triggering process termination (_reap()) and a single transparent respawn. Subsequent requests no longer block forever onself._lock.Cold Subprocess Execution Timeout:
subprocess.TimeoutExpired) tocadgen_bridge.py:run_cadgen_cold, returning{ok: false, error: "cadgen <module> timed out after <N>s"}rather than hanging indefinitely.Chunked Asset & Static File Streaming:
_serve_static_fileand_serve_assetinserver.pyto stream files in 64 KiB chunks usingshutil.copyfileobjinstead of buffering whole multi-GB files in memory viahandle.read().Testing
viewer/server_py/tests/test_worker_timeouts.py:test_worker_read_line_timeout_raises_transport_errortest_worker_request_timeout_reaps_and_recoverstest_cadgen_bridge_cold_subprocess_timeouttest_stream_file_serves_chunksviewer/server_py/tests/passed.viewer/passed.