Skip GitHub fast path when rate-limited (#13033)

This commit is contained in:
Christopher Tee 2025-06-24 15:11:41 -04:00 committed by GitHub
parent 61265b0c14
commit fe11ceedfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 219 additions and 4 deletions

View file

@ -20,6 +20,8 @@ use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
use uv_version::version;
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.
const CHECKOUT_READY_LOCK: &str = ".ok";
@ -787,7 +789,15 @@ fn github_fast_path(
}
};
let url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{github_branch_name}");
// 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 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()
@ -807,6 +817,11 @@ fn github_fast_path(
let response = request.send().await?;
if is_github_rate_limited(&response) {
// Mark that we are being rate-limited by GitHub
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()?;

View file

@ -7,5 +7,6 @@ pub use crate::source::{Fetch, GitSource, Reporter};
mod credentials;
mod git;
mod rate_limit;
mod resolver;
mod source;

View file

@ -0,0 +1,37 @@
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.
/// If we are, avoid "fast-path" attempts.
pub(crate) static GITHUB_RATE_LIMIT_STATUS: GitHubRateLimitStatus = GitHubRateLimitStatus::new();
/// 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)]
pub(crate) struct GitHubRateLimitStatus(AtomicBool);
impl GitHubRateLimitStatus {
const fn new() -> Self {
Self(AtomicBool::new(false))
}
pub(crate) fn activate(&self) {
self.0.store(true, Ordering::Relaxed);
}
pub(crate) fn is_active(&self) -> bool {
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 {
// 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
}

View file

@ -15,7 +15,10 @@ use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl};
use uv_static::EnvVars;
use uv_version::version;
use crate::{Fetch, GitSource, Reporter};
use crate::{
Fetch, GitSource, Reporter,
rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited},
};
#[derive(Debug, thiserror::Error)]
pub enum GitResolverError {
@ -85,10 +88,18 @@ 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();
let github_api_url = format!("https://api.github.com/repos/{owner}/{repo}/commits/{rev}");
let github_api_base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL)
.unwrap_or("https://api.github.com/repos".to_owned());
let github_api_url = format!("{github_api_base_url}/{owner}/{repo}/commits/{rev}");
debug!("Querying GitHub for commit at: {github_api_url}");
let mut request = client.get(&github_api_url);
@ -99,13 +110,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: {github_api_url} ({})",
response.status()
);
if is_github_rate_limited(&response) {
// Mark that we are being rate-limited by GitHub
GITHUB_RATE_LIMIT_STATUS.activate();
}
return Ok(None);
}