Skip to content

Commit b324e80

Browse files
committedMar 7, 2023
retry crates.io requests when calling crate-search API
1 parent f99abae commit b324e80

File tree

7 files changed

+77
-25
lines changed

7 files changed

+77
-25
lines changed
 

‎src/bin/cratesfyi.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -698,9 +698,9 @@ impl Context for BinContext {
698698
let config = self.config()?;
699699
let path = config.registry_index_path.clone();
700700
if let Some(registry_url) = config.registry_url.clone() {
701-
Index::from_url(path, registry_url)
701+
Index::from_url(path, registry_url, config.crates_io_api_call_retries)
702702
} else {
703-
Index::new(path)
703+
Index::new(path, config.crates_io_api_call_retries)
704704
}?
705705
};
706706
fn repository_stats_updater(self) -> RepositoryStatsUpdater = {

‎src/config.rs

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ pub struct Config {
3838
// Gitlab authentication
3939
pub(crate) gitlab_accesstoken: Option<String>,
4040

41+
// amount of retries for external API calls, mostly crates.io
42+
pub crates_io_api_call_retries: u32,
43+
4144
// request timeout in seconds
4245
pub(crate) request_timeout: Option<Duration>,
4346
pub(crate) report_request_timeouts: bool,
@@ -120,6 +123,8 @@ impl Config {
120123
Ok(Self {
121124
build_attempts: env("DOCSRS_BUILD_ATTEMPTS", 5)?,
122125

126+
crates_io_api_call_retries: env("DOCSRS_CRATESIO_API_CALL_RETRIES", 3)?,
127+
123128
registry_index_path: env("REGISTRY_INDEX_PATH", prefix.join("crates.io-index"))?,
124129
registry_url: maybe_env("REGISTRY_URL")?,
125130
prefix: prefix.clone(),

‎src/index/api.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const APP_USER_AGENT: &str = concat!(
1515
#[derive(Debug)]
1616
pub struct Api {
1717
api_base: Option<Url>,
18+
max_retries: u32,
1819
client: reqwest::blocking::Client,
1920
}
2021

@@ -47,7 +48,7 @@ pub struct CrateOwner {
4748
}
4849

4950
impl Api {
50-
pub(super) fn new(api_base: Option<Url>) -> Result<Self> {
51+
pub(super) fn new(api_base: Option<Url>, max_retries: u32) -> Result<Self> {
5152
let headers = vec![
5253
(USER_AGENT, HeaderValue::from_static(APP_USER_AGENT)),
5354
(ACCEPT, HeaderValue::from_static("application/json")),
@@ -59,7 +60,11 @@ impl Api {
5960
.default_headers(headers)
6061
.build()?;
6162

62-
Ok(Self { api_base, client })
63+
Ok(Self {
64+
api_base,
65+
client,
66+
max_retries,
67+
})
6368
}
6469

6570
fn api_base(&self) -> Result<Url> {
@@ -120,7 +125,7 @@ impl Api {
120125

121126
let response: Response = retry(
122127
|| Ok(self.client.get(url.clone()).send()?.error_for_status()?),
123-
3,
128+
self.max_retries,
124129
)?
125130
.json()?;
126131

@@ -159,7 +164,7 @@ impl Api {
159164

160165
let response: Response = retry(
161166
|| Ok(self.client.get(url.clone()).send()?.error_for_status()?),
162-
3,
167+
self.max_retries,
163168
)?
164169
.json()?;
165170

‎src/index/mod.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ fn load_config(repo: &gix::Repository) -> Result<IndexConfig> {
3939
}
4040

4141
impl Index {
42-
pub fn from_url(path: PathBuf, url: String) -> Result<Self> {
42+
pub fn from_url(path: PathBuf, url: String, max_api_call_retries: u32) -> Result<Self> {
4343
let diff = crates_index_diff::Index::from_path_or_cloned_with_options(
4444
&path,
4545
gix::progress::Discard,
@@ -49,21 +49,23 @@ impl Index {
4949
.context("initialising registry index repository")?;
5050

5151
let config = load_config(diff.repository()).context("loading registry config")?;
52-
let api = Api::new(config.api).context("initialising registry api client")?;
52+
let api = Api::new(config.api, max_api_call_retries)
53+
.context("initialising registry api client")?;
5354
Ok(Self {
5455
path,
5556
api,
5657
repository_url: Some(url),
5758
})
5859
}
5960

60-
pub fn new(path: PathBuf) -> Result<Self> {
61+
pub fn new(path: PathBuf, max_api_call_retries: u32) -> Result<Self> {
6162
// This initializes the repository, then closes it afterwards to avoid leaking file descriptors.
6263
// See https://github.com/rust-lang/docs.rs/pull/847
6364
let diff = crates_index_diff::Index::from_path_or_cloned(&path)
6465
.context("initialising registry index repository")?;
6566
let config = load_config(diff.repository()).context("loading registry config")?;
66-
let api = Api::new(config.api).context("initialising registry api client")?;
67+
let api = Api::new(config.api, max_api_call_retries)
68+
.context("initialising registry api client")?;
6769
Ok(Self {
6870
path,
6971
api,

‎src/test/mod.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,11 @@ impl TestEnvironment {
372372
self.index
373373
.get_or_init(|| {
374374
Arc::new(
375-
Index::new(self.config().registry_index_path.clone())
376-
.expect("failed to initialize the index"),
375+
Index::new(
376+
self.config().registry_index_path.clone(),
377+
self.config().crates_io_api_call_retries,
378+
)
379+
.expect("failed to initialize the index"),
377380
)
378381
})
379382
.clone()

‎src/utils/mod.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ use serde::Serialize;
2727
use tracing::error;
2828
pub(crate) mod sized_buffer;
2929

30-
use std::thread;
31-
use std::time::Duration;
30+
use std::{future::Future, thread, time::Duration};
3231
use tracing::warn;
3332

3433
pub(crate) const APP_USER_AGENT: &str = concat!(
@@ -138,6 +137,30 @@ pub(crate) fn retry<T>(mut f: impl FnMut() -> Result<T>, max_attempts: u32) -> R
138137
unreachable!()
139138
}
140139

140+
pub(crate) async fn retry_async<T, Fut, F: FnMut() -> Fut>(mut f: F, max_attempts: u32) -> Result<T>
141+
where
142+
Fut: Future<Output = Result<T>>,
143+
{
144+
for attempt in 1.. {
145+
match f().await {
146+
Ok(result) => return Ok(result),
147+
Err(err) => {
148+
if attempt > max_attempts {
149+
return Err(err);
150+
} else {
151+
let sleep_for = 2u32.pow(attempt);
152+
warn!(
153+
"got error on attempt {}, will try again after {}s:\n{:?}",
154+
attempt, sleep_for, err
155+
);
156+
tokio::time::sleep(Duration::from_secs(sleep_for as u64)).await;
157+
}
158+
}
159+
}
160+
}
161+
unreachable!();
162+
}
163+
141164
#[cfg(test)]
142165
mod tests {
143166
use super::*;

‎src/web/releases.rs

+25-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
cdn,
66
db::Pool,
77
impl_axum_webpage,
8-
utils::{report_error, spawn_blocking},
8+
utils::{report_error, retry_async, spawn_blocking},
99
web::{
1010
axum_parse_uri_with_params, axum_redirect, encode_url_path,
1111
error::{AxumNope, AxumResult},
@@ -128,7 +128,11 @@ struct SearchResult {
128128
/// Get the search results for a crate search query
129129
///
130130
/// This delegates to the crates.io search API.
131-
async fn get_search_results(pool: Pool, query_params: &str) -> Result<SearchResult, anyhow::Error> {
131+
async fn get_search_results(
132+
pool: Pool,
133+
config: &Config,
134+
query_params: &str,
135+
) -> Result<SearchResult, anyhow::Error> {
132136
#[derive(Deserialize)]
133137
struct CratesIoSearchResult {
134138
crates: Vec<CratesIoCrate>,
@@ -178,13 +182,19 @@ async fn get_search_results(pool: Pool, query_params: &str) -> Result<SearchResu
178182
}
179183
});
180184

181-
let releases: CratesIoSearchResult = HTTP_CLIENT
182-
.get(url)
183-
.send()
184-
.await?
185-
.error_for_status()?
186-
.json()
187-
.await?;
185+
let releases: CratesIoSearchResult = retry_async(
186+
|| async {
187+
Ok(HTTP_CLIENT
188+
.get(url.clone())
189+
.send()
190+
.await?
191+
.error_for_status()?)
192+
},
193+
config.crates_io_api_call_retries,
194+
)
195+
.await?
196+
.json()
197+
.await?;
188198

189199
let names = Arc::new(
190200
releases
@@ -584,14 +594,14 @@ pub(crate) async fn search_handler(
584594
return Err(AxumNope::NoResults);
585595
}
586596

587-
get_search_results(pool, &query_params).await?
597+
get_search_results(pool, &config, &query_params).await?
588598
} else if !query.is_empty() {
589599
let query_params: String = form_urlencoded::Serializer::new(String::new())
590600
.append_pair("q", &query)
591601
.append_pair("per_page", &RELEASES_IN_RELEASES.to_string())
592602
.finish();
593603

594-
get_search_results(pool, &format!("?{}", &query_params)).await?
604+
get_search_results(pool, &config, &format!("?{}", &query_params)).await?
595605
} else {
596606
return Err(AxumNope::NoResults);
597607
};
@@ -968,6 +978,10 @@ mod tests {
968978
#[test_case(StatusCode::BAD_GATEWAY)]
969979
fn crates_io_errors_are_correctly_returned_and_we_dont_try_parsing(status: StatusCode) {
970980
wrapper(|env| {
981+
env.override_config(|config| {
982+
config.crates_io_api_call_retries = 0;
983+
});
984+
971985
let _m = mock("GET", "/api/v1/crates")
972986
.match_query(Matcher::AllOf(vec![
973987
Matcher::UrlEncoded("q".into(), "doesnt_matter_here".into()),

0 commit comments

Comments
 (0)
Please sign in to comment.