Skip to content

github-backup(fetch): a git fetch that hits the 3600s timeout raises TimeoutExpired out of fetch_mirrors.main() — every later repo is skipped, no failure is recorded, and state.json/MANIFEST.json are never written #3250

Closed

Description

@dezren39

Summary

github-backup(fetch): fetch_repo in infra/github-backup/bin/fetch_mirrors.py runs git fetch with timeout=3600 and no TimeoutExpired handling — one hanging fetch raises out of main() and kills the whole run: every later repo is never attempted, no failure is recorded for the timed-out repo, state.json is never written (so the coalesce/fail stamps of repos already fetched in the same run are lost too), fetch.log gets no line, and MANIFEST.json is not rebuilt. The module's own contract — "Failures are appended to fetch.log. A repo failing on 3 distinct days in a row makes the run exit non-zero" — never applies to the slowest, most failure-prone failure mode: a timeout can recur daily forever and never count toward the chronic-failure exit, because it is never recorded as a failure at all.

Evidence (main 8e4ef90e)

Verified (laptop probe, real fetch_mirrors.py)

Probe: ~/git/fleet/probe-2026-10-31-laneB-fetchtimeout/ on the laptop — the real fetch_mirrors.py / ghb_common.py / manifest.py copied verbatim (sha256 f0115270…, 2eb427df…, a489798e…, identical in the sandbox checkout at main 8e4ef90e and the laptop clone), three real bare mirrors, the subprocess layer raising TimeoutExpired for the first repo exactly as git fetch hitting the 3600s timeout does, real git for the rest:

CASE hang-first: main([prog, hang/aaa, good/bbb, good/ccc])
  RAISED TimeoutExpired (unhandled): Command '['git', '-C', '.../hang/aaa.git', 'fetch', '--all', '--prune']' timed out after 3600 seconds
  fetch attempts: [only hang/aaa — good/bbb and good/ccc never attempted]
  state.json exists: False
  fetch.log exists: False
  MANIFEST.json exists: False
CONTROL no-hang: main([prog, good/bbb, good/ccc])
  {"results": {"ok": 2, "fail": 0, "skip": 0, "missing": 0}, "chronic_failures": []}
  returned rc = 0
  state.json exists: True (both repos stamped last_fetch, fail_days [])

Why it matters

The daily fetch is the freshness mechanism for the whole mirror estate. Mirrors of large/active repos are exactly the ones that can hit a 3600s fetch (slow network, laptop sleep mid-fetch, a wedged remote). Today one such repo silently stops the entire daily pass — every repo after it in plan order goes stale — and leaves no per-repo record, so the chronic-failure alarm that exists precisely to surface a repeatedly failing repo can never fire for it. The only trace is an unhandled traceback in the systemd journal.

Acceptance criteria

  • A git fetch that exceeds the timeout is recorded as that repo's failure (same shape as a non-zero exit: last_error noted, today appended to fail_days, a fetch.log line written) and the run continues with the remaining repos.
  • state.json is written and MANIFEST.json is rebuilt even when one or more fetches time out; the summary/chronic-failure exit behaviour applies to timeouts exactly as to other failures (3 distinct fail days → exit 1).
  • drain_queue.py's fetch path inherits the same behaviour: one repo's timeout does not abort fetching the remaining queued repos or lose the drainer's state write.
  • Regression tests/probe cover: timeout on the first repo (later repos still fetched, state + log + manifest produced), timeout counted in fail_days, and the no-timeout control unchanged.

Activity

  1. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/github-backupinfra/github-backup estate mirror system
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-10-31 18:20 CDT
    • Found via: filed this run
    • Task: Triage the fetch_mirrors TimeoutExpired defect: one hanging git fetch kills the daily fetch pass and the drainer's fetch loop, unrecorded.
    • Expectation: effort/s — done looks like a timeout recorded as that repo's failure, the run continuing, and state/log/manifest still produced (see acceptance criteria).
    • Links: queue packet packet-3250-triage.json (enqueued this run)
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Design-architect pass 29 (2026-10-31) — agent note: this issue is ADOPTED as a phase of design #3282 ("a mirror is what it can prove — one validity, outcome and alarm contract for the github-backup estate mirror", design doc: files/designs/code-github-backup-mirror-contract.md in the keep-the-four-repos-healthy goal workspace), linked as a sub-issue of #3282 and labelled source/design-architect. Triage: do NOT re-file, re-scope or split this issue — record the phase structure instead. Phase mapping: Phase 1 (validity & destinations) = #3238 + #3233; Phase 2 (recorded outcomes & the alarm) = #3250 + #3219 + #3270 + #3257; Phase 3 (durable state & plan retention) = #3240 + #3229 + closing audit #3283 (gated on Phases 1–2). The design records the pins that span the phases: validity = bare + HEAD resolves, with no emptiness exception inside the plan (build_plan excludes zero-commit repos); destinations are absent / valid / skeleton (remove + clone) / debris (quarantine to .partial-<timestamp> + clone); every fetch attempt ends in one recorded outcome shared by the daily loop and the drainer, and the drainer acks only on ok; the chronic alarm is a streak of 3 consecutive unavailable days (fail, timeout and missing all count); generated state is written atomically only through ghb_common helpers; reconcile retains a failed owner's previously planned entries. Where an origin's acceptance left a choice open (the streak-vs-window rule, missing-counts-vs-immediate, report-vs-retain, skeleton handling), the design doc records the decision — triage follows the design's pin, not the open alternative. Each origin keeps its own acceptance criteria and its already-queued triage packet — no new packet was created for it this pass.

  4. added
    work/assignedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. 36 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — holder: repo-worker-code, at 2026-11-02T13:42:30Z. Parked queue task 5687 for 30 minutes. Fresh metadata confirms #3250 OPEN/assigned to dezren39 and PR #4351 OPEN, non-draft, Drew-authored. The existing queue record identifies the PR as Drew-owned; hard stop respected: no checks, reviews, diff, branch, or worktree inspected and no code touched. Resume only after PR #4351 closes, ownership changes, or explicit rerouting releases the restriction.

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/github-backupinfra/github-backup estate mirror systemeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions