python-poetry / python-poetry/poetry

Parallel installs can fail to resolve setuptools due to a SeparateBodyFileCache race

Open
#11,048 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

kind/bug status/external-issue
Dominant language
Python
Stars
34.3k
Forks
2.5k
Avg merge
2d 19h
Merged PRs (30d)
30

Description

Description

We are hitting intermittent setuptools resolution failures during parallel source-package installs with Poetry 2.4.1 in CI. I also reproduced the underlying empty-index failure with Poetry 2.4.3, the latest release at the time of reporting, and CacheControl 0.14.4.

The failing CI install was preparing argparse 1.2.1 and bravado-core 6.1.1. The package-index request succeeded, but Poetry reported:

Because -root- depends on setuptools (>=40.8.0) which doesn't match any versions, version solving failed.
Cannot resolve build-system.requires for bravado-core.
Package setuptools (84.0.0) not found.

The local reproducer demonstrates a cache race consistent with this symptom: SeparateBodyFileCache publishes metadata before the separate body file exists. A concurrent Poetry repository lookup can interpret the incomplete entry as an empty package index, either on a fresh-cache hit or during HTTP 304 revalidation. The exact interleaving in the original CI job was not captured.

Expected behavior: concurrent lookups should return the available package links, wait for a complete entry, or treat an incomplete entry as a cache miss.

Related upstream work:

This report tracks the impact on Poetry and provides reproduction evidence for evaluating the upstream fix or a Poetry-side mitigation.

Reproduction

The standalone script uses a real localhost HTTP server, Poetry's LegacyRepository, the native CacheControl adapter/controller, and the actual HTML parser. It adds synchronization around existing calls to force the writer to pause before body publication. It does not mock package lists or parser results.

Requirements: uv, Git for the optional fixed control, and Python 3.11 (which uv can provision).

curl -fsSL \
  https://raw.githubusercontent.com/igormishsky/cachecontrol/3af1c3c1e03425793d67a7d0a4799b7f4ed01607/scripts/reproduce_poetry_cache_race.py \
  -o reproduce_poetry_cache_race.py

uv run --no-project --python 3.11 \
  --with 'poetry==2.4.3' --with 'cachecontrol[filecache]==0.14.4' \
  python reproduce_poetry_cache_race.py \
  --expect broken --iterations 5 --output baseline.json

--expect broken exits successfully only when the failure is reproduced as expected. Each run uses new temporary cache directories, so it does not depend on a previously corrupt cache, Artifactory, or a restored GitHub Actions cache.

There are four schedules, repeated five times: threads/processes sharing a cache, each with fresh reads and HTTP 200/304 revalidation. The server sends an ETag and Cache-Control: max-age=60, with no Content-Length or Vary header. One reader sees zero links although the response contains setuptools-84.0.0-py3-none-any.whl.

Results
Poetry CacheControl Empty-index failures
2.4.1 Released 0.14.4 20/20
2.4.3 Released 0.14.4 20/20
2.4.1 Original #465 commit 7df32057203eb379f51149da204bddd3d3447bdc 0/20

The original upstream commit was tested without the additional changes in my fork. To run that control:

git clone https://github.com/trail-of-forks/cachecontrol.git cachecontrol-465
git -C cachecontrol-465 checkout 7df32057203eb379f51149da204bddd3d3447bdc

uv run --no-project --python 3.11 \
  --with 'poetry==2.4.1' --with 'cachecontrol[filecache]==0.14.4' \
  python reproduce_poetry_cache_race.py \
  --cache-source ./cachecontrol-465 \
  --expect fixed --iterations 5 --output fixed.json

Poetry remains unmodified and caching stays enabled. With that commit the reader waits for body publication, and both workers find the expected link. Subsequent warm-cache lookups make zero additional HTTP requests. The script prints loaded source paths because the checkout still reports the distribution version as 0.14.4.

These are deterministic HTTP/cache/index-parsing tests, not complete application installs or measurements of natural failure frequency. No wheel is downloaded or executed. Separate fault-injection tests still expose interrupted-write and concurrent 304-update gaps in the original #465 commit; the passing result above is limited to the reproduced read-during-publication failure.

Workarounds

poetry --no-cache install ... bypasses the affected HTTP cache. A separate focused test of Poetry 2.4.1's CLI cache-bypass path passed 5/5 overlapping repository lookups. This has not been validated as a full application-build fix in this report. Clearing the cache alone does not prevent recurrence: every failing reproduction starts with a fresh temporary cache.

Poetry Installation Method

Other: uv-managed isolated environments with pinned Poetry and CacheControl versions.

Operating System

Local reproduction: macOS 15.7.7, arm64, Python 3.11.5.
Observed CI symptoms: Amazon Linux 2023 on arm64 and amd64, Poetry 2.4.1.

Poetry Version
Poetry (version 2.4.3)

Also tested with Poetry 2.4.1, as shown above.

Poetry Configuration

The standalone reproducer constructs Config(use_environment=False) and overrides only cache-dir with a fresh temporary directory and keyring.enabled with false. It uses a localhost LegacyRepository, with no credentials or plugins. The two concurrent workers are explicitly created by the harness.

Relevant defaults, confirmed using an isolated poetry config --list:

installer.max-workers = null
installer.no-binary = null
installer.only-binary = null
installer.parallel = true
installer.re-resolve = false
requests.max-retries = 0
solver.lazy-wheel = true
virtualenvs.create = true
virtualenvs.in-project = null
Example pyproject.toml

Not required for the standalone reproduction: it exercises Poetry's repository lookup directly. The original symptom occurs during isolated build-system dependency resolution for source distributions.

Poetry Runtime Logs

The CI error excerpt is above. The standalone script directly exercises the repository API rather than invoking poetry install, and records actual HTTP requests and parsed links in its output JSON.

Selected summary fields from the Poetry 2.4.3 baseline run:

{
  "poetry_version": "2.4.3",
  "cachecontrol_distribution_version": "0.14.4",
  "expectation": "broken",
  "total": 20,
  "empty_indexes": 20,
  "both_workers_succeeded": 0
}

The HTTP sequence is [200] for a fresh read and [200, 304] for revalidation. In each baseline trial the writer finds the wheel link, while the reader returns [] before the writer is released. With original #465, both return the wheel link and the reader completes after the writer is released.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked scripts/reproduce_poetry_cache_race.py and run its Poetry 2.4.3/CacheControl 0.14.4 baseline, then compare the original #465 control. Trace the Poetry repository lookup and cache configuration involved; done means deciding, with a focused regression test if appropriate, whether the upstream fix fully resolves the empty-index result or a Poetry-side mitigation is required.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.