mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-18 03:13:48 +00:00
Skip GitHub fast path when rate-limited (#13033)
This commit is contained in:
parent
61265b0c14
commit
fe11ceedfa
7 changed files with 219 additions and 4 deletions
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@ pub use crate::source::{Fetch, GitSource, Reporter};
|
|||
|
||||
mod credentials;
|
||||
mod git;
|
||||
mod rate_limit;
|
||||
mod resolver;
|
||||
mod source;
|
||||
|
|
|
|||
37
crates/uv-git/src/rate_limit.rs
Normal file
37
crates/uv-git/src/rate_limit.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue