Lock during uv sync, uv add and uv remove to avoid race conditions (#13869)

Surprisingly, we weren't locking during `uv sync` so far, so running `uv
sync` in parallel could cause errors in filesystem races.

I've also added locks to `uv add` and `uv remove` which concurrently
modify `pyproject.toml`. These locks only apply after we determined the
interpreter, so they don't actually prevent computing the same thing
twice when running `uv add` in parallel.

All other subcommands that I checked were already locking (with no claim
to exhaustiveness)

Fixes #12751

# Test Plan

I don't have CI-sized reproducer for this.

```toml
[project]
name = "debug"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
  "boto3>=1.38.30",
  "fastapi>=0.115.12",
  "numba>=0.61.2",
  "polars>=1.30.0",
  "protobuf>=6.31.1",
  "pyarrow>=20.0.0",
  "pydantic>=2.11.5",
  "requests>=2.32.3",
  "urllib3>=2.4.0",
  "scikit-learn>=1.6.1",
  "jupyter>=1.1.1",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

```
rm -rf .venv && parallel -n0 "uv sync -q" ::: {1..10}
```
This commit is contained in:
konsti 2025-06-06 14:16:40 +02:00 committed by GitHub
parent b865f76b78
commit bf96c60e3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 44 additions and 22 deletions

View file

@ -1,5 +1,4 @@
use std::borrow::Cow;
use std::env;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -8,7 +7,6 @@ use owo_colors::OwoColorize;
use tracing::debug;
use uv_cache::Cache;
use uv_cache_key::cache_digest;
use uv_fs::{LockedFile, Simplified};
use uv_pep440::Version;
@ -316,23 +314,7 @@ impl PythonEnvironment {
/// Grab a file lock for the environment to prevent concurrent writes across processes.
pub async fn lock(&self) -> Result<LockedFile, std::io::Error> {
if let Some(target) = self.0.interpreter.target() {
// If we're installing into a `--target`, use a target-specific lockfile.
LockedFile::acquire(target.root().join(".lock"), target.root().user_display()).await
} else if let Some(prefix) = self.0.interpreter.prefix() {
// Likewise, if we're installing into a `--prefix`, use a prefix-specific lockfile.
LockedFile::acquire(prefix.root().join(".lock"), prefix.root().user_display()).await
} else if self.0.interpreter.is_virtualenv() {
// If the environment a virtualenv, use a virtualenv-specific lockfile.
LockedFile::acquire(self.0.root.join(".lock"), self.0.root.user_display()).await
} else {
// Otherwise, use a global lockfile.
LockedFile::acquire(
env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.0.root))),
self.0.root.user_display(),
)
.await
}
self.0.interpreter.lock().await
}
/// Return the [`Interpreter`] for this environment.

View file

@ -1,10 +1,10 @@
use std::borrow::Cow;
use std::env::consts::ARCH;
use std::fmt::{Display, Formatter};
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use std::sync::OnceLock;
use std::{env, io};
use configparser::ini::Ini;
use fs_err as fs;
@ -17,7 +17,7 @@ use tracing::{debug, trace, warn};
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness};
use uv_cache_info::Timestamp;
use uv_cache_key::cache_digest;
use uv_fs::{PythonExt, Simplified, write_atomic_sync};
use uv_fs::{LockedFile, PythonExt, Simplified, write_atomic_sync};
use uv_install_wheel::Layout;
use uv_pep440::Version;
use uv_pep508::{MarkerEnvironment, StringVersion};
@ -581,6 +581,31 @@ impl Interpreter {
.into_iter()
.any(|default_name| name == default_name.to_string())
}
/// Grab a file lock for the environment to prevent concurrent writes across processes.
pub async fn lock(&self) -> Result<LockedFile, io::Error> {
if let Some(target) = self.target() {
// If we're installing into a `--target`, use a target-specific lockfile.
LockedFile::acquire(target.root().join(".lock"), target.root().user_display()).await
} else if let Some(prefix) = self.prefix() {
// Likewise, if we're installing into a `--prefix`, use a prefix-specific lockfile.
LockedFile::acquire(prefix.root().join(".lock"), prefix.root().user_display()).await
} else if self.is_virtualenv() {
// If the environment a virtualenv, use a virtualenv-specific lockfile.
LockedFile::acquire(
self.sys_prefix.join(".lock"),
self.sys_prefix.user_display(),
)
.await
} else {
// Otherwise, use a global lockfile.
LockedFile::acquire(
env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))),
self.sys_prefix.user_display(),
)
.await
}
}
}
/// The `EXTERNALLY-MANAGED` file in a Python installation.