OXYGENHUB

A runner tries a fetch again after a network hiccup f53e6d47b56a

Fetching or cloning a job's repository or a subrepo is retried on DNS,
connection, timeout and 502/503/504 errors, waiting 2, 4, 8, 16 and 30
seconds; any other error still ends the job at once. A transient DNS
failure on earth-1 ended a test job as an error. closes oxygenhub#111
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-04 19:57 UTC
Signature
✔ signed by Andy McCall
Hash
f53e6d47b56a822e393d0d576d3584f4c5786039b22e3f0829974905164ee395
Depends on
1abadd4a2fa6 1b2229dab1a4 bb643c252515 c23d519168d8 c3db750ed96e e0918b887850
Depended on by
0077c7488e5d b11e30f5031d c5b702e59c1f
In bookmarks
main newer-pipeline push-uses-login
In tags
v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.5.0 v0.5.1 v0.5.2 v0.5.3
Edits
6 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **A runner tries a fetch again after a network hiccup (oxygenhub#111).** Fetching or cloning the job's repository or a subrepo is retried on DNS, connection, timeout and 502/503/504 errors, waiting 2, 4, 8, 16 and 30 seconds; any other error still ends the job at once.
8 +
7 9
- **Architecture awareness (#36, spec §11.2).** `flow rootfs import` records an image's architecture from its shell's ELF header and refuses an image for another architecture; a claim reports each image's architecture, and a sandbox may say `arch "riscv64"`.
8 10
9 11
- **A job's identity (oxygenhub#103).** `identity "AUDIENCE" { mount-path "/run/secrets/…" }` asks the hub for an OIDC token for that audience, mounted as a file like a secret.
…
16 unchanged lines
modified crates/flow/src/runner.rs +77 −2
…
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