Conversation
In prod backend-only mode with several Granian workers, every worker takes the "evaluate all pages" path when .web is absent and writes .web/backend/stateful_pages.json with mode "w", truncating it. A worker starting slightly later saw the backend dir, read an empty or partial marker, and died with JSONDecodeError. Write the marker to a temporary file in the same directory and swap it into place with Path.replace so readers only ever see a complete file. Read the marker with a single read_text call and treat FileNotFoundError as "no marker yet", falling through to evaluating all pages, which also closes the window between one worker creating the backend dir and swapping its marker in. The marker is now always written, including for stateless apps, so that fall-through does not slow their startup. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSjqov3yBj4cBasJqzyrrQ
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSjqov3yBj4cBasJqzyrrQ
|
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Review completed against the latest diff
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
FarhanAliRaza
left a comment
There was a problem hiding this comment.
Validated end to end in a real example app (stateful + stateless + ComponentState pages): truncated marker crashes main with JSONDecodeError and recovers cleanly on this branch; two concurrent markerless backend workers start without errors and leave a valid marker; browser state behavior unregressed; reflex export succeeds and preserves the marker. Inline comments are non-blocking notes only.
Generated by Claude Code
Type of change
Description
Fixes a startup race condition in backend-only mode with multiple workers where a worker could read a truncated
.web/backend/stateful_pages.jsonfile and crash withJSONDecodeError.Root cause: The marker file was written directly, so concurrent writers could produce partial/corrupted JSON that readers would encounter.
Solution: Write the marker atomically by:
Path.replace()to atomically swap it into placeAdditionally, refactored the marker reading logic to handle the case where the marker doesn't exist yet (another worker may be writing it), which correctly falls through to evaluating all pages rather than assuming "no marker" means "no stateful pages".
Changes
reflex/app.py:_write_stateful_pages_marker()to use atomic writes viatempfile.mkstemp()andPath.replace()reflex/compiler/compiler.py:_read_stateful_pages_marker()helper that returnsNoneif the marker doesn't exist yetcompile_app()logic: only skip full page evaluation if the marker exists and is readable; missing marker falls through to normal compilationTests:
test_write_stateful_pages_marker_never_truncates_final_path(): verifies the marker is never opened for writing (only swapped into place)test_write_stateful_pages_marker_is_always_written(): verifies stateless apps write an empty markertest_write_stateful_pages_marker_concurrent_readers_see_valid_json(): stress test with 4 concurrent writers and 4 concurrent readers, ensuring noJSONDecodeErroror partial readstest_compile_registers_plugin_routes_on_backend_early_return()to correctly expect all pages evaluated when marker is missingTest Plan
All new unit tests pass and cover the atomic write behavior and concurrent access patterns. Existing tests updated to reflect the corrected behavior when the marker is absent.
https://claude.ai/code/session_01GSjqov3yBj4cBasJqzyrrQ