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

Fix race condition with forked monorepos #9723

Merged
merged 3 commits into from
Oct 14, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from poetry.utils.helpers import remove_directory
from poetry.utils.isolated_build import IsolatedBuildError
from poetry.utils.isolated_build import IsolatedBuildInstallError
from poetry.vcs.git import Git
radoering marked this conversation as resolved.
Show resolved Hide resolved


if TYPE_CHECKING:
Expand All @@ -46,6 +47,12 @@
from poetry.utils.env import Env


def _package_get_name(package: Package) -> str | None:
if url := package.repository_url:
return Git.get_name_from_source_url(url)
return None


class Executor:
def __init__(
self,
Expand Down Expand Up @@ -167,8 +174,9 @@ def execute(self, operations: list[Operation]) -> int:
if is_parallel_unsafe:
serial_operations.append(operation)
elif operation.package.source_type == "git":
# Git operations on the same repository should be executed serially
serial_git_operations[operation.package.source_url].append(
# Serially execute git operations that get cloned to the same directory,
# to prevent multiple parallel git operations in the same repo.
serial_git_operations[_package_get_name(operation.package)].append(
radoering marked this conversation as resolved.
Show resolved Hide resolved
operation
)
else:
Expand Down Expand Up @@ -604,8 +612,6 @@ def _prepare_archive(
)

def _prepare_git_archive(self, operation: Install | Update) -> Path:
from poetry.vcs.git import Git

package = operation.package
assert package.source_url is not None

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[tool.poetry]
name = "one"
version = "1.0.0"
description = "Some description."
authors = []
license = "MIT"

[tool.poetry.dependencies]
python = "^3.7"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[tool.poetry]
name = "one"
version = "1.0.0"
description = "Some description."
authors = []
license = "MIT"

[tool.poetry.dependencies]
python = "^3.7"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[tool.poetry]
name = "two"
version = "2.0.0"
description = "Some description."
authors = []
license = "MIT"

[tool.poetry.dependencies]
python = "~2.7 || ^3.4"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
3 changes: 2 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ def mock_clone(
if not source_root:
source_root = Path(Config.create().get("cache-dir")) / "src"

dest = source_root / path
assert parsed.name is not None
dest = source_root / parsed.name
dest.mkdir(parents=True, exist_ok=True)

copy_path(folder, dest)
Expand Down
48 changes: 45 additions & 3 deletions tests/installation/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,7 @@ def test_executor_should_append_subdirectory_for_git(
executor.execute([Install(package)])

archive_arg = spy.call_args[0][0]
assert archive_arg == tmp_venv.path / "src/demo/subdirectories/two"
assert archive_arg == tmp_venv.path / "src/subdirectories/two"


def test_executor_should_install_multiple_packages_from_same_git_repository(
Expand Down Expand Up @@ -1131,10 +1131,52 @@ def test_executor_should_install_multiple_packages_from_same_git_repository(
executor.execute([Install(package_a), Install(package_b)])

archive_arg = spy.call_args_list[0][0][0]
assert archive_arg == tmp_venv.path / "src/demo/subdirectories/package_a"
assert archive_arg == tmp_venv.path / "src/subdirectories/package_a"

archive_arg = spy.call_args_list[1][0][0]
assert archive_arg == tmp_venv.path / "src/demo/subdirectories/package_b"
assert archive_arg == tmp_venv.path / "src/subdirectories/package_b"


def test_executor_should_install_multiple_packages_from_forked_git_repository(
mocker: MockerFixture,
tmp_venv: VirtualEnv,
pool: RepositoryPool,
config: Config,
artifact_cache: ArtifactCache,
io: BufferedIO,
wheel: Path,
) -> None:
package_a = Package(
"one",
"1.0.0",
source_type="git",
source_reference="master",
source_resolved_reference="123456",
source_url="https://github.com/demo/subdirectories.git",
source_subdirectory="one",
)
package_b = Package(
"two",
"2.0.0",
source_type="git",
source_reference="master",
source_resolved_reference="123456",
source_url="https://github.com/forked_demo/subdirectories.git",
source_subdirectory="two",
)

chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config))
chef.set_directory_wheel(wheel)
prepare_spy = mocker.spy(chef, "prepare")

executor = Executor(tmp_venv, pool, config, io)
executor._chef = chef
executor.execute([Install(package_a), Install(package_b)])

# Verify that the repo for package_a is not re-used for package_b.
# both repos must be cloned serially into separate directories.
# If so, executor.prepare() will be called twice.
assert prepare_spy.call_count == 2


def test_executor_should_write_pep610_url_references_for_git_with_subdirectories(
Expand Down
Loading