Skip to content

github-backup(drain): webhook events are acked even when the fetch failed or the mirror is missing — freshness signal silently dropped #3219

Closed

Description

@dezren39

Bug: github-backup drainer acks webhook events even when the fetch failed or the mirror is missing — the freshness signal is silently dropped

Where: infra/github-backup/bin/drain_queue.py (landed in #3214; system tracked by #3215)

Evidence

In main() (drain_queue.py:56-64), the per-repo fetch result is discarded:

if repo in planned:
    fetch_mirrors.fetch_repo(root, repo, state)   # return value ignored
    fetched.append(repo)                          # reported as fetched either way

fetch_repo (bin/fetch_mirrors.py:23-42) returns "ok", "fail" (git fetch non-zero), or "missing" (no valid mirror at the path). The drainer:

  1. Appends every event id to ack up front (drain_queue.py:45) and POSTs the full ack list to the Worker at the end (drain_queue.py:67) — including events whose fetch just failed or whose mirror is missing. The Worker deletes them from KV (worker/src/index.js /queue/ack), so the event can never be redelivered.
  2. Sets the 15-minute per-repo coalesce stamp drain_last::<repo> (drain_queue.py:52) before the fetch, so even a hypothetical redelivery inside the window would be skipped.
  3. Writes nothing to fetch.log for the failure (only unplanned repos get a fetch.log line, drain_queue.py:61-64). The daily fetch_mirrors.py main loop logs failures; the drain path bypasses that logging entirely.
  4. Prints the repo in the run summary's fetched list regardless of outcome, so the systemd journal shows a successful drain.

Reproduced (laptop probe, main @ 96d1349)

Probe ~/git/fleet/probe-2026-10-31-laneB-drainack/probe.py drives the real drain_queue.main() with a stub Worker returning one push event for a planned repo and fetch_repo stubbed to return "fail":

{"events": 1, "fetched": ["owner/repo"]}
ack calls: [('/queue/ack', {'ids': ['ev1']})]
fetch.log exists: False
state keys: ['drain_last::owner/repo']
VERDICT: event acked despite fetch failure: True

The "missing" return takes the same code path (return value ignored), so a planned repo whose clone previously failed also loses its events until the weekly reconcile notices the missing mirror.

Why it matters

The webhook layer exists to keep mirrors fresh between weekly reconciles (Worker header: "Events are a freshness signal only; the weekly reconcile sweep is the correctness floor"). A transient fetch failure (network blip, GitHub 5xx, auth hiccup) currently converts into up-to-a-week of silent staleness for that repo: the event is deleted, the failure is in no log, and the run summary claims success. The correctness floor still catches the data eventually, but the failure is invisible — the opposite of this system's own observability pattern elsewhere (fetch_mirrors exits non-zero on 3-day chronic failures precisely so systemd surfaces them).

Acceptance criteria

  • Drain only acks an event for a planned repo when its fetch returned "ok"; on "fail"/"missing" the event stays queued for the next drain (or is acked only after an explicit retry policy), and the coalesce stamp is not advanced on failure.
  • Fetch failures/missing mirrors on the drain path are appended to fetch.log in the same format the daily pass uses, and the run summary distinguishes fetched vs failed repos.
  • A test (stub Worker + stub fetch, as in the probe) covers: success → acked; failure → not acked + logged; unplanned repo → acked + noted for reconcile (current intended behavior preserved).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    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
    app/github-backupinfra/github-backup estate mirror system
    effort/sSmall: expected under 2 hours of work
    area/infraInfrastructure, deployment, and operations
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: cron job new-issue-creator-code-b, run 2026-10-31 15:50 CDT
    • Found via: filed this run
    • Task: Stop the github-backup drainer acking webhook events whose fetch failed or whose mirror is missing; log drain-path fetch failures to fetch.log and report fetched vs failed honestly.
    • Expectation: effort/s — done when the acceptance criteria on this issue pass, including a stub-Worker/stub-fetch test (success → acked; failure → not acked + logged).
    • Links: evidence in the issue body; laptop probe at ~/git/fleet/probe-2026-10-31-laneB-drainack/probe.py (real drain_queue.main(), main 96d1349, laptop clone sha-matched).
  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.

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 systemarea/infraInfrastructure, deployment, and operationseffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/needs-triageNot yet triaged; needs a first looktask/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