Refactor AppHarness to use reflex run subprocess instead of threads#5555
Conversation
- Replace _start_backend and _start_frontend thread methods with _start_subprocess - Use new_process from reflex/utils/processes.py for subprocess management - Implement console output parsing to detect server startup - Update port management using handle_port for dynamic port assignment - Maintain API compatibility for state management methods - Update process cleanup for proper subprocess termination - Add compatibility attributes and methods for existing tests - Fix type annotations to resolve pyright variable override errors - Add port parameter to _start_backend for test compatibility - Fix docstring documentation for darglint compliance Fixes ENG-6317 Co-Authored-By: masen@reflex.dev <masen@reflex.dev>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
PR Summary
Major architectural change to reflex/testing.py replacing the thread-based AppHarness implementation with a subprocess-based approach using 'reflex run' command.
- Eliminates direct uvicorn server and frontend thread management in favor of unified subprocess control
- Introduces port management for both frontend and backend servers through reflex subprocess
- Removes custom TCP server implementation for static file serving
- Adds process lifecycle management with proper cleanup handling
- Maintains backward compatibility through deprecated thread-related fields
1 file reviewed, 2 comments
Edit PR Review Bot Settings | Greptile
CodSpeed Performance ReportMerging #5555 will not alter performanceComparing Summary
|
- Add timeout and non-blocking stdout reading in _wait_for_servers - Simplify process cleanup logic in stop() method - Add Windows compatibility for subprocess stdout handling - Use daemon threads to avoid hanging on cleanup - Add proper error handling for process termination Fixes CI timeout issues in integration tests Co-Authored-By: masen@reflex.dev <masen@reflex.dev>
- Change from subprocess.Popen with argument list to shell=True with command string - Debug testing confirmed shell=True successfully captures both frontend and backend messages - Add fallback backend detection using port checking for additional robustness - Resolves core subprocess stdout capture issue causing all 12 integration test failures - Remove all debugging print statements and fix ruff-check issues Based on comprehensive subprocess behavior testing that showed: - Basic subprocess approach: Frontend=False, Backend=False - Shell=True approach: Frontend=True, Backend=True Co-Authored-By: masen@reflex.dev <masen@reflex.dev>
* enable logging to stdout * allow prod mode to work * better port selection algorithm * remove useless code * cleaner teardown * test fixups for compatibility
- Add proper docstring to nested consume_output function in _wait_for_servers method - Resolves darglint compliance issue for subprocess implementation - All other pre-commit hooks (ruff-format, ruff-check, codespell, prettier) pass Co-Authored-By: masen@reflex.dev <masen@reflex.dev>
- Resolved merge conflict in reflex/testing.py - Added proper parameter documentation to _start_subprocess and _wait_for_servers methods - Fixed docstring documentation for set_state and modify_state to match actual NotImplementedError behavior - Updated get_state docstring to only document AssertionError (matches actual code) - All darglint checks now pass while preserving subprocess implementation improvements - Applied ruff formatting fixes Co-Authored-By: masen@reflex.dev <masen@reflex.dev>
Allow writing the console.error messages to a separate log file. Mainly used for testing or monitoring for errors.
Remove complex loops and threads from subprocess management in favor of log files. Use a separate error log to check if any errors should be raised in the test process. Raise error if reflex exits on its own with a non-zero code
it throws an error with the source map builder, and OOM without node 22
rx.config is no longer the local app config, since it runs in a subprocess
Avoid issue where we try to restart reflex, but the port is still taken by the process we just killed. To properly test the reconnect logic, the backend needs to come back up on the same port as before.
| # Bring the backend back up once the port is free'd. | ||
| if result := AppHarness._poll_for( | ||
| lambda: processes.handle_port( | ||
| "backend", connection_banner.backend_port or 0, auto_increment=False | ||
| ), | ||
| timeout=120, | ||
| ): | ||
| print(f"Port {result} is now free.") |
There was a problem hiding this comment.
unfortunately this seems to add lots of time in the CI, not sure why the port takes so long to be available again after stopping reflex... @adhami3310 ?
There was a problem hiding this comment.
that's fun, i think we can improve this a tad bit by printing why a port is closed, let me try that in main
Refactor AppHarness to use subprocess instead of threads
Summary
This PR refactors the
AppHarnessclass inreflex/testing.pyto usereflex runin a subprocess instead of thread-based server management. This aligns better with how Reflex apps normally run (using granian by default) and reduces custom code maintenance burden.Key Changes:
_start_backendand_start_frontendmethods with subprocess-based_start_subprocess_wait_for_serversmethod that parses console output to detect server readiness using regex patternsshell=Trueapproach for subprocess to properly capture stdout/stderr fromreflex runstop()method to terminate subprocessesNotImplementedError)Current Status:
Review & Testing Checklist for Human
🔴 High Risk - 5 items to verify:
reflex runprocesses after tests)_wait_for_serverscorrectly detect "App running at:" and "Backend running at:" messages across different environmentsapp_harness.get_state()still work correctly with the new subprocess approachRecommended Test Plan:
uv run pytest tests/integration/test_input.py::test_fully_controlled_input -v -slocallyps aux | grep reflexbefore and after tests to verify no hanging processesREDIS_URL=redis://localhost:6379to verify Redis state manager compatibilityDiagram
%%{ init : { "theme" : "default" }}%% graph TD Testing["reflex/testing.py<br/>AppHarness class"]:::major-edit Processes["reflex/utils/processes.py<br/>new_process helper"]:::context IntegrationTests["tests/integration/<br/>12 failing tests"]:::context Testing -->|"uses subprocess"| Processes Testing -->|"starts via"| ReflexRun["reflex run command<br/>with shell=True"]:::minor-edit IntegrationTests -->|"instantiate"| Testing subgraph "AppHarness Methods" StartSubprocess["_start_subprocess()<br/>NEW METHOD"]:::major-edit WaitServers["_wait_for_servers()<br/>NEW METHOD"]:::major-edit Stop["stop()<br/>UPDATED"]:::minor-edit end Testing --> StartSubprocess Testing --> WaitServers Testing --> Stop subgraph Legend L1[Major Edit]:::major-edit L2[Minor Edit]:::minor-edit L3[Context/No Edit]:::context end classDef major-edit fill:#90EE90 classDef minor-edit fill:#87CEEB classDef context fill:#FFFFFFNotes
shell=Trueapproach was critical for properly capturing subprocess output - previous attempts without it failed to detect server readiness