Make cache errors non-fatal in Planner::build (#12281)

Same basic approach as #11105, including a cache version bump.

Fixes #12274
This commit is contained in:
Aria Desires 2025-03-18 11:27:21 -04:00 committed by GitHub
parent faf16c1349
commit ada1acb32f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 192 additions and 118 deletions

View file

@ -1000,7 +1000,7 @@ impl CacheBucket {
match self {
// Note that when bumping this, you'll also need to bump it
// in `crates/uv/tests/it/cache_prune.rs`.
Self::SourceDistributions => "sdists-v8",
Self::SourceDistributions => "sdists-v9",
Self::FlatIndex => "flat-index-v2",
Self::Git => "git-v0",
Self::Interpreter => "interpreter-v4",

View file

@ -97,7 +97,7 @@ impl<'a> Planner<'a> {
[] => {}
[installed] => {
let source = RequirementSource::from(dist);
match RequirementSatisfaction::check(installed, &source)? {
match RequirementSatisfaction::check(installed, &source) {
RequirementSatisfaction::Mismatch => {
debug!("Requirement installed, but mismatched:\n Installed: {installed:?}\n Requested: {source:?}");
}
@ -108,6 +108,9 @@ impl<'a> Planner<'a> {
RequirementSatisfaction::OutOfDate => {
debug!("Requirement installed, but not fresh: {installed}");
}
RequirementSatisfaction::CacheInvalid => {
// Already logged
}
}
reinstalls.push(installed.clone());
}
@ -180,24 +183,31 @@ impl<'a> Planner<'a> {
.entry(format!("{}.http", wheel.filename.cache_key()));
// Read the HTTP pointer.
if let Some(pointer) = HttpArchivePointer::read_from(&cache_entry)? {
let cache_info = pointer.to_cache_info();
let archive = pointer.into_archive();
if archive.satisfies(hasher.get(dist.as_ref())) {
let cached_dist = CachedDirectUrlDist {
filename: wheel.filename.clone(),
url: VerbatimParsedUrl {
parsed_url: wheel.parsed_url(),
verbatim: wheel.url.clone(),
},
hashes: archive.hashes,
cache_info,
path: cache.archive(&archive.id),
};
match HttpArchivePointer::read_from(&cache_entry) {
Ok(Some(pointer)) => {
let cache_info = pointer.to_cache_info();
let archive = pointer.into_archive();
if archive.satisfies(hasher.get(dist.as_ref())) {
let cached_dist = CachedDirectUrlDist {
filename: wheel.filename.clone(),
url: VerbatimParsedUrl {
parsed_url: wheel.parsed_url(),
verbatim: wheel.url.clone(),
},
hashes: archive.hashes,
cache_info,
path: cache.archive(&archive.id),
};
debug!("URL wheel requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
debug!("URL wheel requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
debug!("Cached URL wheel requirement does not match expected hash policy for: {wheel}");
}
Ok(None) => {}
Err(err) => {
debug!("Failed to deserialize cached URL wheel requirement for: {wheel} ({err})");
}
}
}
@ -230,28 +240,41 @@ impl<'a> Planner<'a> {
)
.entry(format!("{}.rev", wheel.filename.cache_key()));
if let Some(pointer) = LocalArchivePointer::read_from(&cache_entry)? {
let timestamp = Timestamp::from_path(&wheel.install_path)?;
if pointer.is_up_to_date(timestamp) {
let cache_info = pointer.to_cache_info();
let archive = pointer.into_archive();
if archive.satisfies(hasher.get(dist.as_ref())) {
let cached_dist = CachedDirectUrlDist {
filename: wheel.filename.clone(),
url: VerbatimParsedUrl {
parsed_url: wheel.parsed_url(),
verbatim: wheel.url.clone(),
},
hashes: archive.hashes,
cache_info,
path: cache.archive(&archive.id),
};
match LocalArchivePointer::read_from(&cache_entry) {
Ok(Some(pointer)) => {
match Timestamp::from_path(&wheel.install_path) {
Ok(timestamp) => {
if pointer.is_up_to_date(timestamp) {
let cache_info = pointer.to_cache_info();
let archive = pointer.into_archive();
if archive.satisfies(hasher.get(dist.as_ref())) {
let cached_dist = CachedDirectUrlDist {
filename: wheel.filename.clone(),
url: VerbatimParsedUrl {
parsed_url: wheel.parsed_url(),
verbatim: wheel.url.clone(),
},
hashes: archive.hashes,
cache_info,
path: cache.archive(&archive.id),
};
debug!("Path wheel requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
debug!("Path wheel requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
debug!("Cached path wheel requirement does not match expected hash policy for: {wheel}");
}
}
Err(err) => {
debug!("Failed to get timestamp for wheel {wheel} ({err})");
}
}
}
Ok(None) => {}
Err(err) => {
debug!("Failed to deserialize cached path wheel requirement for: {wheel} ({err})");
}
}
}
Dist::Source(SourceDist::Registry(sdist)) => {
@ -281,19 +304,27 @@ impl<'a> Planner<'a> {
Dist::Source(SourceDist::DirectUrl(sdist)) => {
// Find the most-compatible wheel from the cache, since we don't know
// the filename in advance.
if let Some(wheel) = built_index.url(sdist)? {
if wheel.filename.name == sdist.name {
let cached_dist = wheel.into_url_dist(sdist);
debug!("URL source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
match built_index.url(sdist) {
Ok(Some(wheel)) => {
if wheel.filename.name == sdist.name {
let cached_dist = wheel.into_url_dist(sdist);
debug!("URL source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
warn!(
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
sdist,
wheel.filename
);
warn!(
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
sdist,
wheel.filename
);
}
Ok(None) => {}
Err(err) => {
debug!(
"Failed to deserialize cached wheel filename for: {sdist} ({err})"
);
}
}
}
Dist::Source(SourceDist::Git(sdist)) => {
@ -322,19 +353,27 @@ impl<'a> Planner<'a> {
// Find the most-compatible wheel from the cache, since we don't know
// the filename in advance.
if let Some(wheel) = built_index.path(sdist)? {
if wheel.filename.name == sdist.name {
let cached_dist = wheel.into_path_dist(sdist);
debug!("Path source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
match built_index.path(sdist) {
Ok(Some(wheel)) => {
if wheel.filename.name == sdist.name {
let cached_dist = wheel.into_path_dist(sdist);
debug!("Path source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
warn!(
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
sdist,
wheel.filename
);
warn!(
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
sdist,
wheel.filename
);
}
Ok(None) => {}
Err(err) => {
debug!(
"Failed to deserialize cached wheel filename for: {sdist} ({err})"
);
}
}
}
Dist::Source(SourceDist::Directory(sdist)) => {
@ -345,19 +384,29 @@ impl<'a> Planner<'a> {
// Find the most-compatible wheel from the cache, since we don't know
// the filename in advance.
if let Some(wheel) = built_index.directory(sdist)? {
if wheel.filename.name == sdist.name {
let cached_dist = wheel.into_directory_dist(sdist);
debug!("Directory source requirement already cached: {cached_dist}");
cached.push(CachedDist::Url(cached_dist));
continue;
}
match built_index.directory(sdist) {
Ok(Some(wheel)) => {
if wheel.filename.name == sdist.name {
let cached_dist = wheel.into_directory_dist(sdist);
debug!(
"Directory source requirement already cached: {cached_dist}"
);
cached.push(CachedDist::Url(cached_dist));
continue;
}
warn!(
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
sdist,
wheel.filename
);
warn!(
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
sdist,
wheel.filename
);
}
Ok(None) => {}
Err(err) => {
debug!(
"Failed to deserialize cached wheel filename for: {sdist} ({err})"
);
}
}
}
}

View file

@ -15,16 +15,14 @@ pub(crate) enum RequirementSatisfaction {
Mismatch,
Satisfied,
OutOfDate,
CacheInvalid,
}
impl RequirementSatisfaction {
/// Returns true if a requirement is satisfied by an installed distribution.
///
/// Returns an error if IO fails during a freshness check for a local path.
pub(crate) fn check(
distribution: &InstalledDist,
source: &RequirementSource,
) -> anyhow::Result<Self> {
pub(crate) fn check(distribution: &InstalledDist, source: &RequirementSource) -> Self {
trace!(
"Comparing installed with source: {:?} {:?}",
distribution,
@ -35,9 +33,9 @@ impl RequirementSatisfaction {
// If the requirement comes from a registry, check by name.
RequirementSource::Registry { specifier, .. } => {
if specifier.contains(distribution.version()) {
return Ok(Self::Satisfied);
return Self::Satisfied;
}
Ok(Self::Mismatch)
Self::Mismatch
}
RequirementSource::Url {
// We use the location since `direct_url.json` also stores this URL, e.g.
@ -55,7 +53,7 @@ impl RequirementSatisfaction {
..
}) = &distribution
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
let DirectUrl::ArchiveUrl {
url: installed_url,
@ -63,37 +61,47 @@ impl RequirementSatisfaction {
subdirectory: installed_subdirectory,
} = direct_url.as_ref()
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
if *editable {
return Ok(Self::Mismatch);
return Self::Mismatch;
}
if requested_subdirectory != installed_subdirectory {
return Ok(Self::Mismatch);
return Self::Mismatch;
}
if !CanonicalUrl::parse(installed_url)
.is_ok_and(|installed_url| installed_url == CanonicalUrl::new(requested_url))
{
return Ok(Self::Mismatch);
return Self::Mismatch;
}
// If the requirement came from a local path, check freshness.
if requested_url.scheme() == "file" {
if let Ok(archive) = requested_url.to_file_path() {
let Some(cache_info) = cache_info.as_ref() else {
return Ok(Self::OutOfDate);
return Self::OutOfDate;
};
if *cache_info != CacheInfo::from_path(&archive)? {
return Ok(Self::OutOfDate);
match CacheInfo::from_path(&archive) {
Ok(read_cache_info) => {
if *cache_info != read_cache_info {
return Self::OutOfDate;
}
}
Err(err) => {
debug!(
"Failed to read cached requirement for: {distribution} ({err})"
);
return Self::CacheInvalid;
}
}
}
}
// Otherwise, assume the requirement is up-to-date.
Ok(Self::Satisfied)
Self::Satisfied
}
RequirementSource::Git {
url: _,
@ -102,7 +110,7 @@ impl RequirementSatisfaction {
} => {
let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
let DirectUrl::VcsUrl {
url: installed_url,
@ -115,7 +123,7 @@ impl RequirementSatisfaction {
subdirectory: installed_subdirectory,
} = direct_url.as_ref()
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
if requested_subdirectory != installed_subdirectory {
@ -123,7 +131,7 @@ impl RequirementSatisfaction {
"Subdirectory mismatch: {:?} vs. {:?}",
installed_subdirectory, requested_subdirectory
);
return Ok(Self::Mismatch);
return Self::Mismatch;
}
if !RepositoryUrl::parse(installed_url).is_ok_and(|installed_url| {
@ -134,7 +142,7 @@ impl RequirementSatisfaction {
installed_url,
requested_git.repository()
);
return Ok(Self::Mismatch);
return Self::Mismatch;
}
// TODO(charlie): It would be more consistent for us to compare the requested
@ -147,10 +155,10 @@ impl RequirementSatisfaction {
installed_precise,
requested_git.precise()
);
return Ok(Self::OutOfDate);
return Self::OutOfDate;
}
Ok(Self::Satisfied)
Self::Satisfied
}
RequirementSource::Path {
install_path: requested_path,
@ -163,7 +171,7 @@ impl RequirementSatisfaction {
..
}) = &distribution
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
let DirectUrl::ArchiveUrl {
url: installed_url,
@ -171,14 +179,14 @@ impl RequirementSatisfaction {
subdirectory: None,
} = direct_url.as_ref()
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
let Some(installed_path) = Url::parse(installed_url)
.ok()
.and_then(|url| url.to_file_path().ok())
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
if !(*requested_path == installed_path
@ -189,17 +197,25 @@ impl RequirementSatisfaction {
requested_path,
installed_path,
);
return Ok(Self::Mismatch);
return Self::Mismatch;
}
let Some(cache_info) = cache_info.as_ref() else {
return Ok(Self::OutOfDate);
return Self::OutOfDate;
};
if *cache_info != CacheInfo::from_path(requested_path)? {
return Ok(Self::OutOfDate);
match CacheInfo::from_path(requested_path) {
Ok(read_cache_info) => {
if *cache_info != read_cache_info {
return Self::OutOfDate;
}
}
Err(err) => {
debug!("Failed to read cached requirement for: {distribution} ({err})");
return Self::CacheInvalid;
}
}
Ok(Self::Satisfied)
Self::Satisfied
}
RequirementSource::Directory {
install_path: requested_path,
@ -213,7 +229,7 @@ impl RequirementSatisfaction {
..
}) = &distribution
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
let DirectUrl::LocalDirectory {
url: installed_url,
@ -223,7 +239,7 @@ impl RequirementSatisfaction {
},
} = direct_url.as_ref()
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
if *requested_editable != installed_editable.unwrap_or_default() {
@ -232,14 +248,14 @@ impl RequirementSatisfaction {
*requested_editable,
installed_editable.unwrap_or_default()
);
return Ok(Self::Mismatch);
return Self::Mismatch;
}
let Some(installed_path) = Url::parse(installed_url)
.ok()
.and_then(|url| url.to_file_path().ok())
else {
return Ok(Self::Mismatch);
return Self::Mismatch;
};
if !(*requested_path == installed_path
@ -250,17 +266,25 @@ impl RequirementSatisfaction {
requested_path,
installed_path,
);
return Ok(Self::Mismatch);
return Self::Mismatch;
}
let Some(cache_info) = cache_info.as_ref() else {
return Ok(Self::OutOfDate);
return Self::OutOfDate;
};
if *cache_info != CacheInfo::from_path(requested_path)? {
return Ok(Self::OutOfDate);
match CacheInfo::from_path(requested_path) {
Ok(read_cache_info) => {
if *cache_info != read_cache_info {
return Self::OutOfDate;
}
}
Err(err) => {
debug!("Failed to read cached requirement for: {distribution} ({err})");
return Self::CacheInvalid;
}
}
Ok(Self::Satisfied)
Self::Satisfied
}
}
}

View file

@ -437,9 +437,10 @@ impl SitePackages {
[distribution] => {
// Validate that the requirement is satisfied.
if requirement.evaluate_markers(Some(markers), &[]) {
match RequirementSatisfaction::check(distribution, &requirement.source)? {
match RequirementSatisfaction::check(distribution, &requirement.source) {
RequirementSatisfaction::Mismatch
| RequirementSatisfaction::OutOfDate => {
| RequirementSatisfaction::OutOfDate
| RequirementSatisfaction::CacheInvalid => {
return Ok(SatisfiesResult::Unsatisfied(requirement.to_string()))
}
RequirementSatisfaction::Satisfied => {}
@ -449,10 +450,10 @@ impl SitePackages {
// Validate that the installed version satisfies the constraints.
for constraint in constraints.get(name).into_iter().flatten() {
if constraint.evaluate_markers(Some(markers), &[]) {
match RequirementSatisfaction::check(distribution, &constraint.source)?
{
match RequirementSatisfaction::check(distribution, &constraint.source) {
RequirementSatisfaction::Mismatch
| RequirementSatisfaction::OutOfDate => {
| RequirementSatisfaction::OutOfDate
| RequirementSatisfaction::CacheInvalid => {
return Ok(SatisfiesResult::Unsatisfied(
requirement.to_string(),
))

View file

@ -348,7 +348,7 @@ fn prune_stale_revision() -> Result<()> {
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling source revision: [CACHE_DIR]/sdists-v8/[ENTRY]
DEBUG Removing dangling source revision: [CACHE_DIR]/sdists-v9/[ENTRY]
DEBUG Removing dangling cache archive: [CACHE_DIR]/archive-v0/[ENTRY]
Removed [N] files ([SIZE])
"###);