Skip to content

github-backup(drain): owner allowlist crashes the whole drain run on a non-dict owner/account — malformed event becomes a poison pill instead of a DRAIN-DROP #3331

Closed

Description

@dezren39

Summary

infra/github-backup/bin/drain_queue.py — the #3326 owner allowlist crashes the whole drain run on a queued event whose repository.owner (or installation.account) is present but not a JSON object. The allowlist's stated contract (module docstring, #3326) is that "malformed events whose owner cannot be determined are dropped: acked as handled-by-policy, logged as one loud DRAIN-DROP line". A non-dict owner instead raises AttributeError before any drop, fetch, or ack happens — and because the event is never acked, it stays at the head of the queue and crashes every subsequent 5-minute drain run the same way: a poison pill that stalls all mirror fetches until someone removes the event by hand.

Evidence (verified this run, current main 72e34760)

event_owner at drain_queue.py:111-127 guards the containers it was thinking of, but not the leaf containers:

rep = ev.get("repository")
if isinstance(rep, dict):
    login = (rep.get("owner") or {}).get("login")   # owner may be str/list/int -> AttributeError
    ...
inst = ev.get("installation")
if isinstance(inst, dict):
    login = (inst.get("account") or {}).get("login") # account may be str/int -> AttributeError

Laptop probe ~/git/fleet/probe-2026-10-31-laneB-drainowner/probe.py driving the real module (sha256 af2fbb2c7d1af7da31aa36b8d3ce39185f1ea33785ca989b7425272e37ff4480, byte-identical to main):

control-missing-owner:  event_owner -> None
control-owner-dict:     event_owner -> 'mallory'
bug-owner-string:       event_owner RAISED AttributeError: 'str' object has no attribute 'get'
bug-owner-list:         event_owner RAISED AttributeError: 'list' object has no attribute 'get'
bug-owner-int:          event_owner RAISED AttributeError: 'int' object has no attribute 'get'
bug-account-string:     event_owner RAISED AttributeError: 'str' object has no attribute 'get'
bug-account-int:        event_owner RAISED AttributeError: 'int' object has no attribute 'get'
main-poison: main() RAISED AttributeError — fetch_calls=[] ack_calls=[]
main-control (valid planned event only): rc=0 fetch_calls=['owner/repo'] ack_calls=[['good1']]

The main-poison case puts the malformed event first and a valid, planned owner/repo event second: the valid event is never fetched and never acked, no DRAIN-DROP line is written, and main() exits via the exception — the exact opposite of the #3326 design, in which this event class is acked and counted in dropped_foreign.

The existing tests in infra/github-backup/tests/test_drain_queue.py cover the missing-owner shape ({"id": "e10", ...} with no repository/installation at all, case 10) but no shape where owner/account is present with a non-object type — the shapes a hand-built or future payload (which event_repo's docstring explicitly says the drainer accepts) can carry.

Why it matters

Acceptance criteria

  • event_owner returns None (→ DRAIN-DROP + ack + dropped_foreign count) for events whose repository.owner or installation.account is any non-dict value (string, list, number, bool), instead of raising.
  • A drain run containing such an event first still fetches and acks the valid planned events behind it and exits 0.
  • Regression cases in infra/github-backup/tests/test_drain_queue.py for at least the string-owner and string-account shapes, asserting drop + ack + no fetch for the malformed event and normal processing for a following valid event.
  • No behavior change for well-formed events: existing 54 drain-queue tests still pass.

Related: #3326 (the allowlist this bug lives in; its implementation commit cc35ae40 is on current main). Distinct from #3300 (reconcile clone-pass exit code), #3279/#3240 (non-atomic state writes), #3238/#3229 (inventory/mirror validity).

Activity

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

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-10-31 23:20 CDT (2026-11-01T04:26Z)
    • Found via: filed this run
    • Task: Triage the drain_queue owner-allowlist crash on non-dict owner/account shapes (poison-pill for the whole drain queue)
    • Expectation: effort/s — done looks like event_owner returning None for non-dict owner/account, the malformed event DRAIN-DROPped + acked, valid events behind it processed, regression tests in test_drain_queue.py
    • Links: evidence in issue body; laptop probe ~/git/fleet/probe-2026-10-31-laneB-drainowner/ (real drain_queue.py, sha256 af2fbb2c matched to main 72e3476)
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed (addendum, same run)

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Found via: filed this run — second malformed shape verified in the same probe session, same drop path, same fix area (the DRAIN-DROP path must never raise on a malformed event)
    • Evidence: an event with no id at all ({"event":"push","ts":...} — no repo, no owner) reaches the drop branch: the DRAIN-DROP line IS written (delivery=None, via ev.get("id") at drain_queue.py:170), then ack.append(ev["id"]) at :172 raises KeyError: 'id'. Laptop probe2 (same sha-matched module): main() RAISED KeyError: 'id' fetch_calls=[] ack_calls=[] — the valid planned event behind it is again never fetched, the ack POST never happens, so this event also re-crashes every drain run (and appends a fresh DRAIN-DROP line each time).
    • Acceptance addition: an id-less malformed event must not raise in the drop path — it cannot be acked by id, so the run should log the drop, skip the ack for that event, continue with the remaining events, and exit normally; a regression case pins this shape alongside the string-owner/string-account cases.
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 3 remaining items

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. added a commit that references this issue on Nov 2, 2026
  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 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