Make metadata deserialization failures non-fatal in the cache (#11105)

## Summary

If we fail to deserialize cached metadata in the cache, we should just
ignore it, rather than failing.

Ideally, this never happens. If it does, it means we missed a cache
version bump. But if it does happen, it should still be non-fatal.

Closes https://github.com/astral-sh/uv/issues/11043.

Closes https://github.com/astral-sh/uv/issues/11101.

## Test Plan

Prior to this PR, the following would fail:

- `uvx uv@0.5.25 venv --python 3.12 --cache-dir foo`
- `uvx uv@0.5.25 pip install ./scripts/packages/hatchling_dynamic
--no-deps --python 3.12 --cache-dir foo`
- `uvx uv@0.5.18 venv --python 3.12 --cache-dir foo`
- `uvx uv@0.5.18 pip install ./scripts/packages/hatchling_dynamic
--no-deps --python 3.12 --cache-dir foo`

We can't go back and fix 0.5.18, but this will prevent such regressions
in the future.
This commit is contained in:
Charlie Marsh 2025-01-30 12:48:35 -05:00 committed by GitHub
parent 1dfa650ab4
commit d106ab1a9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 64 deletions

View File

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

View File

@ -551,15 +551,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// If the cache contains compatible metadata, return it. // If the cache contains compatible metadata, return it.
let metadata_entry = cache_shard.entry(METADATA); let metadata_entry = cache_shard.entry(METADATA);
if let Some(metadata) = CachedMetadata::read(&metadata_entry) match CachedMetadata::read(&metadata_entry).await {
.await? Ok(Some(metadata)) => {
.filter(|metadata| metadata.matches(source.name(), source.version())) if metadata.matches(source.name(), source.version()) {
{ debug!("Using cached metadata for: {source}");
debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata {
return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata.into()),
metadata: Metadata::from_metadata23(metadata.into()), hashes: revision.into_hashes(),
hashes: revision.into_hashes(), });
}); }
debug!("Cached metadata does not match expected name and version for: {source}");
}
Ok(None) => {}
Err(err) => {
debug!("Failed to deserialize cached metadata for: {source} ({err})");
}
} }
// Otherwise, we need a wheel. // Otherwise, we need a wheel.
@ -882,15 +888,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// If the cache contains compatible metadata, return it. // If the cache contains compatible metadata, return it.
let metadata_entry = cache_shard.entry(METADATA); let metadata_entry = cache_shard.entry(METADATA);
if let Some(metadata) = CachedMetadata::read(&metadata_entry) match CachedMetadata::read(&metadata_entry).await {
.await? Ok(Some(metadata)) => {
.filter(|metadata| metadata.matches(source.name(), source.version())) if metadata.matches(source.name(), source.version()) {
{ debug!("Using cached metadata for: {source}");
debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata {
return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata.into()),
metadata: Metadata::from_metadata23(metadata.into()), hashes: revision.into_hashes(),
hashes: revision.into_hashes(), });
}); }
debug!("Cached metadata does not match expected name and version for: {source}");
}
Ok(None) => {}
Err(err) => {
debug!("Failed to deserialize cached metadata for: {source} ({err})");
}
} }
// Otherwise, we need a source distribution. // Otherwise, we need a source distribution.
@ -1183,31 +1195,38 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// If the cache contains compatible metadata, return it. // If the cache contains compatible metadata, return it.
let metadata_entry = cache_shard.entry(METADATA); let metadata_entry = cache_shard.entry(METADATA);
if let Some(metadata) = CachedMetadata::read(&metadata_entry) match CachedMetadata::read(&metadata_entry).await {
.await? Ok(Some(metadata)) => {
.filter(|metadata| metadata.matches(source.name(), source.version())) if metadata.matches(source.name(), source.version()) {
{ debug!("Using cached metadata for: {source}");
// If necessary, mark the metadata as dynamic.
let metadata = if dynamic {
ResolutionMetadata {
dynamic: true,
..metadata.into()
}
} else {
metadata.into()
};
return Ok(ArchiveMetadata::from( // If necessary, mark the metadata as dynamic.
Metadata::from_workspace( let metadata = if dynamic {
metadata, ResolutionMetadata {
resource.install_path.as_ref(), dynamic: true,
None, ..metadata.into()
self.build_context.locations(), }
self.build_context.sources(), } else {
self.build_context.bounds(), metadata.into()
) };
.await?, return Ok(ArchiveMetadata::from(
)); Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
None,
self.build_context.locations(),
self.build_context.sources(),
self.build_context.bounds(),
)
.await?,
));
}
debug!("Cached metadata does not match expected name and version for: {source}");
}
Ok(None) => {}
Err(err) => {
debug!("Failed to deserialize cached metadata for: {source} ({err})");
}
} }
// If the backend supports `prepare_metadata_for_build_wheel`, use it. // If the backend supports `prepare_metadata_for_build_wheel`, use it.
@ -1635,27 +1654,35 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.map_err(Error::CacheRead)? .map_err(Error::CacheRead)?
.is_fresh() .is_fresh()
{ {
if let Some(metadata) = CachedMetadata::read(&metadata_entry) match CachedMetadata::read(&metadata_entry).await {
.await? Ok(Some(metadata)) => {
.filter(|metadata| metadata.matches(source.name(), source.version())) if metadata.matches(source.name(), source.version()) {
{ debug!("Using cached metadata for: {source}");
let git_member = GitWorkspaceMember {
fetch_root: fetch.path(),
git_source: resource,
};
debug!("Using cached metadata for: {source}"); let git_member = GitWorkspaceMember {
return Ok(ArchiveMetadata::from( fetch_root: fetch.path(),
Metadata::from_workspace( git_source: resource,
metadata.into(), };
&path, return Ok(ArchiveMetadata::from(
Some(&git_member), Metadata::from_workspace(
self.build_context.locations(), metadata.into(),
self.build_context.sources(), &path,
self.build_context.bounds(), Some(&git_member),
) self.build_context.locations(),
.await?, self.build_context.sources(),
)); self.build_context.bounds(),
)
.await?,
));
}
debug!(
"Cached metadata does not match expected name and version for: {source}"
);
}
Ok(None) => {}
Err(err) => {
debug!("Failed to deserialize cached metadata for: {source} ({err})");
}
} }
} }

View File

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