1use anyhow::Error;
2
3use crate::util::errors::{CargoResult, HttpNot200};
4use crate::util::Config;
5
6pub struct Retry<'a> {
7 config: &'a Config,
8 remaining: u32,
9}
10
11impl<'a> Retry<'a> {
12 pub fn new(config: &'a Config) -> CargoResult<Retry<'a>> {
13 Ok(Retry {
14 config,
15 remaining: config.net_config()?.retry.unwrap_or(2),
16 })
17 }
18
19 pub fn r#try<T>(&mut self, f: impl FnOnce() -> CargoResult<T>) -> CargoResult<Option<T>> {
20 match f() {
21 Err(ref e) if maybe_spurious(e) && self.remaining > 0 => {
22 let msg = format!(
23 "spurious network error ({} tries \
24 remaining): {}",
25 self.remaining, e
26 );
27 self.config.shell().warn(msg)?;
28 self.remaining -= 1;
29 Ok(None)
30 }
31 other => other.map(Some),
32 }
33 }
34}
35
36fn maybe_spurious(err: &Error) -> bool {
37 for e in err.chain() {
38 if let Some(git_err) = e.downcast_ref::<git2::Error>() {
39 match git_err.class() {
40 git2::ErrorClass::Net | git2::ErrorClass::Os => return true,
41 _ => (),
42 }
43 }
44 if let Some(curl_err) = e.downcast_ref::<curl::Error>() {
45 if curl_err.is_couldnt_connect()
46 || curl_err.is_couldnt_resolve_proxy()
47 || curl_err.is_couldnt_resolve_host()
48 || curl_err.is_operation_timedout()
49 || curl_err.is_recv_error()
50 || curl_err.is_http2_stream_error()
51 || curl_err.is_ssl_connect_error()
52 || curl_err.is_partial_file()
53 {
54 return true;
55 }
56 }
57 if let Some(not_200) = e.downcast_ref::<HttpNot200>() {
58 if 500 <= not_200.code && not_200.code < 600 {
59 return true;
60 }
61 }
62 }
63 false
64}
65
66pub fn with_retry<T, F>(config: &Config, mut callback: F) -> CargoResult<T>
83where
84 F: FnMut() -> CargoResult<T>,
85{
86 let mut retry = Retry::new(config)?;
87 loop {
88 if let Some(ret) = retry.r#try(&mut callback)? {
89 return Ok(ret);
90 }
91 }
92}
93
94#[test]
95fn with_retry_repeats_the_call_then_works() {
96 use crate::core::Shell;
97
98 let error1 = HttpNot200 {
100 code: 501,
101 url: "Uri".to_string(),
102 }
103 .into();
104 let error2 = HttpNot200 {
105 code: 502,
106 url: "Uri".to_string(),
107 }
108 .into();
109 let mut results: Vec<CargoResult<()>> = vec![Ok(()), Err(error1), Err(error2)];
110 let config = Config::default().unwrap();
111 *config.shell() = Shell::from_write(Box::new(Vec::new()));
112 let result = with_retry(&config, || results.pop().unwrap());
113 assert!(result.is_ok())
114}
115
116#[test]
117fn with_retry_finds_nested_spurious_errors() {
118 use crate::core::Shell;
119
120 let error1 = anyhow::Error::from(HttpNot200 {
123 code: 501,
124 url: "Uri".to_string(),
125 });
126 let error1 = anyhow::Error::from(error1.context("A non-spurious wrapping err"));
127 let error2 = anyhow::Error::from(HttpNot200 {
128 code: 502,
129 url: "Uri".to_string(),
130 });
131 let error2 = anyhow::Error::from(error2.context("A second chained error"));
132 let mut results: Vec<CargoResult<()>> = vec![Ok(()), Err(error1), Err(error2)];
133 let config = Config::default().unwrap();
134 *config.shell() = Shell::from_write(Box::new(Vec::new()));
135 let result = with_retry(&config, || results.pop().unwrap());
136 assert!(result.is_ok())
137}
138
139#[test]
140fn curle_http2_stream_is_spurious() {
141 let code = curl_sys::CURLE_HTTP2_STREAM;
142 let err = curl::Error::new(code);
143 assert!(maybe_spurious(&err.into()));
144}