Skip to content

Commit

Permalink
Lock parallel package operations
Browse files Browse the repository at this point in the history
This ensures that two tox invocation on different target environments will work.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Dec 5, 2022
1 parent 6e2290b commit 98ff14a
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 98ff14a

Please sign in to comment.