From 0b9e8d183e0adeb560133adf1baa0cd612181115 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Mon, 21 Apr 2025 19:07:52 -0400 Subject: [PATCH 01/23] Error and skip on 429 --- crates/uv-distribution/src/source/mod.rs | 7 +++++++ crates/uv-git/src/resolver.rs | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 00fee7a2e7020..bb0718f1fb3ad 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1627,6 +1627,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(None) => { // Nothing to do. } + Err(uv_git::GitResolverError::Reqwest(error)) + if error.status() == Some(StatusCode::TOO_MANY_REQUESTS) => + { + // With 429 hitting GitHub may no longer be the fast path. + // Error and skip the subsequent Git fetch. + return Err(error.into()) + } Err(err) => { debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})"); } diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index 2ac85c5f5c19f..524b541155737 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use dashmap::mapref::one::Ref; use dashmap::DashMap; use fs_err::tokio as fs; +use reqwest::StatusCode; use reqwest_middleware::ClientWithMiddleware; use tracing::debug; @@ -86,13 +87,20 @@ impl GitResolver { ); let response = request.send().await?; - if !response.status().is_success() { + let status = response.status(); + if !status.is_success() { // Returns a 404 if the repository does not exist, and a 422 if GitHub is unable to // resolve the requested rev. - debug!( - "GitHub API request failed for: {url} ({})", - response.status() - ); + debug!("GitHub API request failed for: {url} ({})", status); + + // Return error for 429 as not only is there no fast path, + // subsequent git operations by the caller should be skipped. + if status == StatusCode::TOO_MANY_REQUESTS { + // Unwrap won't panic on a 429. + let err = response.error_for_status().unwrap_err(); + return Err(err.into()); + } + return Ok(None); } From 3d0ea051da9661664e26641e361a37448a5edfb4 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Mon, 21 Apr 2025 19:22:03 -0400 Subject: [PATCH 02/23] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20cargo=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv-distribution/src/source/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index bb0718f1fb3ad..218b80e1fc7a4 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1632,7 +1632,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { { // With 429 hitting GitHub may no longer be the fast path. // Error and skip the subsequent Git fetch. - return Err(error.into()) + return Err(error.into()); } Err(err) => { debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})"); From e11c0ea989f9dd7aa634b413093792286a0ef3cf Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Mon, 21 Apr 2025 19:24:30 -0400 Subject: [PATCH 03/23] =?UTF-8?q?=F0=9F=93=9D=20Improve=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv-distribution/src/source/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 218b80e1fc7a4..13d7217467373 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1630,7 +1630,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Err(uv_git::GitResolverError::Reqwest(error)) if error.status() == Some(StatusCode::TOO_MANY_REQUESTS) => { - // With 429 hitting GitHub may no longer be the fast path. + // With 429, fetching from GitHub may no longer be the fast path. // Error and skip the subsequent Git fetch. return Err(error.into()); } From 7dd16fdec73349d5df2e32c4be72819656fc7810 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 22 Apr 2025 09:33:32 -0400 Subject: [PATCH 04/23] Re-trigger CI From 1ff93df366f9ea58dce3f95e7d1f916b72432973 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 17 May 2025 15:23:18 -0400 Subject: [PATCH 05/23] Update implementation based on feedback --- crates/uv-git/src/git.rs | 13 +++++++++++++ crates/uv-git/src/lib.rs | 1 + crates/uv-git/src/rate_limit.rs | 26 ++++++++++++++++++++++++++ crates/uv-git/src/resolver.rs | 15 +++++++++------ 4 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 crates/uv-git/src/rate_limit.rs diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index af82c56fa1659..56f6d09f12e9a 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -19,6 +19,8 @@ use uv_git_types::{GitHubRepository, GitOid, GitReference}; use uv_static::EnvVars; use uv_version::version; +use crate::rate_limit::GITHUB_RATE_LIMIT_STATUS; + /// A file indicates that if present, `git reset` has been done and a repo /// checkout is ready to go. See [`GitCheckout::reset`] for why we need this. const CHECKOUT_READY_LOCK: &str = ".ok"; @@ -786,6 +788,12 @@ fn github_fast_path( } }; + // Check if we're rate-limited by GitHub before determining the FastPathRev + if GITHUB_RATE_LIMIT_STATUS.is_active() { + debug!("Skipping GitHub fast path attempt for: {url} (rate-limited)"); + return Ok(FastPathRev::Indeterminate); + } + let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{github_branch_name}"); let runtime = tokio::runtime::Builder::new_current_thread() @@ -806,6 +814,11 @@ fn github_fast_path( let response = request.send().await?; + // Mark that we are currently being rate-limited by GitHub. + if response.status() == StatusCode::TOO_MANY_REQUESTS { + GITHUB_RATE_LIMIT_STATUS.activate(); + } + // GitHub returns a 404 if the repository does not exist, and a 422 if it exists but GitHub // is unable to resolve the requested revision. response.error_for_status_ref()?; diff --git a/crates/uv-git/src/lib.rs b/crates/uv-git/src/lib.rs index 89a9d1558b8f6..1cc4bcc029ee4 100644 --- a/crates/uv-git/src/lib.rs +++ b/crates/uv-git/src/lib.rs @@ -7,5 +7,6 @@ pub use crate::source::{Fetch, GitSource, Reporter}; mod credentials; mod git; +mod rate_limit; mod resolver; mod source; diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs new file mode 100644 index 0000000000000..b403a5d302d05 --- /dev/null +++ b/crates/uv-git/src/rate_limit.rs @@ -0,0 +1,26 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::LazyLock; + +/// A global state on whether we are being rate-limited by GitHub's REST API. +/// If we are, avoid "fast-path" attempts. +pub(crate) static GITHUB_RATE_LIMIT_STATUS: LazyLock = + LazyLock::new(GitHubRateLimitStatus::default); + +/// GitHub REST API rate limit status tracker. +/// +/// ## Assumptions +/// +/// The rate limit timeout duration is much longer than the runtime of a `uv` command. +/// And so we do not need to invalidate this state based on `x-ratelimit-reset`. +#[derive(Debug, Default)] +pub(crate) struct GitHubRateLimitStatus(AtomicBool); + +impl GitHubRateLimitStatus { + pub(crate) fn activate(&self) { + self.0.store(true, Ordering::SeqCst); + } + + pub(crate) fn is_active(&self) -> bool { + self.0.load(Ordering::SeqCst) + } +} diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index 92cadafe5e982..3a0a4c1ac7128 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -15,7 +15,7 @@ use uv_fs::LockedFile; use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; use uv_version::version; -use crate::{Fetch, GitSource, Reporter}; +use crate::{rate_limit::GITHUB_RATE_LIMIT_STATUS, Fetch, GitSource, Reporter}; #[derive(Debug, thiserror::Error)] pub enum GitResolverError { @@ -73,6 +73,12 @@ impl GitResolver { return Ok(None); }; + // Check if we're rate-limited by GitHub, before determining the Git reference + if GITHUB_RATE_LIMIT_STATUS.is_active() { + debug!("Rate-limited by GitHub. Skipping GitHub fast path attempt for: {url}"); + return Ok(None); + } + // Determine the Git reference. let rev = url.reference().as_rev(); @@ -93,12 +99,9 @@ impl GitResolver { // resolve the requested rev. debug!("GitHub API request failed for: {url} ({})", status); - // Return error for 429 as not only is there no fast path, - // subsequent git operations by the caller should be skipped. + // Mark that we are currently being rate-limited by GitHub. if status == StatusCode::TOO_MANY_REQUESTS { - // Unwrap won't panic on a 429. - let err = response.error_for_status().unwrap_err(); - return Err(err.into()); + GITHUB_RATE_LIMIT_STATUS.activate(); } return Ok(None); From 8ff21120d90ab9045612baf17b23378086353ef5 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 17 May 2025 15:25:53 -0400 Subject: [PATCH 06/23] =?UTF-8?q?=F0=9F=94=A5=20Remove=20previous=20implem?= =?UTF-8?q?entation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv-distribution/src/source/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 9b75107257393..717e2c769bec7 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1629,13 +1629,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(None) => { // Nothing to do. } - Err(uv_git::GitResolverError::Reqwest(error)) - if error.status() == Some(StatusCode::TOO_MANY_REQUESTS) => - { - // With 429, fetching from GitHub may no longer be the fast path. - // Error and skip the subsequent Git fetch. - return Err(error.into()); - } Err(err) => { debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})"); } From eab2d321f8de6c2c1119cb0e92659003b96a9721 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 17 May 2025 15:38:55 -0400 Subject: [PATCH 07/23] Format docs --- crates/uv-git/src/rate_limit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index b403a5d302d05..1e987db9dfa79 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -9,7 +9,7 @@ pub(crate) static GITHUB_RATE_LIMIT_STATUS: LazyLock = /// GitHub REST API rate limit status tracker. /// /// ## Assumptions -/// +/// /// The rate limit timeout duration is much longer than the runtime of a `uv` command. /// And so we do not need to invalidate this state based on `x-ratelimit-reset`. #[derive(Debug, Default)] From c14c21ba615690a5860039041dfec4c324368818 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 09:59:26 -0400 Subject: [PATCH 08/23] =?UTF-8?q?=F0=9F=8E=A8=20Reformat=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv-git/src/rate_limit.rs | 2 +- crates/uv-git/src/resolver.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index 1e987db9dfa79..cc46431609c6c 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -1,5 +1,5 @@ -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::LazyLock; +use std::sync::atomic::{AtomicBool, Ordering}; /// A global state on whether we are being rate-limited by GitHub's REST API. /// If we are, avoid "fast-path" attempts. diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index f1694c0bbd403..e78e3557effd1 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -15,7 +15,7 @@ use uv_fs::LockedFile; use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; use uv_version::version; -use crate::{rate_limit::GITHUB_RATE_LIMIT_STATUS, Fetch, GitSource, Reporter}; +use crate::{Fetch, GitSource, Reporter, rate_limit::GITHUB_RATE_LIMIT_STATUS}; #[derive(Debug, thiserror::Error)] pub enum GitResolverError { From a2a9dac6fa0dea971d6013c4a4b5bdc6bf43d97e Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 16:22:17 -0400 Subject: [PATCH 09/23] =?UTF-8?q?=F0=9F=94=A7=20Add=20`UV=5FGITHUB=5FFAST?= =?UTF-8?q?=5FPATH=5FURL`=20env=20var?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv-static/src/env_vars.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 58191fe64411e..6c210a47e4f26 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -667,6 +667,10 @@ impl EnvVars { #[attr_hidden] pub const UV_TEST_INDEX_URL: &'static str = "UV_TEST_INDEX_URL"; + /// Used to set the GitHub fast-path url for tests. + #[attr_hidden] + pub const UV_GITHUB_FAST_PATH_URL: &'static str = "UV_GITHUB_FAST_PATH_URL"; + /// Hide progress messages with non-deterministic order in tests. #[attr_hidden] pub const UV_TEST_NO_CLI_PROGRESS: &'static str = "UV_TEST_NO_CLI_PROGRESS"; From 5e04f8883a2cb70b657c4fc9b1a218af37f9fc9a Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 16:22:43 -0400 Subject: [PATCH 10/23] Apply env var change --- crates/uv-git/src/git.rs | 4 +++- crates/uv-git/src/resolver.rs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index 96d90833abef0..5c3904bee74f9 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -794,7 +794,9 @@ fn github_fast_path( return Ok(FastPathRev::Indeterminate); } - let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{github_branch_name}"); + let base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL) + .unwrap_or("https://api.github.com/repos".to_owned()); + let url = format!("{base_url}/{owner}/{repo}/commits/{github_branch_name}"); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index e78e3557effd1..603e3e20bc5fd 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -13,6 +13,7 @@ use tracing::debug; use uv_cache_key::{RepositoryUrl, cache_digest}; use uv_fs::LockedFile; use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; +use uv_static::EnvVars; use uv_version::version; use crate::{Fetch, GitSource, Reporter, rate_limit::GITHUB_RATE_LIMIT_STATUS}; @@ -82,7 +83,9 @@ impl GitResolver { // Determine the Git reference. let rev = url.reference().as_rev(); - let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{rev}"); + let base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL) + .unwrap_or("https://api.github.com/repos".to_owned()); + let url = format!("{base_url}/{owner}/{repo}/commits/{rev}"); debug!("Querying GitHub for commit at: {url}"); let mut request = client.get(&url); From 1fb7bca26b480f97020d784e0c137c5c8ab8b6f9 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 16:23:04 -0400 Subject: [PATCH 11/23] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20GitResolve?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv/tests/it/pip_install.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 7a205f89138bb..05dda5df900a1 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2071,6 +2071,36 @@ fn install_git_public_https_missing_branch_or_tag() { "###); } +#[tokio::test] +#[cfg(feature = "git")] +async fn install_git_public_rate_limited_by_github_rest_api() { + use uv_client::DEFAULT_RETRIES; + + let context = TestContext::new("3.12"); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(429)) + .expect(1 + u64::from(DEFAULT_RETRIES)) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .pip_install() + .arg("pip-test-package @ git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7") + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + pip-test-package==0.1.1 (from git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7) + "); +} + /// Install a package from a public GitHub repository at a ref that does not exist #[test] #[cfg(feature = "git")] From 29ce14cebe13bb1cf55eebdbbb6ab2715ec0585e Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 16:41:01 -0400 Subject: [PATCH 12/23] =?UTF-8?q?=E2=9C=85=20Add=20unit=20test=20for=20`gi?= =?UTF-8?q?thub=5Ffast=5Fpath`=20fn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv/tests/it/edit.rs | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index dda76b0b92e35..c0084da92a111 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -496,6 +496,48 @@ fn add_git_private_raw() -> Result<()> { Ok(()) } +#[tokio::test] +#[cfg(feature = "git")] +async fn add_git_private_rate_limited_by_github_rest_api() -> Result<()> { + use uv_client::DEFAULT_RETRIES; + + let context = TestContext::new("3.12"); + let token = decode_token(READ_ONLY_GITHUB_TOKEN); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(429)) + .expect(1 + u64::from(DEFAULT_RETRIES)) + .mount(&server) + .await; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context + .add() + .arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")) + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-private-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "); + + Ok(()) +} + #[test] #[cfg(feature = "git")] fn add_git_error() -> Result<()> { From 7a7fd111223c5b348e01682097f3248387bcb293 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 21:49:59 -0400 Subject: [PATCH 13/23] Do not retry on HTTP 429 --- crates/uv-client/src/base_client.rs | 22 ++++++++++++++++++++++ crates/uv/tests/it/edit.rs | 4 +--- crates/uv/tests/it/pip_install.rs | 6 ++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 2db0a920e9baf..27653101d017d 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use std::time::Duration; use std::{env, io, iter}; +use http::StatusCode; use itertools::Itertools; use reqwest::{Client, ClientBuilder, Proxy, Response}; use reqwest_middleware::{ClientWithMiddleware, Middleware}; @@ -452,6 +453,9 @@ impl RetryableStrategy for UvRetryableStrategy { { Some(Retryable::Transient) } + Some(Retryable::Transient) if res.as_ref().is_ok_and(is_extended_fatal_response) => { + Some(Retryable::Fatal) + } default => default, }; @@ -505,6 +509,24 @@ pub fn is_extended_transient_error(err: &dyn Error) -> bool { false } +/// Check for additional fatal response kinds not supported by the default retry strategy in `reqwest_retry`. +/// +/// These cases should be considered [`Retryable::Fatal`]. +fn is_extended_fatal_response(response: &Response) -> bool { + // First, try to show a nice trace log + let status = response.status(); + trace!( + "Considering skipping retry of response HTTP {status} for {}", + response.url() + ); + if status == StatusCode::TOO_MANY_REQUESTS { + trace!("Skipping retry for error: `TooManyRequests"); + true + } else { + false + } +} + /// Find the first source error of a specific type. /// /// See diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index c0084da92a111..55659addb6f96 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -499,15 +499,13 @@ fn add_git_private_raw() -> Result<()> { #[tokio::test] #[cfg(feature = "git")] async fn add_git_private_rate_limited_by_github_rest_api() -> Result<()> { - use uv_client::DEFAULT_RETRIES; - let context = TestContext::new("3.12"); let token = decode_token(READ_ONLY_GITHUB_TOKEN); let server = MockServer::start().await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(429)) - .expect(1 + u64::from(DEFAULT_RETRIES)) + .expect(1) .mount(&server) .await; diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 05dda5df900a1..2949463b32157 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2074,14 +2074,12 @@ fn install_git_public_https_missing_branch_or_tag() { #[tokio::test] #[cfg(feature = "git")] async fn install_git_public_rate_limited_by_github_rest_api() { - use uv_client::DEFAULT_RETRIES; - let context = TestContext::new("3.12"); let server = MockServer::start().await; Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(429)) - .expect(1 + u64::from(DEFAULT_RETRIES)) + .respond_with(ResponseTemplate::new(408)) + .expect(1) .mount(&server) .await; From f3f835521588c09aaf5fcdca569a10129204c0ae Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 20 May 2025 22:01:49 -0400 Subject: [PATCH 14/23] Revert "Do not retry on HTTP 429" This reverts commit 7a7fd111223c5b348e01682097f3248387bcb293. --- crates/uv-client/src/base_client.rs | 22 ---------------------- crates/uv/tests/it/edit.rs | 4 +++- crates/uv/tests/it/pip_install.rs | 6 ++++-- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 27653101d017d..2db0a920e9baf 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use std::time::Duration; use std::{env, io, iter}; -use http::StatusCode; use itertools::Itertools; use reqwest::{Client, ClientBuilder, Proxy, Response}; use reqwest_middleware::{ClientWithMiddleware, Middleware}; @@ -453,9 +452,6 @@ impl RetryableStrategy for UvRetryableStrategy { { Some(Retryable::Transient) } - Some(Retryable::Transient) if res.as_ref().is_ok_and(is_extended_fatal_response) => { - Some(Retryable::Fatal) - } default => default, }; @@ -509,24 +505,6 @@ pub fn is_extended_transient_error(err: &dyn Error) -> bool { false } -/// Check for additional fatal response kinds not supported by the default retry strategy in `reqwest_retry`. -/// -/// These cases should be considered [`Retryable::Fatal`]. -fn is_extended_fatal_response(response: &Response) -> bool { - // First, try to show a nice trace log - let status = response.status(); - trace!( - "Considering skipping retry of response HTTP {status} for {}", - response.url() - ); - if status == StatusCode::TOO_MANY_REQUESTS { - trace!("Skipping retry for error: `TooManyRequests"); - true - } else { - false - } -} - /// Find the first source error of a specific type. /// /// See diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 55659addb6f96..c0084da92a111 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -499,13 +499,15 @@ fn add_git_private_raw() -> Result<()> { #[tokio::test] #[cfg(feature = "git")] async fn add_git_private_rate_limited_by_github_rest_api() -> Result<()> { + use uv_client::DEFAULT_RETRIES; + let context = TestContext::new("3.12"); let token = decode_token(READ_ONLY_GITHUB_TOKEN); let server = MockServer::start().await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(429)) - .expect(1) + .expect(1 + u64::from(DEFAULT_RETRIES)) .mount(&server) .await; diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 2949463b32157..05dda5df900a1 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2074,12 +2074,14 @@ fn install_git_public_https_missing_branch_or_tag() { #[tokio::test] #[cfg(feature = "git")] async fn install_git_public_rate_limited_by_github_rest_api() { + use uv_client::DEFAULT_RETRIES; + let context = TestContext::new("3.12"); let server = MockServer::start().await; Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(408)) - .expect(1) + .respond_with(ResponseTemplate::new(429)) + .expect(1 + u64::from(DEFAULT_RETRIES)) .mount(&server) .await; From 67f9a65de590d07606985211ad01b18243410dd6 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 21 Jun 2025 09:22:06 -0400 Subject: [PATCH 15/23] Remove use of LazyLock --- crates/uv-git/src/rate_limit.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index cc46431609c6c..216e24d854cc8 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -1,10 +1,8 @@ -use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; /// A global state on whether we are being rate-limited by GitHub's REST API. /// If we are, avoid "fast-path" attempts. -pub(crate) static GITHUB_RATE_LIMIT_STATUS: LazyLock = - LazyLock::new(GitHubRateLimitStatus::default); +pub(crate) static GITHUB_RATE_LIMIT_STATUS: GitHubRateLimitStatus = GitHubRateLimitStatus::new(); /// GitHub REST API rate limit status tracker. /// @@ -12,10 +10,14 @@ pub(crate) static GITHUB_RATE_LIMIT_STATUS: LazyLock = /// /// The rate limit timeout duration is much longer than the runtime of a `uv` command. /// And so we do not need to invalidate this state based on `x-ratelimit-reset`. -#[derive(Debug, Default)] +#[derive(Debug)] pub(crate) struct GitHubRateLimitStatus(AtomicBool); impl GitHubRateLimitStatus { + pub(crate) const fn new() -> Self { + Self(AtomicBool::new(false)) + } + pub(crate) fn activate(&self) { self.0.store(true, Ordering::SeqCst); } From dc85d68c6e30df056286978dc44073b8a47226f6 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 21 Jun 2025 09:22:43 -0400 Subject: [PATCH 16/23] Change memory ordering --- crates/uv-git/src/rate_limit.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index 216e24d854cc8..e7a7ff8f645c9 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -19,10 +19,10 @@ impl GitHubRateLimitStatus { } pub(crate) fn activate(&self) { - self.0.store(true, Ordering::SeqCst); + self.0.store(true, Ordering::Relaxed); } pub(crate) fn is_active(&self) -> bool { - self.0.load(Ordering::SeqCst) + self.0.load(Ordering::Relaxed) } } From a0936666dec18cda2bf75ea27349ba89f3b536b5 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 21 Jun 2025 09:26:35 -0400 Subject: [PATCH 17/23] Check rate-limited status from response header --- crates/uv-git/src/git.rs | 6 +++--- crates/uv-git/src/rate_limit.rs | 14 ++++++++++++++ crates/uv-git/src/resolver.rs | 10 ++++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index 46a30908fd744..4ee4c2670ec88 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -20,7 +20,7 @@ use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_version::version; -use crate::rate_limit::GITHUB_RATE_LIMIT_STATUS; +use crate::rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited}; /// A file indicates that if present, `git reset` has been done and a repo /// checkout is ready to go. See [`GitCheckout::reset`] for why we need this. @@ -817,8 +817,8 @@ fn github_fast_path( let response = request.send().await?; - // Mark that we are currently being rate-limited by GitHub. - if response.status() == StatusCode::TOO_MANY_REQUESTS { + if is_github_rate_limited(&response) { + // Mark that we are being rate-limited by GitHub GITHUB_RATE_LIMIT_STATUS.activate(); } diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index e7a7ff8f645c9..cafabde1f18bb 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -1,3 +1,4 @@ +use reqwest::Response; use std::sync::atomic::{AtomicBool, Ordering}; /// A global state on whether we are being rate-limited by GitHub's REST API. @@ -26,3 +27,16 @@ impl GitHubRateLimitStatus { self.0.load(Ordering::Relaxed) } } + + +/// Determine if GitHub is applying rate-limiting based on the response +pub(crate) fn is_github_rate_limited(response: &Response) -> bool { + // Check if our remaining quota is within the rate limit window. + // If it's "0", mark that we are currently being rate-limited by GitHub. + // Source: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#checking-the-status-of-your-rate-limit. + response + .headers() + .get("x-ratelimit-remaining") + .and_then(|h| h.to_str().ok()) + == Some("0") +} diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index eb6c4a26efd7f..34751f137b6eb 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use dashmap::DashMap; use dashmap::mapref::one::Ref; use fs_err::tokio as fs; -use reqwest::StatusCode; use reqwest_middleware::ClientWithMiddleware; use tracing::debug; @@ -16,7 +15,10 @@ use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; use uv_static::EnvVars; use uv_version::version; -use crate::{Fetch, GitSource, Reporter, rate_limit::GITHUB_RATE_LIMIT_STATUS}; +use crate::{ + Fetch, GitSource, Reporter, + rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited}, +}; #[derive(Debug, thiserror::Error)] pub enum GitResolverError { @@ -106,8 +108,8 @@ impl GitResolver { // resolve the requested rev. debug!("GitHub API request failed for: {url} ({})", status); - // Mark that we are currently being rate-limited by GitHub. - if status == StatusCode::TOO_MANY_REQUESTS { + if is_github_rate_limited(&response) { + // Mark that we are being rate-limited by GitHub GITHUB_RATE_LIMIT_STATUS.activate(); } From 912dbaa3aa274938b491c718e3fde6bdefea5bf8 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 21 Jun 2025 10:43:22 -0400 Subject: [PATCH 18/23] Determine rate limit via status codes --- crates/uv-git/src/rate_limit.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index cafabde1f18bb..7cfbb9cb90c28 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -1,4 +1,4 @@ -use reqwest::Response; +use reqwest::{Response, StatusCode}; use std::sync::atomic::{AtomicBool, Ordering}; /// A global state on whether we are being rate-limited by GitHub's REST API. @@ -28,15 +28,10 @@ impl GitHubRateLimitStatus { } } - /// Determine if GitHub is applying rate-limiting based on the response pub(crate) fn is_github_rate_limited(response: &Response) -> bool { - // Check if our remaining quota is within the rate limit window. - // If it's "0", mark that we are currently being rate-limited by GitHub. - // Source: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#checking-the-status-of-your-rate-limit. - response - .headers() - .get("x-ratelimit-remaining") - .and_then(|h| h.to_str().ok()) - == Some("0") + // HTTP 403 and 429 are possible status codes in the event of a primary or secondary rate limit. + // Source: https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2022-11-28#rate-limit-errors + let status_code = response.status(); + status_code == StatusCode::FORBIDDEN || status_code == StatusCode::TOO_MANY_REQUESTS } From 872d6ee0f2f9f39761875cf7334855590e68baa7 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Sat, 21 Jun 2025 10:43:30 -0400 Subject: [PATCH 19/23] =?UTF-8?q?=E2=9C=85=20Update=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/uv/tests/it/edit.rs | 44 +++++++++++++++++++++++++++++-- crates/uv/tests/it/pip_install.rs | 32 ++++++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index c91003c8b223c..6bdaec17bd1f8 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -496,7 +496,47 @@ fn add_git_private_raw() -> Result<()> { #[tokio::test] #[cfg(feature = "git")] -async fn add_git_private_rate_limited_by_github_rest_api() -> Result<()> { +async fn add_git_private_rate_limited_by_github_rest_api_403_response() -> Result<()> { + let context = TestContext::new("3.12"); + let token = decode_token(READ_ONLY_GITHUB_TOKEN); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(403)) + .expect(1) + .mount(&server) + .await; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context + .add() + .arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")) + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-private-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "); + + Ok(()) +} + +#[tokio::test] +#[cfg(feature = "git")] +async fn add_git_private_rate_limited_by_github_rest_api_429_response() -> Result<()> { use uv_client::DEFAULT_RETRIES; let context = TestContext::new("3.12"); @@ -505,7 +545,7 @@ async fn add_git_private_rate_limited_by_github_rest_api() -> Result<()> { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(429)) - .expect(1 + u64::from(DEFAULT_RETRIES)) + .expect(1 + u64::from(DEFAULT_RETRIES)) // Middleware retries on 429 by default .mount(&server) .await; diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index ac18db696007f..5edec7a885020 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2068,7 +2068,35 @@ fn install_git_public_https_missing_branch_or_tag() { #[tokio::test] #[cfg(feature = "git")] -async fn install_git_public_rate_limited_by_github_rest_api() { +async fn install_git_public_rate_limited_by_github_rest_api_403_response() { + let context = TestContext::new("3.12"); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(403)) + .expect(1) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .pip_install() + .arg("pip-test-package @ git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7") + .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + pip-test-package==0.1.1 (from git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7) + "); +} + +#[tokio::test] +#[cfg(feature = "git")] +async fn install_git_public_rate_limited_by_github_rest_api_429_response() { use uv_client::DEFAULT_RETRIES; let context = TestContext::new("3.12"); @@ -2076,7 +2104,7 @@ async fn install_git_public_rate_limited_by_github_rest_api() { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(429)) - .expect(1 + u64::from(DEFAULT_RETRIES)) + .expect(1 + u64::from(DEFAULT_RETRIES)) // Middleware retries on 429 by default .mount(&server) .await; From 8875411242b74805fa7085eefe2cf711daa0affe Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 24 Jun 2025 08:59:44 -0400 Subject: [PATCH 20/23] Limit visibility for GitHubRateLimitStatus::new --- crates/uv-git/src/rate_limit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-git/src/rate_limit.rs b/crates/uv-git/src/rate_limit.rs index 7cfbb9cb90c28..4d277e6522f23 100644 --- a/crates/uv-git/src/rate_limit.rs +++ b/crates/uv-git/src/rate_limit.rs @@ -15,7 +15,7 @@ pub(crate) static GITHUB_RATE_LIMIT_STATUS: GitHubRateLimitStatus = GitHubRateLi pub(crate) struct GitHubRateLimitStatus(AtomicBool); impl GitHubRateLimitStatus { - pub(crate) const fn new() -> Self { + const fn new() -> Self { Self(AtomicBool::new(false)) } From e9b0a90a008931a884ed3c4c1e3de7a7bcb09f1b Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 24 Jun 2025 09:06:28 -0400 Subject: [PATCH 21/23] Update tests --- crates/uv/tests/it/pip_install.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 5edec7a885020..604b8db1507c6 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2080,7 +2080,7 @@ async fn install_git_public_rate_limited_by_github_rest_api_403_response() { uv_snapshot!(context.filters(), context .pip_install() - .arg("pip-test-package @ git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7") + .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage") .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" success: true exit_code: 0 @@ -2090,7 +2090,7 @@ async fn install_git_public_rate_limited_by_github_rest_api_403_response() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + pip-test-package==0.1.1 (from git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7) + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) "); } @@ -2110,7 +2110,7 @@ async fn install_git_public_rate_limited_by_github_rest_api_429_response() { uv_snapshot!(context.filters(), context .pip_install() - .arg("pip-test-package @ git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7") + .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage") .env("UV_GITHUB_FAST_PATH_URL", server.uri()), @r" success: true exit_code: 0 @@ -2120,7 +2120,7 @@ async fn install_git_public_rate_limited_by_github_rest_api_429_response() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + pip-test-package==0.1.1 (from git+https://github.com/pypa/pip-test-package@5547fa909e83df8bd743d3978d6667497983a4b7) + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) "); } From ab45228b16e80fdb4da7a18c0bbe9cc4265fccee Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 24 Jun 2025 13:22:49 -0400 Subject: [PATCH 22/23] Fix lint --- crates/uv-git/src/resolver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index 8b35d6622cc37..3c12fc5892292 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -123,7 +123,7 @@ impl GitResolver { // Mark that we are being rate-limited by GitHub GITHUB_RATE_LIMIT_STATUS.activate(); } - + return Ok(None); } From 9d94d9bfa2a7f1eaf3fff9713fdb2742bdf76ca3 Mon Sep 17 00:00:00 2001 From: Christopher Tee Date: Tue, 24 Jun 2025 13:45:22 -0400 Subject: [PATCH 23/23] Retrigger CI