Skip to content

Serialize production frontend work sharing one .web directory - #7453

Merged
masenf merged 6 commits into
mainfrom
farhan/frontend-workspace-lock
Oct 19, 2026
Merged

masenf merged 6 commits into
mainfrom
farhan/frontend-workspace-lock

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Split out of #7054. This is the lock only; the opt-in build cache that motivated it is #7455 and is stacked on this.

Problem

Two production exports in the same project (or an export racing a reflex run --env prod start) share .web: the second one rewrites env.json, build/, and the compiled sources while the first is still compiling or zipping, so one of them ships the other's frontend. Review bots on #7054 flagged this for the cache, but it is a pre-existing race.

Changes

  • path_ops.json_file_lock (from Prevent concurrent reflex.json update corruption #7052) becomes path_ops.file_lock: one implementation of the platform-specific locking (flock on POSIX, msvcrt.locking on Windows) for files and directories, instead of a second copy for builds. It gains what the build lock needs: nested calls from the same thread reuse the lock, other threads and processes wait, and a forked child drops inherited ownership and waits for its parent. On Windows the lock is released explicitly before the descriptor closes. json_file_lock was never released, so there is no deprecation shim. Serialize concurrent frontend dependency installs #7051 makes the same rename and locks get_web_dir() through it, so the two converge.
  • The lock is the existing sidecar in REFLEX_DIR keyed by the resolved .web path, not a file inside .web. Locking never creates .web, and the lock inode survives .web being replaced by reflex init. As with json_file_lock, an unwritable lock directory or an unsupported filesystem logs at debug and proceeds unlocked.
  • export.export() holds it from configuration through ZIP creation. Backend-only exports always take it too, since they archive .web/backend; the earlier "only when .web exists" condition existed to avoid creating .web, which the sidecar makes moot, so the race the review bots flagged on that line is closed.
  • _run_prod / _run_preview hold it through compile and build and release it before serving.
  • Tests: spawned-process serialization and same-thread reentrancy, a second thread waiting, fork ownership, Windows contention retry and release, and a real two-process export test where a backend ZIP waits for the compiler's temporary stateful-pages marker to be renamed.

Measurement

No speedup is claimed. Lock overhead on an unchanged bun export, repeat runs:

main this PR
total export 6.90 s 6.84 s
compile phase 0.46 s 0.44 s

This is pure complexity in service of correctness (and of #7455). If the cache is not wanted, the question for this one is only whether the race above is worth fixing.

Measurement setup

Five-page app from the blank template (Radix Themes + Tailwind v4 plugins), Linux x86_64, 4 vCPU, Python 3.14, Node 22.22, npm 10.9, bun 1.4.2. Each run is a fresh interpreter calling reflex.utils.export.export() the same way reflex export does; durations are the phase timings the export already reports to telemetry (compile includes the dependency install). Medians of 3 runs unless noted. Vite dominates every export (~6.5 s); these numbers isolate the part this PR touches.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • This change requires a documentation update

Checklist

  • Tests added for the change; tests/units/utils, test_reflex.py, test_prerequisites.py and test_workflow_gates.py pass locally
  • ruff check, ruff format --check, codespell, and pyright reflex tests pass locally
  • News fragment added for every package touched

https://claude.ai/code/session_018nWB9r2HcC5tkT72UxWgGj
https://claude.ai/code/session_017XAFCkYB3Z8w3S6pgJrLCh

Two exports (or an export and a prod/preview start) in the same project
used to race on .web: the second replaced build/ and env.json while the
first was still compiling or zipping, so one of them shipped the other's
frontend. Hold an exclusive, reentrant file lock on
.web/.reflex-build.lock from compilation through ZIP creation, release it
before serving, and never create .web for a backend-only export that has
nothing to protect.

Split out of #7054 (lock only; the opt-in build cache that needs it follows
separately).

Claude-Session: https://claude.ai/code/session_018nWB9r2HcC5tkT72UxWgGj
@FarhanAliRaza
FarhanAliRaza requested review from a team and Alek99 as code owners October 6, 2026 18:19
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T18:24:53.157193Z b7d35ca PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread reflex/reflex.py Outdated
Comment thread tests/units/utils/test_export.py
Comment thread reflex/utils/export.py Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

if frontend or (backend and zipping and web_dir.is_dir())

P1 Badge Lock backend exports before checking for .web

When a backend-only export starts while .web is absent and a concurrent frontend export creates it immediately after this is_dir() check, the backend export proceeds under nullcontext(). build.zip_app() later globs .web/backend/*, so it can archive missing or partially written compiler artifacts while the frontend process believes it owns the workspace lock. The existence check and decision about whether generated backend files may be included must be synchronized to close this first-build race.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change reaches production export and frontend-start workflows that share a `.web` directory.

The reported import failure in stacked PR #7455 still needs to be addressed before the two PRs can merge together.

Summary

This PR makes production exports and startup builds wait when they share .web.

  • One shared lock now covers files and folders.
  • Production exports wait for other work using the same .web folder.
  • Production and preview starts lock .web during their first frontend build.

Reviews (4) · Last reviewed commit: "Keep the export race test's lock directo..." · Reviewed by Greptile

Comment thread reflex/utils/export.py Outdated
@codspeed

codspeed Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 13 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 148 untouched benchmarks


Comparing farhan/frontend-workspace-lock (47eb606) with main (d3f8ccb)

Open in CodSpeed

claude added 2 commits October 6, 2026 19:58
Brings in #7449, which drops Python 3.10 and moves ruff to py311.

Claude-Session: https://claude.ai/code/session_01PXcFKAgnzhgdnSvhuCmxWE
PERF203 only applies before Python 3.11, where try/except in a loop costs
per iteration. With ruff targeting py311 since #7449 the directive is
unused, which RUF100 rejects.

Claude-Session: https://claude.ai/code/session_01PXcFKAgnzhgdnSvhuCmxWE
@masenf masenf added the on deck PRs lined up to review / merge next label Oct 13, 2026

@masenf masenf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should consolidate locking implementation with existing json_file_lock so we don't have multiple code paths handling platform-specific file locking in slightly different ways

@masenf masenf added the documentation Improvements or additions to documentation label Oct 19, 2026 — with ChatGPT Codex Connector
The production build lock had its own copy of the platform-specific
locking (flock / msvcrt) next to the sidecar-based json_file_lock, with
different Windows polling, a different failure mode and a lock file inside
.web. Keep one implementation: json_file_lock becomes file_lock, usable for
files and directories, and gains the reentrancy and fork handling the build
lock needed. Exports and production/preview startup now lock the .web
directory through it.

The sidecar lives in REFLEX_DIR, so locking never creates .web and the
lock inode survives .web being replaced. Backend-only exports therefore
always take the lock instead of skipping it when .web is absent, which
closes the race the review bots flagged. The lock is released explicitly
on Windows before the descriptor closes.

Claude-Session: https://claude.ai/code/session_017XAFCkYB3Z8w3S6pgJrLCh
Comment thread reflex/utils/path_ops.py

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread reflex/utils/path_ops.py
Comment thread reflex/utils/export.py
The backend ZIP walks the app root, and REFLEX_DIR sat inside it, so the
exporter archived its own held lock file. Windows refuses reads of a
byte-range-locked file, failing the zip with PermissionError.

Claude-Session: https://claude.ai/code/session_01PXcFKAgnzhgdnSvhuCmxWE
@masenf
masenf merged commit 937c5a2 into main Oct 19, 2026
169 checks passed
@masenf
masenf deleted the farhan/frontend-workspace-lock branch October 19, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants