Repository: astral-sh/uv
File: crates/uv-client/src/base_client.rs (Rust)

Code before:
    /// Whether request should be retried. Waits with backoff if required.
    ///
    /// Takes the number of retries from nested layers associated with the specific `err` type as
    /// `error_retries`.
    pub async fn should_retry(&mut self, err: &(dyn Error + 'static), error_retries: u32) -> bool {
        // Check if the middleware already performed retries
        self.total_retries += error_retries;
        match retryable_on_request_failure(err) {
            Some(Retryable::Transient) => {
                // If middleware already retried, consider that in our retry budget
                let retry_decision = self
                    .retry_policy
                    .should_retry(self.start_time, self.total_retries);
                if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision {
                    let duration = execute_after
                        .duration_since(SystemTime::now())
                        .unwrap_or_else(|_| Duration::default());

                    debug!(
                        "Transient failure while handling response from {}; retrying after {:.1}s...",
                        self.url,
                        duration.as_secs_f32(),
                    );
                    // TODO(konsti): Should we show a spinner plus a message in the CLI while
                    // waiting?
                    tokio::time::sleep(duration).await;
                    self.total_retries += 1;
                    return true;
                }

                false
            }
            Some(Retryable::Fatal) | None => false,
        }
    }
}

/// Whether the error is a status code error that is retryable.
///
/// Port of `reqwest_retry::default_on_request_success`.
fn is_retryable_status_error(reqwest_err: &reqwest::Error) -> bool {
    let Some(status) = reqwest_err.status() else {
        return false;
    };
    status.is_server_error()
        || status == StatusCode::REQUEST_TIMEOUT
        || status == StatusCode::TOO_MANY_REQUESTS
}

/// Find the first source error of a specific type.
///

Review comment (bug):
I think it looks odd for a function named `should_retry` to be async and to sleep.

Maybe the alternative is to return e.g. the metadata for how long the function expects to sleep for, and handling that externally? the debug! and URL could also then be done externally...

Not sure, I think I have a better overall idea for how this may be approached later but maybe this is fine for now.

Code after:
    /// After a failed retryable request, this equals the maximum number of retries.
    pub fn total_retries(&self) -> u32 {
        self.total_retries
    }

    /// Determines whether request should be retried.
    ///
    /// Takes the number of retries from nested layers associated with the specific `err` type as
    /// `error_retries`.
    ///
    /// Returns the backoff duration if the request should be retried.
    #[must_use]
    pub fn should_retry(
        &mut self,
        err: &(dyn Error + 'static),
        error_retries: u32,
    ) -> Option<Duration> {
        // If the middleware performed any retries, consider them in our budget.
        self.total_retries += error_retries;
        match retryable_on_request_failure(err) {
            Some(Retryable::Transient) => {
                let retry_decision = self
                    .retry_policy
                    .should_retry(self.start_time, self.total_retries);
                if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision {
                    let duration = execute_after
                        .duration_since(SystemTime::now())
                        .unwrap_or_else(|_| Duration::default());

                    self.total_retries += 1;
                    return Some(duration);
                }

                None
            }
            Some(Retryable::Fatal) | None => None,
        }
    }

    /// Wait before retrying the request.
    pub async fn sleep_backoff(&self, duration: Duration) {
        debug!(
            "Transient failure while handling response from {}; retrying after {:.1}s...",
            self.url,
            duration.as_secs_f32(),
        );
        // TODO(konsti): Should we show a spinner plus a message in the CLI while
        // waiting?
        tokio::time::sleep(duration).await;
    }
}