Skip to content

cold-store-reaper: resumable upload loops forever on a 308 whose Range stops advancing — no progress check or retry cap, and a Range-less 308 counts unacknowledged bytes as sent #2797

Closed

Description

@dezren39

Problem

ColdStore._resumable_upload (pkgs/cold-store-reaper/cold_store.py:812-843, main c90fcbc7) loops on HTTP 308 with no progress check, no retry cap, and no stall detection:

while sent < total or total == 0:
    ...
    if status == 308:
        range_hdr = headers.get("Range") or headers.get("range") or ""
        try:
            sent = int(range_hdr.rsplit("-", 1)[1]) + 1
        except (IndexError, ValueError):
            sent += len(chunk)
        f.seek(sent)
        continue

Two verified failure modes in the same loop:

  1. A 308 whose Range stops advancing loops forever. sent is recomputed from the server's acknowledged range on every iteration, so if Drive keeps answering 308 Range: bytes=0-0 (session wedged, bytes landing nowhere), sent is pinned at 1 and the client re-PUTs the same chunk indefinitely — no maximum attempts, no requirement that sent increase, no backoff. The upload never fails, never finishes, and the nightly job hangs while hammering the Drive API.
  2. A 308 with no Range header is treated as if the whole chunk landed. The fallback sent += len(chunk) assumes bytes the server never acknowledged (per the Drive resumable protocol, a 308 without Range means zero bytes received so far). For a single-chunk file the client then exits the loop and reports upload finished without a file id — a misleading error that hides the actual incomplete upload; for a multi-chunk file it skips unacknowledged bytes and only the post-upload md5 check in upload() catches the resulting corruption, after the whole file has been sent.

The existing fake-Drive test (tests/test_cold_store.py:186-212) always returns a Range that advances by exactly the bytes received, so neither case is exercised.

Evidence (verified this run, main c90fcbc7)

Laptop probe (~/git/fleet/probe-2026-10-30-laneB-coldstore308/, real module imported from the laptop clone; cold_store.py md5 882a83a1d80b409192d5ae1db3ccb975 matches the sandbox clone). Transport monkeypatched; no credentials involved:

probe.py  (PUT always -> 308, Range: bytes=0-0; sentinel cap at 30 PUTs)
RESULT: PROBE-CAP: 30 PUTs reached, loop still going
PUTs issued: 31 (file is 1000 bytes, chunk cap 8MiB -> a healthy upload needs 1-2 PUTs)

probe2.py (PUT always -> 308, no Range header)
RESULT: ColdStoreError: upload finished without a file id
PUTs issued: 1 (server acknowledged 0 bytes, yet the client treated the chunk as sent and left the loop)

Dedup: title + keyword search over open and closed issues (cold-store resumable, 308, upload loop) finds nothing covering this loop — #2733 is the S3 list-page parser in the same package, #938 (closed) was upload-limit enforcement in upload()/put_backup.

Why it matters

cold-store-reaper is the fleet's backup path to Drive; its uploads run unattended (nightly). A wedged resumable session currently turns into an infinite, silent PUT loop instead of a failure the job can report and retry tomorrow — the bundle stays in uploading state and the run never completes. The missing-Range fallback additionally misreports an incomplete upload as a finished one.

Acceptance criteria

  • _resumable_upload terminates with a ColdStoreError when a 308 response does not advance the acknowledged offset (no-progress detection), and/or after a bounded number of 308 retries — a stuck session fails the upload instead of looping forever.
  • A 308 with a missing or unparseable Range header no longer assumes the whole chunk was received; the client re-sends from the last server-acknowledged offset (or queries the session status, per the Drive resumable protocol) — triage pins the exact approach.
  • Tests extend the fake-Drive coverage with (a) a 308 whose Range never advances and (b) a 308 with no Range header; the existing resumable-upload tests still pass.
  • Healthy-path behavior is unchanged: advancing 308s resume from the acknowledged byte, and a completed upload still returns the file id.

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/cold-store-reaperPackage/app: cold-store-reaper (pkgs/ or apps/ in this repo)
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b + run time 2026-10-30 13:20 CDT
    • Found via: filed this run
    • Task: Triage the resumable-upload 308 loop in pkgs/cold-store-reaper (no-progress 308 loops forever; Range-less 308 counts unacknowledged bytes as sent) and pin the stall/retry approach
    • Expectation: effort/s — done looks like progress detection + bounded 308 retries in _resumable_upload, with fake-Drive tests for a non-advancing Range and a missing Range
    • Links: probe laptop ~/git/fleet/probe-2026-10-30-laneB-coldstore308/ (probe.py, probe2.py)
  3. 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
  4. 2 remaining items

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Correction to the findings comment above: the follow-up small_fix is queue task 5413 (the placeholder in that comment did not render the id).

    🤖 Agent note · done

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    work/assignedQueue worker lifecycle state
    queue/claimedQueue 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 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), lane C
    • Session / ID: queue task 5413
    • Found via: queue claim
    • Task: Implement the triage-pinned fix for _resumable_upload 308 stalls: progress = strictly-increasing acknowledged offset; missing/unparseable Range = no progress (never sent += len(chunk)); 3 consecutive no-progress 308s raise ColdStoreError. Plus the three regression tests.
    • Expectation: effort s; done = fix + tests pass, PR merged, issue closed.
    • Links: task 5413
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), lane C
    • Session / ID: queue task 5413
    • Found via: queue claim
    • Task: Bounded _resumable_upload 308 stalls per the triage-pinned approach: progress = strictly-increasing acknowledged offset; missing/unparseable Range = no progress (chunk resent from last acknowledged offset); 3 consecutive stalls raise ColdStoreError naming the stall.
    • Expectation: done — PR fix(cold-store-reaper): bound resumable-upload 308 stalls #3479 merged, issue closed; python3 -m unittest tests.test_cold_store 99 tests OK (1 skipped) on laptop incl. 3 new stall tests; Codex review completed with no findings.
    • Links: PR fix(cold-store-reaper): bound resumable-upload 308 stalls #3479 · branch queue/5413-2797-upload-308-stall
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/cold-store-reaperPackage/app: cold-store-reaper (pkgs/ or apps/ in this repo)effort/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