Skip to content
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

Lock parallel package operations #2593

Merged
merged 1 commit into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ repos:
- flake8-unused-arguments==0.0.12
- flake8-noqa==1.3
- pep8-naming==0.13.2
- flake8-pyproject==1.2.1
- flake8-pyproject==1.2.2
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.7.1"
hooks:
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog/2594.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Ensure that two parallel tox instance invocations on different tox environment targets will work by holding a file lock
onto the packaging operations (e.g., in bash ``tox4 r -e py311 &; tox4 r -e py310``) - by :user:`gaborbernat`.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"pyproject-api>=1.2.1",
'tomli>=2.0.1; python_version < "3.11"',
"virtualenv>=20.17",
"filelock>=3.8.1",
'importlib-metadata>=5.1; python_version < "3.8"',
'typing-extensions>=4.4; python_version < "3.8"',
]
Expand All @@ -49,7 +50,6 @@ optional-dependencies.testing = [
"devpi-process>=0.3",
"diff-cover>=7.2",
"distlib>=0.3.6",
"filelock>=3.8",
"flaky>=3.7",
"hatch-vcs>=0.2",
"hatchling>=1.11.1",
Expand Down
2 changes: 1 addition & 1 deletion src/tox/tox_env/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def _clean(self, transitive: bool = False) -> None: # noqa: U100
env_dir = self.env_dir
if env_dir.exists():
LOGGER.warning("remove tox env folder %s", env_dir)
ensure_empty_dir(env_dir)
ensure_empty_dir(env_dir, except_filename="file.lock")
self._log_id = 0 # we deleted logs, so start over counter
self.cache.reset()
self._run_state.update({"setup": False, "clean": True})
Expand Down
22 changes: 17 additions & 5 deletions src/tox/tox_env/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from types import MethodType
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, cast

from filelock import FileLock

from tox.config.main import Config
from tox.config.sets import EnvConfigSet

Expand All @@ -31,29 +33,39 @@ def __str__(self) -> str:
return str(self.path)


def _lock_method(lock: RLock, meth: Callable[..., Any]) -> Callable[..., Any]:
def _lock_method(thread_lock: RLock, file_lock: FileLock | None, meth: Callable[..., Any]) -> Callable[..., Any]:
def _func(*args: Any, **kwargs: Any) -> Any:
with lock:
return meth(*args, **kwargs)
with thread_lock:
if file_lock is not None and file_lock.is_locked is False: # file_lock is to lock from other tox processes
file_lock.acquire()
try:
return meth(*args, **kwargs)
finally:
if file_lock is not None:
file_lock.release()

return _func


class PackageToxEnv(ToxEnv, ABC):
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
self._lock = RLock()
self._thread_lock = RLock()
self._file_lock: FileLock | None = None
super().__init__(create_args)
self._envs: set[str] = set()

def __getattribute__(self, name: str) -> Any:
# the packaging class might be used by multiple environments in parallel, hold a lock for operations on it
obj = object.__getattribute__(self, name)
if isinstance(obj, MethodType):
obj = _lock_method(self._lock, obj)
obj = _lock_method(self._thread_lock, self._file_lock, obj)
return obj

def register_config(self) -> None:
super().register_config()
file_lock_path: Path = self.conf["env_dir"] / "file.lock"
self._file_lock = FileLock(file_lock_path)
file_lock_path.parent.mkdir(parents=True, exist_ok=True)
self.core.add_config(
keys=["package_root", "setupdir"],
of_type=Path,
Expand Down
4 changes: 3 additions & 1 deletion src/tox/util/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from shutil import rmtree


def ensure_empty_dir(path: Path) -> None:
def ensure_empty_dir(path: Path, except_filename: str | None = None) -> None:
if path.exists():
if path.is_dir():
for sub_path in path.iterdir():
if sub_path.name == except_filename:
continue
if sub_path.is_dir():
rmtree(sub_path, ignore_errors=True)
else:
Expand Down