-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Serialize concurrent frontend dependency installs #7051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Alek99
wants to merge
3
commits into
main
Choose a base branch
from
codex/serialize-frontend-installs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Serialize frontend dependency installation per app and atomically persist package-manager files so concurrent Reflex commands cannot corrupt or duplicate an install. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Write disk-backed procedure caches atomically and recover automatically from truncated cache data. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| """Cross-process locking for frontend project mutations.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import errno | ||
| import os | ||
| import threading | ||
| import time | ||
| from collections.abc import Iterator | ||
| from pathlib import Path | ||
|
|
||
| from reflex_base import constants | ||
|
|
||
| _project_locks_guard = threading.Lock() | ||
| _project_locks: dict[Path, threading.RLock] = {} | ||
| _project_locks_held = threading.local() | ||
| # 50 milliseconds between contended Windows lock attempts. | ||
| _WINDOWS_LOCK_RETRY_DELAY = 0.05 | ||
| _open_project_lock_fds: set[int] = set() | ||
| _project_lock_fds_guard = threading.RLock() | ||
|
|
||
|
|
||
| def _prepare_project_locks_for_fork() -> None: | ||
| """Prevent a fork from splitting an open/close FD transition.""" | ||
| _project_lock_fds_guard.acquire() | ||
|
|
||
|
|
||
| def _resume_project_locks_after_fork() -> None: | ||
| """Release the parent's fork transition guard.""" | ||
| _project_lock_fds_guard.release() | ||
|
|
||
|
|
||
| def _reset_project_locks_after_fork() -> None: | ||
| """Discard inherited lock ownership in a forked child process.""" | ||
| global _project_lock_fds_guard, _project_locks_guard, _project_locks_held | ||
|
|
||
| # ``flock`` ownership follows the inherited open file description. Close | ||
| # the child's duplicate so a fresh open below blocks on the parent rather | ||
| # than inheriting or deadlocking against its own copy of the lock. | ||
| # Only raw descriptor operations are safe here: buffered file objects may | ||
| # retain a lock owned by a different thread that vanished during the fork. | ||
| for lock_fd in _open_project_lock_fds: | ||
| with contextlib.suppress(OSError): | ||
| os.close(lock_fd) | ||
| _open_project_lock_fds.clear() | ||
| _project_locks.clear() | ||
| _project_lock_fds_guard = threading.RLock() | ||
| _project_locks_guard = threading.Lock() | ||
| _project_locks_held = threading.local() | ||
|
|
||
|
|
||
| if hasattr(os, "register_at_fork"): | ||
| os.register_at_fork( | ||
| before=_prepare_project_locks_for_fork, | ||
| after_in_parent=_resume_project_locks_after_fork, | ||
| after_in_child=_reset_project_locks_after_fork, | ||
| ) | ||
|
|
||
|
|
||
| def _acquire_project_file_lock(lock_fd: int) -> None: | ||
| """Acquire an advisory lock on an open project lock file descriptor. | ||
|
|
||
| Args: | ||
| lock_fd: Raw descriptor kept open for the lifetime of the lock. | ||
| """ | ||
| if os.lseek(lock_fd, 0, os.SEEK_END) == 0: | ||
| os.write(lock_fd, b"\0") | ||
| os.lseek(lock_fd, 0, os.SEEK_SET) | ||
|
|
||
| if constants.IS_WINDOWS: | ||
| import msvcrt | ||
|
|
||
| while True: | ||
| try: | ||
| os.lseek(lock_fd, 0, os.SEEK_SET) | ||
| msvcrt.locking( # pyright: ignore[reportAttributeAccessIssue] | ||
| lock_fd, | ||
| msvcrt.LK_NBLCK, # pyright: ignore[reportAttributeAccessIssue] | ||
| 1, | ||
| ) | ||
| except OSError as err: # noqa: PERF203 # contention requires retrying | ||
| if err.errno not in {errno.EACCES, errno.EAGAIN, errno.EDEADLK}: | ||
| raise | ||
| time.sleep(_WINDOWS_LOCK_RETRY_DELAY) | ||
| else: | ||
| return | ||
|
|
||
| import fcntl | ||
|
|
||
| fcntl.flock(lock_fd, fcntl.LOCK_EX) | ||
|
|
||
|
|
||
| def _release_project_file_lock(lock_fd: int) -> None: | ||
| """Release an advisory lock on an open project lock file descriptor. | ||
|
|
||
| Args: | ||
| lock_fd: Raw descriptor previously passed to the acquire helper. | ||
| """ | ||
| os.lseek(lock_fd, 0, os.SEEK_SET) | ||
| if constants.IS_WINDOWS: | ||
| import msvcrt | ||
|
|
||
| msvcrt.locking( # pyright: ignore[reportAttributeAccessIssue] | ||
| lock_fd, | ||
| msvcrt.LK_UNLCK, # pyright: ignore[reportAttributeAccessIssue] | ||
| 1, | ||
| ) | ||
| return | ||
|
|
||
| import fcntl | ||
|
|
||
| fcntl.flock(lock_fd, fcntl.LOCK_UN) | ||
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def frontend_project_lock() -> Iterator[None]: | ||
| """Serialize frontend directory mutations for the current app. | ||
|
|
||
| The stable project-root lock file is outside ``.web`` and ``reflex.lock`` | ||
| because both directories may be replaced during recovery. Advisory OS locks | ||
| are released automatically if a process exits, while the in-process | ||
| reentrant lock makes nested use from the same thread safe. | ||
|
|
||
| Yields: | ||
| Once this process exclusively owns the current app's frontend lock. | ||
| """ | ||
| lock_path = (Path.cwd() / constants.Dirs.FRONTEND_INSTALL_LOCK).resolve() | ||
| with _project_locks_guard: | ||
| thread_lock = _project_locks.get(lock_path) | ||
| if thread_lock is None: | ||
| thread_lock = _project_locks[lock_path] = threading.RLock() | ||
|
|
||
| owner_pid = os.getpid() | ||
| thread_lock.acquire() | ||
| try: | ||
| held_paths = getattr(_project_locks_held, "paths", None) | ||
| if held_paths is None: | ||
| held_paths = _project_locks_held.paths = set() | ||
| if lock_path in held_paths: | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| yield | ||
| return | ||
|
|
||
| lock_path.parent.mkdir(parents=True, exist_ok=True) | ||
| with _project_lock_fds_guard: | ||
| lock_fd = os.open( | ||
| lock_path, | ||
| os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0), | ||
| 0o666, | ||
| ) | ||
| try: | ||
| _open_project_lock_fds.add(lock_fd) | ||
| except BaseException: | ||
| os.close(lock_fd) | ||
| raise | ||
| lock_acquired = False | ||
| path_held = False | ||
| try: | ||
| _acquire_project_file_lock(lock_fd) | ||
| lock_acquired = True | ||
| held_paths.add(lock_path) | ||
| path_held = True | ||
| yield | ||
| finally: | ||
| if os.getpid() == owner_pid: | ||
| try: | ||
| try: | ||
| if path_held: | ||
| held_paths.remove(lock_path) | ||
| finally: | ||
| if lock_acquired: | ||
| _release_project_file_lock(lock_fd) | ||
| finally: | ||
| with _project_lock_fds_guard: | ||
| _open_project_lock_fds.discard(lock_fd) | ||
| os.close(lock_fd) | ||
| finally: | ||
| # A child forked inside the yielded transaction has fresh process-local | ||
| # state and must not release the inherited parent-side RLock. | ||
| if os.getpid() == owner_pid: | ||
| thread_lock.release() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.