Fix reflex run hanging after fatal error in the frontend thread - #6990
Conversation
A SystemExit (e.g. the node minimum-version check failing on the npm path) raised inside the run_frontend task only killed its worker thread: run_concurrently_context checked its futures after the with-body, but in dev mode the main thread blocks indefinitely serving the backend, so the error was swallowed and reflex run never exited. run_concurrently_context now interrupts the main thread when a task fails while the with-body is executing - a real SIGINT via pthread_kill on POSIX so blocking C calls return with EINTR, interrupt_main on Windows - and re-raises the task's own exception so the CLI exits promptly with the original error. A task that fails before the body starts is surfaced without entering the body, and a genuine Ctrl+C with no failed task still propagates unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
Greptile SummaryThis PR updates concurrent development-server startup so a fatal frontend worker failure interrupts the blocking main thread and is re-raised promptly.
Confidence Score: 4/5The PR appears safe to merge, with only a non-blocking clarity issue in the new timing assertion. The changed concurrency path has focused coverage for worker failures and genuine interrupts; the only accepted concern is that the regression test leaves its five-second threshold unexplained. Files Needing Attention: tests/units/utils/test_processes.py
|
| Filename | Overview |
|---|---|
| reflex/utils/processes.py | Adds synchronized failed-task detection and main-thread interruption while preserving genuine keyboard interrupts. |
| tests/units/utils/test_processes.py | Adds focused concurrency regression tests; the elapsed-time assertion should explain its five-second threshold. |
| news/+node-error-hang.bugfix.md | Documents the corrected CLI behavior when a fatal frontend worker error occurs. |
Reviews (1): Last reviewed commit: "Fix reflex run hanging after fatal error..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
All Submissions:
Type of change
Changes To Core Features:
Defect
FINDING-011 (MEDIUM) from the 0.9.9a1 pre-release testing round:
reflex runhangs indefinitely after a fatal node-version error on the npm path. With an outdated node first onPATHandREFLEX_USE_NPM=1,js_runtimes.validate_frontend_dependenciescorrectly prints "Reflex requires node version 22.22.0 or higher..." and raisesSystemExit(1)— but that happens inside therun_frontendtask submitted to aThreadPoolExecutorbyprocesses.run_concurrently_context. TheSystemExitonly kills the worker thread: the futures are checked after the with-body, and in dev mode the main thread blocks forever serving the backend (granianserve()), so the error is swallowed and the process never exits (it kept the backend serving with no frontend until killed by timeout). The same swallowing applies to any fatal error raised by an executor task consumed through this helper.Fix
run_concurrently_contextnow surfaces task failures to the main thread:signal.pthread_kill(main_thread, SIGINT)) so a blocking C call (lock wait, server loop) returns withEINTRand runs the SIGINT handler —_thread.interrupt_main()only sets a pending flag there and never wakes a blocked main thread. On Windowsinterrupt_main()is the right mechanism (it sets the SIGINT event that blocking waits monitor).as_completedcheck re-raise the task's exception, or raisesKeyboardInterruptin the body, which the context manager now catches and converts back into the failed task's own exception. A genuine Ctrl+C with no failed task propagates unchanged.run_concurrently(empty body) keeps its existing error semantics.Test plan
tests/units/utils/test_processes.py::test_run_concurrently_context_unblocks_main_thread_on_task_failure: a task raisingSystemExitwhile the with-body blocks on anEvent.wait(timeout=10)must propagate promptly. Against unfixed main it fails (the body blocks the full 10s before the error surfaces — the unit-scale version of the infinite hang); with the fix it passes in milliseconds. Two companion tests cover genuineKeyboardInterruptpassthrough andrun_concurrentlyexception propagation.run_concurrentlypath): no hang, no escapedKeyboardInterrupt.nodeshim printingv22.12.0,REFLEX_USE_NPM=1 reflex run— unfixed main runs until killed (timeoutexit 124 after 90s); with the fix the process prints the node version error, "Reflex app stopped.", and exits code 1 in ~5s.uv run ruff check .anduv run ruff format .clean;uv run pyright reflex tests0 errors;uv run pytest tests/units/utilspasses (629 passed; the only 2 failures are pre-existing on clean main in this sandbox, which lacks IPv6).🤖 Generated with Claude Code
https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
Generated by Claude Code