Skip to content

gateway: a total deadline per request; up-front body reads time out on h2c (D264, D265) - #126

Merged
jaredLunde merged 5 commits into
mainfrom
request-deadlines
Oct 4, 2026
Merged

jaredLunde merged 5 commits into
mainfrom
request-deadlines

Conversation

@jaredLunde

Copy link
Copy Markdown
Contributor

From the long-run audit: requests that could live forever and hold a tenant slot.

D264: no request had an absolute deadline. long_running rows had no read timeout and others only bounded the gap between reads, so a stream kept alive by bytes or h2 PINGs held a tenant slot, the in-flight gauge and its budget indefinitely. New request_max_secs, default 4 h (14400 s; covers the catalog's largest output, 384k tokens, at ~27 tok/s), 0 disables.

  • Silent requests: each attempt's read timeout is capped at the time left (no extra timer); a walk reaching a candidate after the deadline ends before connecting.
  • Moving streams: one relaxed atomic load per chunk against a coarse per-process clock (src/deadline.rs, one ticker thread advancing once a second, started only when the ceiling is on). No syscall or timer per chunk; a cut can land up to 1 s late.
  • Expiry before the head: 504 "request exceeded the gateway's maximum duration"; after: cut like existing mid-stream cuts (cut_short, billed an estimate). Large-body re-runs inherit the parent's deadline.
  • A policy limit, not a provider failure: no breaker hit, no failover, no key cooling. New ai_rejections_total{reason="request_deadline"}.

D265: an h2c client stalling during the up-front body read had no timeout at all (pingora's h2 body read has none), holding a tenant slot indefinitely. On HTTP/1.1 pingora's 60 s fired but surfaced as 504 "upstream timed out" for a request no provider saw. New client_read_timeout_secs, default 60 s; applied to h1 via pingora and as a per-read timer on h2 only. Up-front read errors are tagged as the client's: a stall is 408 "request body timed out" (consistent with D260), nothing billed, no provider contacted.

Tests (tests/request_deadlines.rs, 8): endless stream cut and slot released; silent provider 504 on an ordinary and a -pro row with no failover/breaker/key effect; ceiling off at 0; h2c and HTTP/1.1 up-front stalls → 408 with no upstream contact; trickled up-front body; trickled BYO upload; re-run inherits the deadline; late candidate never contacted. 18/18 hand mutations caught. ARCHITECTURE.md, config example updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk

@jaredLunde
jaredLunde enabled auto-merge (squash) October 4, 2026 18:04
jaredLunde and others added 5 commits October 4, 2026 11:38
…p front times out (D264, D265)

No request had an absolute deadline: every bound was per read or per write,
and long_running (-pro) rows had no read timeout at all, so a stream kept
moving by its own bytes or kept alive by HTTP/2 PINGs held its tenant slot,
the in-flight gauge and body budget for as long as the client did not hang up.
request_max_secs (default 4h: the catalog's largest max output, 384k tokens,
at ~27 tok/s; 0 disables) fixes a deadline in request_filter (FullBody re-runs
inherit it). upstream_peer caps each attempt's read timeout at the time left
(read_capped), so a silent request ends exactly at the deadline; a moving one
is cut per chunk, both ways, against a coarse clock (deadline.rs: one relaxed
atomic load, one ticker thread per process). Before the head: a 504 "request
exceeded the gateway's maximum duration"; after: a cut stream (cut_short,
estimated). A policy limit: no breaker failure, no resend/failover, no key
cooled; ai_rejections_total{reason="request_deadline"}.

An h2c client that stalled while peek_body_model read its body up front had
no timeout at all (pingora's HTTP/2 server sets none) and held its tenant slot
forever; on HTTP/1.1 pingora's 60s timeout fired but, untagged, was answered
504 "upstream timed out". client_read_timeout_secs (default 60, pingora's own
HTTP/1.1 value; 0 disables) is set as the session read timeout (HTTP/1.1) and
wraps each up-front read where the session has none (HTTP/2); the peek's I/O
errors are tagged downstream, so a stall is a 408 with no provider contacted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
…nd on h1 and h2c (D265)

Kills the two changed-lines survivors: a 0 s bound applied when disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
…ond, same instant)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
@jaredLunde
jaredLunde merged commit 2969a5d into main Oct 4, 2026
17 of 19 checks passed
@jaredLunde
jaredLunde deleted the request-deadlines branch October 4, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant