Skip to content

lab-ops-hub: container crawler's 20s timeout is per-socket-read, not a deadline — a slow-drip target hangs the single-threaded crawler indefinitely #532

Closed

Description

@dezren39

Problem

The Lab Ops Hub container crawler's 20-second timeout is a per-socket-operation timeout, not a deadline — a slow-drip target defeats it indefinitely, unlike the Worker fallback's absolute 20s deadline. Worse, the container serves requests on a single thread, so one dripping crawl wedges the whole crawler service.

Evidence on current main (a8a3bf7):

Reproduction (verified this run)

Local drip server (headers, then 1 byte/second) read with exactly the container's call shape but timeout=3:

with urllib.request.urlopen(url, timeout=3) as res:
    raw = res.read(2_000_000)

The read completed after ~15s (15 bytes at 1 byte/s) despite the 3s timeout — each individual socket read finished within 3s, so no timeout ever fired. With the production timeout=20 and a 19s drip interval, a crawl could in principle run for months of wall-clock time; in practice it runs until something external kills it, blocking the single-threaded server the whole time.

Why it matters

POST /api/crawl targets are caller-supplied public URLs (validated, but a validated public server can still be slow or malicious). One drip crawl:

  • holds the container's only request thread indefinitely — the health endpoint and all later crawls stall;
  • defeats the sleepAfter=2m idle-kill economics the README relies on (the container is busy, not idle, and burns container minutes);
  • makes the Worker's 120s container-dispatch timeout fire while the container keeps crawling in the background, so retries pile onto an already-wedged service.

Acceptance criteria

  • The container crawl enforces a total deadline for a whole /crawl fetch (headers + body, per redirect hop or overall — document which), comparable to the Worker fallback's 20s AbortSignal.timeout semantics.
  • A regression test in pkgs/lab-ops-hub/test/test_container_validate.py (or a sibling suite run by npm test) drives a drip/slow server (or a fake transport with the same per-read behaviour) and shows the crawl aborting at the deadline instead of completing.
  • The fix does not weaken any existing validation: npm test in pkgs/lab-ops-hub passes (both TS and Python suites).
  • Optional but welcome: note the single-threaded HTTPServer coupling in the fix or file it as a follow-up — a ThreadingHTTPServer alone does not fix the unbounded crawl, so it is not a substitute for the deadline.

Suggested fix

Track a monotonic deadline in the crawl path: e.g. wrap the response read in a loop with res.read1/read(65536) chunks accumulating to the existing 2,000,000-byte cap, checking time.monotonic() against a start timestamp before each chunk and aborting past the deadline (chunked reads also match the Worker's capped-reader shape). Redirect hops go through the same opener, so decide and document whether the deadline is per hop (matching the Worker) or for the whole crawl.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/sSmall: expected under 2 hours of work
    app/lab-ops-hubPackage/app: lab-ops-hub (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 26, 2026
  2. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    Intake review

    Verified still live on current main 14dd66b6 (2026-10-26, fleet monitor). In pkgs/lab-ops-hub/container/app.py, the crawl still opens with _OPENER.open(req, timeout=20) (:1230 — the per-socket-operation timeout the body describes; the line has moved since filing as the file grew) and the container still serves with the plain single-threaded HTTPServer(...).serve_forever() (:1320) — no ThreadingHTTPServer, no absolute deadline. The slow-drip hole and the one-stuck-crawl-wedges-everything failure mode are both intact. The issue arrived with a full acceptance-criteria section and a verified reproduction, so this pass is labels + this comment only.

    Related: #500 / #524 — the Worker fallback fix (AbortSignal.timeout(20_000) in pkgs/lab-ops-hub/src/lib.ts) whose absolute-deadline semantics are the model to mirror; the body shows the container's timeout=20 was cited as the model for that fix while meaning something weaker.

    Risks / suggested approach: effort/s: give the container crawl one absolute deadline shared across connect, headers and body read (mirroring the Worker's per-hop 20s), and consider ThreadingHTTPServer so one stuck crawl cannot wedge the health endpoint — the body's acceptance criteria cover both the deadline and a drip-server regression test. The security shape (a reachable slow endpoint as a one-request DoS of the whole crawler service) is why this is priority/normal rather than low despite the small diff.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), lane F
    • Session / ID: queue task 2119 (triage)
    • Found via: queue claim
    • Task: Triage verdict for lab-ops-hub: container crawler's 20s timeout is per-socket-read, not a deadline — a slow-drip target hangs the single-threaded crawler indefinitely #532 — CONFIRMED on current main 558cc1d23e1d2d90df2a0d36189b5e585f12f99b.
    • Evidence:
      • pkgs/lab-ops-hub/container/app.py:1230 — response = _OPENER.open(req, timeout=20) (per-socket-operation timeout, unchanged since filing; the line moved from :363 as the file grew).
      • pkgs/lab-ops-hub/container/app.py:1237 — return status, res.read(2_000_000) with no total deadline or chunk loop.
      • pkgs/lab-ops-hub/container/app.py:1320 — plain single-threaded HTTPServer(("0.0.0.0", 8080), Handler).serve_forever() (import at :22 is HTTPServer only, no ThreadingHTTPServer).
      • Independent laptop reproduction this run: a local server dripping 1 byte/second read with exactly the container's call shape (urlopen(url, timeout=3) then res.read(2_000_000)) completed in 8.1s with 8 bytes — no timeout ever fired, confirming the timeout bounds each socket read, not the fetch.
      • No open or merged PR supersedes this (search for 532 in PR title/body returns only the unrelated id(roc): F11.15 smaller example modules — wasm-opt -Os + strip, all 7 rebuilt ≤400 KB (#555) #1453).
    • Route: scoped follow-up fix enqueued as queue task 4817 (kind small_fix, dedupe repo-health:v1:fix:developing-today/code:issue:532): monotonic total deadline in _fetch_crawl, chunked reads to the existing 2 MB cap, per-hop vs whole-crawl documented, drip regression test in the lab-ops-hub Python suite, npm test in pkgs/lab-ops-hub green. ThreadingHTTPServer explicitly excluded as a substitute.
    • Expectation: issue stays open pending task 4817; labels synced to queue/queued for the follow-up.
  6. 1 remaining item

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: ses_e5e683b6d8effesiXqmP1KmndR / queue task 4817
    • Found via: atomic claim from repo-health-code-pilot; issue revalidated open on 2026-11-01.
    • Task: Enforce a total deadline in the lab-ops-hub container crawler and add a drip regression test, within the queued packet scope.
    • Expectation: inspect fresh main and current PR state; implement and run focused tests, then open/complete or park if CI is pending.
  9. 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
  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: ses_e5e683b6d8effesiXqmP1KmndR / queue task 4817
    • Found via: queue claim; implementation and local validation complete.
    • Task: PR fix(lab-ops-hub): enforce crawler fetch deadline #3761 adds the container crawler deadline and a slow-drip regression test.
    • Evidence: npm test passed (TypeScript 53/53, Python 64/64); focused TestFetchCrawl passed 11/11.
    • Checks: PR is open and merge state is clean; the only check is CodeRabbit skipping (neutral), with no pending CI checks.
    • Expectation: review/merge PR fix(lab-ops-hub): enforce crawler fetch deadline #3761; issue remains open until merge.
  11. dezren39 commented on Nov 1, 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/lab-ops-hubPackage/app: lab-ops-hub (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)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/humanFiled directly by a humanstatus:in-review

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions