mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Add retries for Python downloads (#9274)
## Summary This uses the same approach as in the rest of uv, but with another dedicated method for retries. Closes https://github.com/astral-sh/uv/issues/8525.
This commit is contained in:
parent
289771e311
commit
1b13036674
16 changed files with 107 additions and 54 deletions
|
@ -45,6 +45,7 @@ owo-colors = { workspace = true }
|
|||
regex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-middleware = { workspace = true }
|
||||
reqwest-retry = { workspace = true }
|
||||
rmp-serde = { workspace = true }
|
||||
same-file = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
use futures::TryStreamExt;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use owo_colors::OwoColorize;
|
||||
use reqwest_retry::RetryPolicy;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use tokio_util::either::Either;
|
||||
use tracing::{debug, instrument};
|
||||
use url::Url;
|
||||
use uv_client::WrappedReqwestError;
|
||||
|
||||
use uv_client::{is_extended_transient_error, WrappedReqwestError};
|
||||
use uv_distribution_filename::{ExtensionError, SourceDistExtension};
|
||||
use uv_extract::hash::Hasher;
|
||||
use uv_fs::{rename_with_retry, Simplified};
|
||||
|
@ -417,6 +421,7 @@ impl FromStr for PythonDownloadRequest {
|
|||
|
||||
include!("downloads.inc");
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DownloadResult {
|
||||
AlreadyAvailable(PathBuf),
|
||||
Fetched(PathBuf),
|
||||
|
@ -458,7 +463,57 @@ impl ManagedPythonDownload {
|
|||
self.sha256
|
||||
}
|
||||
|
||||
/// Download and extract
|
||||
/// Download and extract a Python distribution, retrying on failure.
|
||||
#[instrument(skip(client, installation_dir, cache_dir, reporter), fields(download = % self.key()))]
|
||||
pub async fn fetch_with_retry(
|
||||
&self,
|
||||
client: &uv_client::BaseClient,
|
||||
installation_dir: &Path,
|
||||
cache_dir: &Path,
|
||||
reinstall: bool,
|
||||
python_install_mirror: Option<&str>,
|
||||
pypy_install_mirror: Option<&str>,
|
||||
reporter: Option<&dyn Reporter>,
|
||||
) -> Result<DownloadResult, Error> {
|
||||
let mut n_past_retries = 0;
|
||||
let start_time = SystemTime::now();
|
||||
let retry_policy = client.retry_policy();
|
||||
loop {
|
||||
let result = self
|
||||
.fetch(
|
||||
client,
|
||||
installation_dir,
|
||||
cache_dir,
|
||||
reinstall,
|
||||
python_install_mirror,
|
||||
pypy_install_mirror,
|
||||
reporter,
|
||||
)
|
||||
.await;
|
||||
if result
|
||||
.as_ref()
|
||||
.err()
|
||||
.is_some_and(|err| is_extended_transient_error(err))
|
||||
{
|
||||
let retry_decision = retry_policy.should_retry(start_time, n_past_retries);
|
||||
if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision {
|
||||
debug!(
|
||||
"Transient failure while handling response for {}; retrying...",
|
||||
self.key()
|
||||
);
|
||||
let duration = execute_after
|
||||
.duration_since(SystemTime::now())
|
||||
.unwrap_or_else(|_| Duration::default());
|
||||
tokio::time::sleep(duration).await;
|
||||
n_past_retries += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and extract a Python distribution.
|
||||
#[instrument(skip(client, installation_dir, cache_dir, reporter), fields(download = % self.key()))]
|
||||
pub async fn fetch(
|
||||
&self,
|
||||
|
@ -466,8 +521,8 @@ impl ManagedPythonDownload {
|
|||
installation_dir: &Path,
|
||||
cache_dir: &Path,
|
||||
reinstall: bool,
|
||||
python_install_mirror: Option<String>,
|
||||
pypy_install_mirror: Option<String>,
|
||||
python_install_mirror: Option<&str>,
|
||||
pypy_install_mirror: Option<&str>,
|
||||
reporter: Option<&dyn Reporter>,
|
||||
) -> Result<DownloadResult, Error> {
|
||||
let url = self.download_url(python_install_mirror, pypy_install_mirror)?;
|
||||
|
@ -492,7 +547,7 @@ impl ManagedPythonDownload {
|
|||
|
||||
debug!(
|
||||
"Downloading {url} to temporary location: {}",
|
||||
temp_dir.path().simplified().display()
|
||||
temp_dir.path().simplified_display()
|
||||
);
|
||||
|
||||
let mut hashers = self
|
||||
|
@ -589,8 +644,8 @@ impl ManagedPythonDownload {
|
|||
/// appropriate environment variable, use it instead.
|
||||
fn download_url(
|
||||
&self,
|
||||
python_install_mirror: Option<String>,
|
||||
pypy_install_mirror: Option<String>,
|
||||
python_install_mirror: Option<&str>,
|
||||
pypy_install_mirror: Option<&str>,
|
||||
) -> Result<Url, Error> {
|
||||
match self.key.implementation {
|
||||
LenientImplementationName::Known(ImplementationName::CPython) => {
|
||||
|
|
|
@ -86,8 +86,8 @@ impl PythonInstallation {
|
|||
client_builder: &BaseClientBuilder<'a>,
|
||||
cache: &Cache,
|
||||
reporter: Option<&dyn Reporter>,
|
||||
python_install_mirror: Option<String>,
|
||||
pypy_install_mirror: Option<String>,
|
||||
python_install_mirror: Option<&str>,
|
||||
pypy_install_mirror: Option<&str>,
|
||||
) -> Result<Self, Error> {
|
||||
let request = request.unwrap_or_else(|| &PythonRequest::Default);
|
||||
|
||||
|
@ -132,8 +132,8 @@ impl PythonInstallation {
|
|||
client_builder: &BaseClientBuilder<'a>,
|
||||
cache: &Cache,
|
||||
reporter: Option<&dyn Reporter>,
|
||||
python_install_mirror: Option<String>,
|
||||
pypy_install_mirror: Option<String>,
|
||||
python_install_mirror: Option<&str>,
|
||||
pypy_install_mirror: Option<&str>,
|
||||
) -> Result<Self, Error> {
|
||||
let installations = ManagedPythonInstallations::from_settings()?.init()?;
|
||||
let installations_dir = installations.root();
|
||||
|
@ -145,7 +145,7 @@ impl PythonInstallation {
|
|||
|
||||
info!("Fetching requested Python...");
|
||||
let result = download
|
||||
.fetch(
|
||||
.fetch_with_retry(
|
||||
&client,
|
||||
installations_dir,
|
||||
&cache_dir,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue