Skip to content

Commit c1da457

Browse files
committed
Add retry support to sparse registries
1 parent 9467f81 commit c1da457

File tree

6 files changed

+109
-83
lines changed

6 files changed

+109
-83
lines changed

src/cargo/core/package.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::core::{Dependency, Manifest, PackageId, SourceId, Target};
2727
use crate::core::{SourceMap, Summary, Workspace};
2828
use crate::ops;
2929
use crate::util::config::PackageCacheLock;
30-
use crate::util::errors::{CargoResult, HttpNot200};
30+
use crate::util::errors::{CargoResult, HttpNotSuccessful};
3131
use crate::util::interning::InternedString;
3232
use crate::util::network::Retry;
3333
use crate::util::{self, internal, Config, Progress, ProgressStyle};
@@ -868,18 +868,19 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> {
868868
let code = handle.response_code()?;
869869
if code != 200 && code != 0 {
870870
let url = handle.effective_url()?.unwrap_or(url);
871-
return Err(HttpNot200 {
871+
return Err(HttpNotSuccessful {
872872
code,
873873
url: url.to_string(),
874+
body: data,
874875
}
875876
.into());
876877
}
877-
Ok(())
878+
Ok(data)
878879
})
879880
.with_context(|| format!("failed to download from `{}`", dl.url))?
880881
};
881882
match ret {
882-
Some(()) => break (dl, data),
883+
Some(data) => break (dl, data),
883884
None => {
884885
self.pending_ids.insert(dl.id);
885886
self.enqueue(dl, handle)?

src/cargo/ops/registry.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_I
2828
use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange};
2929
use crate::util::errors::CargoResult;
3030
use crate::util::important_paths::find_root_manifest_for_wd;
31-
use crate::util::IntoUrl;
31+
use crate::util::{truncate_with_ellipsis, IntoUrl};
3232
use crate::{drop_print, drop_println, version};
3333

3434
mod auth;
@@ -963,18 +963,6 @@ pub fn search(
963963
limit: u32,
964964
reg: Option<String>,
965965
) -> CargoResult<()> {
966-
fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
967-
// We should truncate at grapheme-boundary and compute character-widths,
968-
// yet the dependencies on unicode-segmentation and unicode-width are
969-
// not worth it.
970-
let mut chars = s.chars();
971-
let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
972-
if chars.next().is_some() {
973-
prefix.push('…');
974-
}
975-
prefix
976-
}
977-
978966
let (mut registry, _, source_id) =
979967
registry(config, None, index.as_deref(), reg.as_deref(), false, false)?;
980968
let (crates, total_crates) = registry.search(query, limit).with_context(|| {

src/cargo/sources/registry/http_remote.rs

Lines changed: 69 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use crate::ops;
77
use crate::sources::registry::download;
88
use crate::sources::registry::MaybeLock;
99
use crate::sources::registry::{LoadResponse, RegistryConfig, RegistryData};
10-
use crate::util::errors::CargoResult;
11-
use crate::util::{Config, Filesystem, IntoUrl, Progress, ProgressStyle};
10+
use crate::util::errors::{CargoResult, HttpNotSuccessful};
11+
use crate::util::network::Retry;
12+
use crate::util::{internal, Config, Filesystem, IntoUrl, Progress, ProgressStyle};
1213
use anyhow::Context;
1314
use cargo_util::paths;
1415
use curl::easy::{HttpVersion, List};
@@ -83,15 +84,12 @@ pub struct Downloads<'cfg> {
8384
/// When a download is started, it is added to this map. The key is a
8485
/// "token" (see `Download::token`). It is removed once the download is
8586
/// finished.
86-
pending: HashMap<usize, (Download, EasyHandle)>,
87-
/// Set of paths currently being downloaded, mapped to their tokens.
87+
pending: HashMap<usize, (Download<'cfg>, EasyHandle)>,
88+
/// Set of paths currently being downloaded.
8889
/// This should stay in sync with `pending`.
89-
pending_ids: HashMap<PathBuf, usize>,
90-
/// The final result of each download. A pair `(token, result)`. This is a
91-
/// temporary holding area, needed because curl can report multiple
92-
/// downloads at once, but the main loop (`wait`) is written to only
93-
/// handle one at a time.
94-
results: HashMap<PathBuf, Result<CompletedDownload, curl::Error>>,
90+
pending_ids: HashSet<PathBuf>,
91+
/// The final result of each download.
92+
results: HashMap<PathBuf, CargoResult<CompletedDownload>>,
9593
/// The next ID to use for creating a token (see `Download::token`).
9694
next: usize,
9795
/// Progress bar.
@@ -100,7 +98,7 @@ pub struct Downloads<'cfg> {
10098
downloads_finished: usize,
10199
}
102100

103-
struct Download {
101+
struct Download<'cfg> {
104102
/// The token for this download, used as the key of the `Downloads::pending` map
105103
/// and stored in `EasyHandle` as well.
106104
token: usize,
@@ -117,6 +115,9 @@ struct Download {
117115
/// Statistics updated from the progress callback in libcurl.
118116
total: Cell<u64>,
119117
current: Cell<u64>,
118+
119+
/// Logic used to track retrying this download if it's a spurious failure.
120+
retry: Retry<'cfg>,
120121
}
121122

122123
struct CompletedDownload {
@@ -155,7 +156,7 @@ impl<'cfg> HttpRegistry<'cfg> {
155156
downloads: Downloads {
156157
next: 0,
157158
pending: HashMap::new(),
158-
pending_ids: HashMap::new(),
159+
pending_ids: HashSet::new(),
159160
results: HashMap::new(),
160161
progress: RefCell::new(Some(Progress::with_style(
161162
"Fetch",
@@ -217,37 +218,60 @@ impl<'cfg> HttpRegistry<'cfg> {
217218
);
218219

219220
// Collect the results from the Multi handle.
220-
let pending = &mut self.downloads.pending;
221-
self.multi.messages(|msg| {
222-
let token = msg.token().expect("failed to read token");
223-
let (_, handle) = &pending[&token];
224-
let result = match msg.result_for(handle) {
225-
Some(result) => result,
226-
None => return, // transfer is not yet complete.
227-
};
228-
229-
let (download, mut handle) = pending.remove(&token).unwrap();
230-
self.downloads.pending_ids.remove(&download.path).unwrap();
231-
232-
let result = match result {
233-
Ok(()) => {
234-
self.downloads.downloads_finished += 1;
235-
match handle.response_code() {
236-
Ok(code) => Ok(CompletedDownload {
237-
response_code: code,
238-
data: download.data.take(),
239-
index_version: download
240-
.index_version
241-
.take()
242-
.unwrap_or_else(|| UNKNOWN.to_string()),
243-
}),
244-
Err(e) => Err(e),
221+
let results = {
222+
let mut results = Vec::new();
223+
let pending = &mut self.downloads.pending;
224+
self.multi.messages(|msg| {
225+
let token = msg.token().expect("failed to read token");
226+
let (_, handle) = &pending[&token];
227+
if let Some(result) = msg.result_for(handle) {
228+
results.push((token, result));
229+
};
230+
});
231+
results
232+
};
233+
for (token, result) in results {
234+
let (mut download, handle) = self.downloads.pending.remove(&token).unwrap();
235+
let mut handle = self.multi.remove(handle)?;
236+
let data = download.data.take();
237+
let url = self.full_url(&download.path);
238+
let result = match download.retry.r#try(|| {
239+
result.with_context(|| format!("failed to download from `{}`", url))?;
240+
let code = handle.response_code()?;
241+
// Keep this list of expected status codes in sync with the codes handled in `load`
242+
if !matches!(code, 200 | 304 | 401 | 404 | 451) {
243+
let url = handle.effective_url()?.unwrap_or(&url);
244+
return Err(HttpNotSuccessful {
245+
code,
246+
url: url.to_owned(),
247+
body: data,
245248
}
249+
.into());
250+
}
251+
Ok(data)
252+
}) {
253+
Ok(Some(data)) => Ok(CompletedDownload {
254+
response_code: handle.response_code()?,
255+
data,
256+
index_version: download
257+
.index_version
258+
.take()
259+
.unwrap_or_else(|| UNKNOWN.to_string()),
260+
}),
261+
Ok(None) => {
262+
// retry the operation
263+
let handle = self.multi.add(handle)?;
264+
self.downloads.pending.insert(token, (download, handle));
265+
continue;
246266
}
247267
Err(e) => Err(e),
248268
};
269+
270+
assert!(self.downloads.pending_ids.remove(&download.path));
249271
self.downloads.results.insert(download.path, result);
250-
});
272+
self.downloads.downloads_finished += 1;
273+
}
274+
251275
self.downloads.tick()?;
252276

253277
Ok(())
@@ -339,6 +363,8 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
339363
debug!("downloaded the index file `{}` twice", path.display())
340364
}
341365

366+
// The status handled here need to be kept in sync with the codes handled
367+
// in `handle_completed_downloads`
342368
match result.response_code {
343369
200 => {}
344370
304 => {
@@ -355,13 +381,7 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
355381
return Poll::Ready(Ok(LoadResponse::NotFound));
356382
}
357383
code => {
358-
return Err(anyhow::anyhow!(
359-
"server returned unexpected HTTP status code {} for {}\nbody: {}",
360-
code,
361-
self.full_url(path),
362-
str::from_utf8(&result.data).unwrap_or("<invalid utf8>"),
363-
))
364-
.into();
384+
return Err(internal(format!("unexpected HTTP status code {code}"))).into();
365385
}
366386
}
367387

@@ -371,13 +391,6 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
371391
}));
372392
}
373393

374-
if self.config.offline() {
375-
return Poll::Ready(Err(anyhow::anyhow!(
376-
"can't download index file from '{}': you are in offline mode (--offline)",
377-
self.url
378-
)));
379-
}
380-
381394
// Looks like we're going to have to do a network request.
382395
self.start_fetch()?;
383396

@@ -433,9 +446,8 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
433446
let token = self.downloads.next;
434447
self.downloads.next += 1;
435448
debug!("downloading {} as {}", path.display(), token);
436-
assert_eq!(
437-
self.downloads.pending_ids.insert(path.to_path_buf(), token),
438-
None,
449+
assert!(
450+
self.downloads.pending_ids.insert(path.to_path_buf()),
439451
"path queued for download more than once"
440452
);
441453

@@ -496,6 +508,7 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
496508
index_version: RefCell::new(None),
497509
total: Cell::new(0),
498510
current: Cell::new(0),
511+
retry: Retry::new(self.config)?,
499512
};
500513

501514
// Finally add the request we've lined up to the pool of requests that cURL manages.
@@ -613,7 +626,7 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
613626
let timeout = self
614627
.multi
615628
.get_timeout()?
616-
.unwrap_or_else(|| Duration::new(5, 0));
629+
.unwrap_or_else(|| Duration::new(1, 0));
617630
self.multi
618631
.wait(&mut [], timeout)
619632
.with_context(|| "failed to wait on curl `Multi`")?;

src/cargo/util/errors.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,32 @@ use anyhow::Error;
44
use std::fmt;
55
use std::path::PathBuf;
66

7+
use super::truncate_with_ellipsis;
8+
79
pub type CargoResult<T> = anyhow::Result<T>;
810

911
#[derive(Debug)]
10-
pub struct HttpNot200 {
12+
pub struct HttpNotSuccessful {
1113
pub code: u32,
1214
pub url: String,
15+
pub body: Vec<u8>,
1316
}
1417

15-
impl fmt::Display for HttpNot200 {
18+
impl fmt::Display for HttpNotSuccessful {
1619
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20+
let body = std::str::from_utf8(&self.body)
21+
.map(|s| truncate_with_ellipsis(s, 512))
22+
.unwrap_or_else(|_| format!("[{} non-utf8 bytes]", self.body.len()));
23+
1724
write!(
1825
f,
19-
"failed to get 200 response from `{}`, got {}",
26+
"failed to get successful HTTP response from `{}`, got {}\nbody:\n{body}",
2027
self.url, self.code
2128
)
2229
}
2330
}
2431

25-
impl std::error::Error for HttpNot200 {}
32+
impl std::error::Error for HttpNotSuccessful {}
2633

2734
// =============================================================================
2835
// Verbose error

src/cargo/util/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,15 @@ pub fn indented_lines(text: &str) -> String {
107107
})
108108
.collect()
109109
}
110+
111+
pub fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
112+
// We should truncate at grapheme-boundary and compute character-widths,
113+
// yet the dependencies on unicode-segmentation and unicode-width are
114+
// not worth it.
115+
let mut chars = s.chars();
116+
let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
117+
if chars.next().is_some() {
118+
prefix.push('…');
119+
}
120+
prefix
121+
}

src/cargo/util/network.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::Error;
22

3-
use crate::util::errors::{CargoResult, HttpNot200};
3+
use crate::util::errors::{CargoResult, HttpNotSuccessful};
44
use crate::util::Config;
55
use std::task::Poll;
66

@@ -31,6 +31,7 @@ impl<'a> Retry<'a> {
3131
})
3232
}
3333

34+
/// Returns `Ok(None)` for operations that should be re-tried.
3435
pub fn r#try<T>(&mut self, f: impl FnOnce() -> CargoResult<T>) -> CargoResult<Option<T>> {
3536
match f() {
3637
Err(ref e) if maybe_spurious(e) && self.remaining > 0 => {
@@ -73,7 +74,7 @@ fn maybe_spurious(err: &Error) -> bool {
7374
return true;
7475
}
7576
}
76-
if let Some(not_200) = err.downcast_ref::<HttpNot200>() {
77+
if let Some(not_200) = err.downcast_ref::<HttpNotSuccessful>() {
7778
if 500 <= not_200.code && not_200.code < 600 {
7879
return true;
7980
}
@@ -114,14 +115,16 @@ fn with_retry_repeats_the_call_then_works() {
114115
use crate::core::Shell;
115116

116117
//Error HTTP codes (5xx) are considered maybe_spurious and will prompt retry
117-
let error1 = HttpNot200 {
118+
let error1 = HttpNotSuccessful {
118119
code: 501,
119120
url: "Uri".to_string(),
121+
body: Vec::new(),
120122
}
121123
.into();
122-
let error2 = HttpNot200 {
124+
let error2 = HttpNotSuccessful {
123125
code: 502,
124126
url: "Uri".to_string(),
127+
body: Vec::new(),
125128
}
126129
.into();
127130
let mut results: Vec<CargoResult<()>> = vec![Ok(()), Err(error1), Err(error2)];
@@ -137,14 +140,16 @@ fn with_retry_finds_nested_spurious_errors() {
137140

138141
//Error HTTP codes (5xx) are considered maybe_spurious and will prompt retry
139142
//String error messages are not considered spurious
140-
let error1 = anyhow::Error::from(HttpNot200 {
143+
let error1 = anyhow::Error::from(HttpNotSuccessful {
141144
code: 501,
142145
url: "Uri".to_string(),
146+
body: Vec::new(),
143147
});
144148
let error1 = anyhow::Error::from(error1.context("A non-spurious wrapping err"));
145-
let error2 = anyhow::Error::from(HttpNot200 {
149+
let error2 = anyhow::Error::from(HttpNotSuccessful {
146150
code: 502,
147151
url: "Uri".to_string(),
152+
body: Vec::new(),
148153
});
149154
let error2 = anyhow::Error::from(error2.context("A second chained error"));
150155
let mut results: Vec<CargoResult<()>> = vec![Ok(()), Err(error1), Err(error2)];

0 commit comments

Comments
 (0)