Skip to content

Network retry issue 1602 #2396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ advapi32-sys = "0.1"
crates-io = { path = "src/crates-io", version = "0.2" }
crossbeam = "0.2"
curl = "0.2"
curl-sys = "0.1"
docopt = "0.6"
env_logger = "0.3"
filetime = "0.1"
Expand Down
1 change: 1 addition & 0 deletions src/cargo/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
extern crate crates_io as registry;
extern crate crossbeam;
extern crate curl;
extern crate curl_sys;
extern crate docopt;
extern crate filetime;
extern crate flate2;
Expand Down
5 changes: 3 additions & 2 deletions src/cargo/sources/git/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ impl<'cfg> Source for GitSource<'cfg> {
format!("git repository `{}`", self.remote.url())));

trace!("updating git source `{:?}`", self.remote);
let repo = try!(self.remote.checkout(&db_path));

let repo = try!(self.remote.checkout(&db_path, &self.config));
let rev = try!(repo.rev_for(&self.reference));
(repo, rev)
} else {
Expand All @@ -172,7 +173,7 @@ impl<'cfg> Source for GitSource<'cfg> {
// Copy the database to the checkout location. After this we could drop
// the lock on the database as we no longer needed it, but we leave it
// in scope so the destructors here won't tamper with too much.
try!(repo.copy_to(actual_rev.clone(), &checkout_path));
try!(repo.copy_to(actual_rev.clone(), &checkout_path, &self.config));

let source_id = self.source_id.with_precise(Some(actual_rev.to_string()));
let path_source = PathSource::new_recursive(&checkout_path,
Expand Down
45 changes: 24 additions & 21 deletions src/cargo/sources/git/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use url::Url;
use git2::{self, ObjectType};

use core::GitReference;
use util::{CargoResult, ChainError, human, ToUrl, internal};
use util::{CargoResult, ChainError, human, ToUrl, internal, Config, network};

#[derive(PartialEq, Clone, Debug)]
pub struct GitRevision(git2::Oid);
Expand Down Expand Up @@ -109,16 +109,16 @@ impl GitRemote {
db.rev_for(reference)
}

pub fn checkout(&self, into: &Path) -> CargoResult<GitDatabase> {
pub fn checkout(&self, into: &Path, cargo_config: &Config) -> CargoResult<GitDatabase> {
let repo = match git2::Repository::open(into) {
Ok(repo) => {
try!(self.fetch_into(&repo).chain_error(|| {
try!(self.fetch_into(&repo, &cargo_config).chain_error(|| {
human(format!("failed to fetch into {}", into.display()))
}));
repo
}
Err(..) => {
try!(self.clone_into(into).chain_error(|| {
try!(self.clone_into(into, &cargo_config).chain_error(|| {
human(format!("failed to clone into: {}", into.display()))
}))
}
Expand All @@ -140,21 +140,21 @@ impl GitRemote {
})
}

fn fetch_into(&self, dst: &git2::Repository) -> CargoResult<()> {
fn fetch_into(&self, dst: &git2::Repository, cargo_config: &Config) -> CargoResult<()> {
// Create a local anonymous remote in the repository to fetch the url
let url = self.url.to_string();
let refspec = "refs/heads/*:refs/heads/*";
fetch(dst, &url, refspec)
fetch(dst, &url, refspec, &cargo_config)
}

fn clone_into(&self, dst: &Path) -> CargoResult<git2::Repository> {
fn clone_into(&self, dst: &Path, cargo_config: &Config) -> CargoResult<git2::Repository> {
let url = self.url.to_string();
if fs::metadata(&dst).is_ok() {
try!(fs::remove_dir_all(dst));
}
try!(fs::create_dir_all(dst));
let repo = try!(git2::Repository::init_bare(dst));
try!(fetch(&repo, &url, "refs/heads/*:refs/heads/*"));
try!(fetch(&repo, &url, "refs/heads/*:refs/heads/*", &cargo_config));
Ok(repo)
}
}
Expand All @@ -164,21 +164,21 @@ impl GitDatabase {
&self.path
}

pub fn copy_to(&self, rev: GitRevision, dest: &Path)
pub fn copy_to(&self, rev: GitRevision, dest: &Path, cargo_config: &Config)
-> CargoResult<GitCheckout> {
let checkout = match git2::Repository::open(dest) {
Ok(repo) => {
let checkout = GitCheckout::new(dest, self, rev, repo);
if !checkout.is_fresh() {
try!(checkout.fetch());
try!(checkout.fetch(&cargo_config));
try!(checkout.reset());
assert!(checkout.is_fresh());
}
checkout
}
Err(..) => try!(GitCheckout::clone_into(dest, self, rev)),
};
try!(checkout.update_submodules().chain_error(|| {
try!(checkout.update_submodules(&cargo_config).chain_error(|| {
internal("failed to update submodules")
}));
Ok(checkout)
Expand Down Expand Up @@ -276,12 +276,12 @@ impl<'a> GitCheckout<'a> {
}
}

fn fetch(&self) -> CargoResult<()> {
fn fetch(&self, cargo_config: &Config) -> CargoResult<()> {
info!("fetch {}", self.repo.path().display());
let url = try!(self.database.path.to_url().map_err(human));
let url = url.to_string();
let refspec = "refs/heads/*:refs/heads/*";
try!(fetch(&self.repo, &url, refspec));
try!(fetch(&self.repo, &url, refspec, &cargo_config));
Ok(())
}

Expand All @@ -303,10 +303,10 @@ impl<'a> GitCheckout<'a> {
Ok(())
}

fn update_submodules(&self) -> CargoResult<()> {
return update_submodules(&self.repo);
fn update_submodules(&self, cargo_config: &Config) -> CargoResult<()> {
return update_submodules(&self.repo, &cargo_config);

fn update_submodules(repo: &git2::Repository) -> CargoResult<()> {
fn update_submodules(repo: &git2::Repository, cargo_config: &Config) -> CargoResult<()> {
info!("update submodules for: {:?}", repo.workdir().unwrap());

for mut child in try!(repo.submodules()).into_iter() {
Expand Down Expand Up @@ -346,14 +346,14 @@ impl<'a> GitCheckout<'a> {

// Fetch data from origin and reset to the head commit
let refspec = "refs/heads/*:refs/heads/*";
try!(fetch(&repo, url, refspec).chain_error(|| {
try!(fetch(&repo, url, refspec, &cargo_config).chain_error(|| {
internal(format!("failed to fetch submodule `{}` from {}",
child.name().unwrap_or(""), url))
}));

let obj = try!(repo.find_object(head, None));
try!(repo.reset(&obj, git2::ResetType::Hard, None));
try!(update_submodules(&repo));
try!(update_submodules(&repo, &cargo_config));
}
Ok(())
}
Expand Down Expand Up @@ -389,7 +389,7 @@ impl<'a> GitCheckout<'a> {
/// attempted and we don't try the same ones again.
fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
-> CargoResult<T>
where F: FnMut(&mut git2::Credentials) -> Result<T, git2::Error>
where F: FnMut(&mut git2::Credentials) -> CargoResult<T>
{
let mut cred_helper = git2::CredentialHelper::new(url);
cred_helper.config(cfg);
Expand Down Expand Up @@ -555,7 +555,7 @@ fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
}

pub fn fetch(repo: &git2::Repository, url: &str,
refspec: &str) -> CargoResult<()> {
refspec: &str, cargo_config: &Config) -> CargoResult<()> {
// Create a local anonymous remote in the repository to fetch the url

with_authentication(url, &try!(repo.config()), |f| {
Expand All @@ -565,7 +565,10 @@ pub fn fetch(repo: &git2::Repository, url: &str,
let mut opts = git2::FetchOptions::new();
opts.remote_callbacks(cb)
.download_tags(git2::AutotagOption::All);
try!(remote.fetch(&[refspec], Some(&mut opts), None));

try!(network::with_retry(cargo_config, ||{
remote.fetch(&[refspec], Some(&mut opts), None)
}));
Ok(())
})
}
3 changes: 2 additions & 1 deletion src/cargo/sources/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,8 @@ impl<'cfg> RegistrySource<'cfg> {
// git fetch origin
let url = self.source_id.url().to_string();
let refspec = "refs/heads/*:refs/remotes/origin/*";
try!(git::fetch(&repo, &url, refspec).chain_error(|| {

try!(git::fetch(&repo, &url, refspec, &self.config).chain_error(|| {
internal(format!("failed to fetch `{}`", url))
}));

Expand Down
15 changes: 15 additions & 0 deletions src/cargo/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,21 @@ impl Config {
}
}

pub fn net_retry(&self) -> CargoResult<i64> {
match try!(self.get_i64("net.retry")) {
Some(v) => {
let value = v.val;
if value < 0 {
bail!("net.retry must be positive, but found {} in {}",
v.val, v.definition)
} else {
Ok(value)
}
}
None => Ok(2),
}
}

pub fn expected<T>(&self, ty: &str, key: &str, val: CV) -> CargoResult<T> {
val.expected(ty).map_err(|e| {
human(format!("invalid configuration for key `{}`\n{}", key, e))
Expand Down
31 changes: 31 additions & 0 deletions src/cargo/util/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::str;
use std::string;

use curl;
use curl_sys;
use git2;
use rustc_serialize::json;
use semver;
Expand Down Expand Up @@ -285,6 +286,36 @@ impl From<Box<CargoError>> for CliError {
}
}

// =============================================================================
// NetworkError trait

pub trait NetworkError: CargoError {
fn maybe_spurious(&self) -> bool;
}

impl NetworkError for git2::Error {
fn maybe_spurious(&self) -> bool {
match self.class() {
git2::ErrorClass::Net |
git2::ErrorClass::Os => true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come Os is included here? In theory that's not spurious?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the class I got for my test when it timed out. When I tell the test to connect to a server that is not running https://github.com/rust-lang/cargo/pull/2396/files#diff-5a6267705b81a22950ae48ebdce1a553R8

I thought it would have gave a Net error but Os is what it printed.

_ => false
}
}
}
impl NetworkError for curl::ErrCode {
fn maybe_spurious(&self) -> bool {
match self.code() {
curl_sys::CURLcode::CURLE_COULDNT_CONNECT |
curl_sys::CURLcode::CURLE_COULDNT_RESOLVE_PROXY |
curl_sys::CURLcode::CURLE_COULDNT_RESOLVE_HOST |
curl_sys::CURLcode::CURLE_OPERATION_TIMEDOUT |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an operation we shouldn't retry by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like curl retries (at least once) on CURLE_OPERATION_TIMEDOUT https://github.com/curl/curl/blob/b9728bca549709a26a5228f1d44f7488dd26811d/src/tool_operate.c#L1449

I do not have any data around which errors people are seeing. It looks like, after a quick search in the curl code, that CURLE_OPERATION_TIMEDOUT covers a lot of different case. After this is merge the list can be changed easy. In this case I believe catching more is better only because if this error code turns out to be bad the work around would be to configure the setting. If you release cargo without this and it turns out that it is the core failure case for curl everyone who is affected need to wait for a new release cycle.

Let me know if you would still like to remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's leave it in and we can always back it out if weird things happen

curl_sys::CURLcode::CURLE_RECV_ERROR
=> true,
_ => false
}
}
}

// =============================================================================
// various impls

Expand Down
1 change: 1 addition & 0 deletions src/cargo/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod to_url;
pub mod toml;
pub mod lev_distance;
pub mod job;
pub mod network;
mod cfg;
mod dependency_queue;
mod rustc;
Expand Down
86 changes: 86 additions & 0 deletions src/cargo/util/network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use util::{CargoResult, Config, errors};

/// Wrapper method for network call retry logic.
///
/// Retry counts provided by Config object 'net.retry'. Config shell outputs
/// a warning on per retry.
///
/// Closure must return a CargoResult.
///
/// Example:
/// use util::network;
/// cargo_result = network.with_retry(&config, || something.download());
pub fn with_retry<T, E, F>(config: &Config, mut callback: F) -> CargoResult<T>
where F: FnMut() -> Result<T, E>,
E: errors::NetworkError
{
let mut remaining = try!(config.net_retry());
loop {
match callback() {
Ok(ret) => return Ok(ret),
Err(ref e) if e.maybe_spurious() && remaining > 0 => {
let msg = format!("spurious network error ({} tries \
remaining): {}", remaining, e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the remaining count here is actually remaining - 1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry config is 2 by default.

count is 2 
I run once and fail i output 
  2 tries remains 
    subtract 1 count is 1
run a second time 
  1 tries remaining 
    subtract 1 count is 0
retry again
just error. 

The retry count is the count of retries not the count of total tries. I can subtract one from the config or update the docs to be explicitly clear it is the count of retries and not the total network tries?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right yeah that makes sense, nevermind!

try!(config.shell().warn(msg));
remaining -= 1;
}
Err(e) => return Err(Box::new(e)),
}
}
}
#[test]
fn with_retry_repeats_the_call_then_works() {

use std::error::Error;
use util::human;
use std::fmt;

#[derive(Debug)]
struct NetworkRetryError {
error: Box<errors::CargoError>,
}

impl Error for NetworkRetryError {
fn description(&self) -> &str {
self.error.description()
}
fn cause(&self) -> Option<&Error> {
self.error.cause()
}
}

impl NetworkRetryError {
fn new(error: &str) -> NetworkRetryError {
let error = human(error.to_string());
NetworkRetryError {
error: error,
}
}
}

impl fmt::Display for NetworkRetryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.error, f)
}
}

impl errors::CargoError for NetworkRetryError {
fn is_human(&self) -> bool {
false
}
}

impl errors::NetworkError for NetworkRetryError {
fn maybe_spurious(&self) -> bool {
true
}
}

let error1 = NetworkRetryError::new("one");
let error2 = NetworkRetryError::new("two");
let mut results: Vec<Result<(), NetworkRetryError>> = vec![Ok(()),
Err(error1), Err(error2)];
let config = Config::default().unwrap();
let result = with_retry(&config, || results.pop().unwrap());
assert_eq!(result.unwrap(), ())
}
4 changes: 4 additions & 0 deletions src/doc/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ rustflags = ["..", ".."] # custom flags to pass to all compiler invocations
[term]
verbose = false # whether cargo provides verbose output
color = 'auto' # whether cargo colorizes output

# Network configuration
[net]
retry = 2 # number of times a network call will automatically retried
```

# Environment Variables
Expand Down
1 change: 1 addition & 0 deletions tests/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ fn substitute_macros(input: &str) -> String {
("[RUNNING]", " Running"),
("[COMPILING]", " Compiling"),
("[ERROR]", "error:"),
("[WARNING]", "warning:"),
("[DOCUMENTING]", " Documenting"),
("[FRESH]", " Fresh"),
("[UPDATING]", " Updating"),
Expand Down
Loading