Skip to content

Commit

Permalink
Lock parallel package operations (#2593)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaborbernat committed Dec 5, 2022
1 parent 3d50713 commit 4c77457
Show file tree
Hide file tree
Showing 6 changed files with 25 additions and 9 deletions.
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

0 comments on commit 4c77457

Please sign in to comment.