|
|
… |
536 unchanged lines |
| 537 |
537 |
|
out |
| 538 |
538 |
|
} |
| 539 |
539 |
|
|
|
540 |
+ |
/// Whether a network error is worth another try: DNS, a refused or reset |
|
541 |
+ |
/// connection, a timeout, or a gateway answering 502, 503 or 504 while the |
|
542 |
+ |
/// hub restarts (oxygenhub#111). |
|
543 |
+ |
fn transient(msg: &str) -> bool { |
|
544 |
+ |
let m = msg.to_lowercase(); |
|
545 |
+ |
[ |
|
546 |
+ |
"dns failed", |
|
547 |
+ |
"name resolution", |
|
548 |
+ |
"connection refused", |
|
549 |
+ |
"connection reset", |
|
550 |
+ |
"timed out", |
|
551 |
+ |
"timeout", |
|
552 |
+ |
"http 502", |
|
553 |
+ |
"http 503", |
|
554 |
+ |
"http 504", |
|
555 |
+ |
"broken pipe", |
|
556 |
+ |
"unexpected eof", |
|
557 |
+ |
] |
|
558 |
+ |
.iter() |
|
559 |
+ |
.any(|k| m.contains(k)) |
|
560 |
+ |
} |
|
561 |
+ |
|
|
562 |
+ |
/// Run `f` again after a transient network error, waiting `waits` between |
|
563 |
+ |
/// tries (about a minute in all), and give up on any other error at once. |
|
564 |
+ |
fn with_retries<T>(waits: &[Duration], mut f: impl FnMut() -> Result<T>) -> Result<T> { |
|
565 |
+ |
let mut tries = waits.iter(); |
|
566 |
+ |
loop { |
|
567 |
+ |
match f() { |
|
568 |
+ |
Ok(v) => return Ok(v), |
|
569 |
+ |
Err(e) => match tries.next() { |
|
570 |
+ |
Some(w) if transient(&format!("{e:#}")) => std::thread::sleep(*w), |
|
571 |
+ |
_ => return Err(e), |
|
572 |
+ |
}, |
|
573 |
+ |
} |
|
574 |
+ |
} |
|
575 |
+ |
} |
|
576 |
+ |
|
|
577 |
+ |
/// The waits between tries of a fetch: 2, 4, 8, 16 and 30 seconds. |
|
578 |
+ |
const FETCH_WAITS: [Duration; 5] = |
|
579 |
+ |
[Duration::from_secs(2), Duration::from_secs(4), Duration::from_secs(8), Duration::from_secs(16), Duration::from_secs(30)]; |
|
580 |
+ |
|
| 540 |
581 |
|
/// The runner's copy of a repository, fetched up to date with the job's token. |
| 541 |
582 |
|
fn cached_repo(home: &Home, cfg: &RunnerConfig, claim: &Claim) -> Result<Repo> { |
| 542 |
583 |
|
cached_repo_at(home, cfg, &claim.repo, &claim.clone_url, &claim.token) |
|
|
… |
13 unchanged lines |
| 556 |
597 |
|
if dir.join(".oxy").is_dir() { |
| 557 |
598 |
|
let mut repo = Repo::open(&dir).map_err(|e| anyhow::anyhow!("{e}"))?; |
| 558 |
599 |
|
set_remote(&mut repo)?; |
| 559 |
|
- |
repo.fetch("origin").map_err(|e| anyhow::anyhow!("fetch {clone_url}: {e}"))?; |
|
600 |
+ |
with_retries(&FETCH_WAITS, || repo.fetch("origin").map(|_| ()).map_err(|e| anyhow::anyhow!("fetch {clone_url}: {e}")))?; |
| 560 |
601 |
|
return Ok(repo); |
| 561 |
602 |
|
} |
| 562 |
603 |
|
// First job for this repository: clone with the job's token. |
|
|
… |
1 unchanged lines |
| 564 |
605 |
|
if let Some(ca) = &cfg.ca { |
| 565 |
606 |
|
std::env::set_var("OXY_CA", ca); |
| 566 |
607 |
|
} |
| 567 |
|
- |
let cloned = Repo::clone(clone_url, &dir); |
|
608 |
+ |
let cloned = with_retries(&FETCH_WAITS, || { |
|
609 |
+ |
let _ = std::fs::remove_dir_all(&dir); |
|
610 |
+ |
Repo::clone(clone_url, &dir).map_err(|e| anyhow::anyhow!("{e}")) |
|
611 |
+ |
}); |
| 568 |
612 |
|
std::env::remove_var("OXY_TOKEN"); |
| 569 |
613 |
|
std::env::remove_var("OXY_CA"); |
| 570 |
614 |
|
let mut repo = cloned.map_err(|e| { |
|
|
… |
7 unchanged lines |
| 578 |
622 |
|
#[cfg(test)] |
| 579 |
623 |
|
mod tests { |
| 580 |
624 |
|
use super::*; |
|
625 |
+ |
|
|
626 |
+ |
#[test] |
|
627 |
+ |
fn transient_network_errors_are_tried_again_and_others_are_not() { |
|
628 |
+ |
assert!(transient("fetch x: Dns Failed: resolve dns name 'oxygenhub.io:443': Temporary failure in name resolution")); |
|
629 |
+ |
assert!(transient("HTTP 503: the hub is restarting")); |
|
630 |
+ |
assert!(!transient("HTTP 404: no such repository")); |
|
631 |
+ |
let mut n = 0; |
|
632 |
+ |
let got: Result<u32> = with_retries(&[Duration::ZERO, Duration::ZERO], || { |
|
633 |
+ |
n += 1; |
|
634 |
+ |
if n < 3 { |
|
635 |
+ |
Err(anyhow::anyhow!("connection refused")) |
|
636 |
+ |
} else { |
|
637 |
+ |
Ok(7) |
|
638 |
+ |
} |
|
639 |
+ |
}); |
|
640 |
+ |
assert_eq!((got.unwrap(), n), (7, 3), "two retries, then success"); |
|
641 |
+ |
let mut m = 0; |
|
642 |
+ |
let got: Result<u32> = with_retries(&[Duration::ZERO; 4], || { |
|
643 |
+ |
m += 1; |
|
644 |
+ |
Err(anyhow::anyhow!("HTTP 403: forbidden")) |
|
645 |
+ |
}); |
|
646 |
+ |
assert!(got.is_err()); |
|
647 |
+ |
assert_eq!(m, 1, "a refusal is not tried again"); |
|
648 |
+ |
let mut k = 0; |
|
649 |
+ |
let got: Result<u32> = with_retries(&[Duration::ZERO; 2], || { |
|
650 |
+ |
k += 1; |
|
651 |
+ |
Err(anyhow::anyhow!("timed out")) |
|
652 |
+ |
}); |
|
653 |
+ |
assert!(got.is_err()); |
|
654 |
+ |
assert_eq!(k, 3, "gives up after the last wait"); |
|
655 |
+ |
} |
| 581 |
656 |
|
|
| 582 |
657 |
|
/// A hub that answers each request with the next of `codes`, counting them. |
| 583 |
658 |
|
fn fake_hub(codes: Vec<u16>) -> (String, std::sync::Arc<std::sync::atomic::AtomicUsize>) { |
|
|
… |
132 unchanged lines |