Skip to content

Let a callback select a bounded per-request deadline - #7

Merged
renerocksai merged 3 commits into
mainfrom
feat/request-deadline
Sep 25, 2026
Merged

renerocksai merged 3 commits into
mainfrom
feat/request-deadline

Conversation

@renerocksai

@renerocksai renerocksai commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Config.timeout_ms bounds every request cycle, and streaming flushes do not reset it.
One value serves two needs badly.
An event stream (SSE) wants minutes, and an ordinary request wants seconds.
A large global value lets any stalled request hold a connection for the same long time.

Change

  • Config.max_timeout_ms (default 0, disabled) bounds the longest deadline a callback may select.
    Validation requires timeout_ms ≤ max_timeout_ms ≤ 24 h when the value is nonzero.
  • Context.setRequestTimeout(ms) selects the deadline of the current request: ms after the request started.
    It returns InvalidRequestTimeout for zero or values above the bound, RequestTimeoutUnavailable when disabled, and Cancelled after cancellation.
  • The owner adopts the value after the callback publishes its result or a worker stream snapshot.
    The worker writes one slot field before the phase release, and the owner reads it after the acquire.
  • Earlier unsent pipelined responses keep their deadline; the new deadline applies when their batch completes.
  • A finished request restores timeout_ms for the idle keep-alive cycle and the next request.
  • Idle time no longer counts against a request (second commit). Before, a kept-alive request's deadline counted from the end of the previous response, so idle time shortened it.
    Found with tailscale serve, which reuses idle backend connections: with 15 s, a stream after 12 s idle ended after 3 s.
    Now a connection is idle while no request byte is buffered or active, and Config.idle_timeout_ms bounds that wait (0 = timeout_ms, the previous behavior).
    The first received byte starts the request deadline, including the one from setRequestTimeout.
  • No memory reservation changes. A longer deadline only holds one admitted connection for longer.

The reference server gains --max-timeout-ms and /extended-wait-only.
docs/USING.md documents the option and the method.

Motivation

Baz applications such as the omajot hub serve an SSE "doorbell" next to short JSON routes.
A follow-up Baz PR exposes this as RouteOptions.timeout_ms.

Verification (exact Zig 0.16.0)

Host Gate Result
Linux omarx1 (io_uring) zig build verify pass
Linux zig build test 123 passed, 2 skipped
Linux continuation_integration.py (ReleaseSafe) 24/24 groups, including new extended_request_deadline, idle_keep_alive_then_request, idle_timeout_is_separate in both modes
Linux idle_keep_alive_then_request against the first commit 96c249b fails in both modes (EOF mid-request), which confirms the regression test
Linux integration.py, batch, inline, gather, arena_lifecycle 34 / 29 / 10 / 11 / 8 passed
macOS maxross (kqueue) zig build test 120 passed, 4 skipped
macOS continuation_integration.py 18 passed, 2 single-owner skips
macOS integration.py 30 passed

Windows was not run locally; the hosted Windows workflow covers it.

Not in this PR

Graceful termination at the deadline: today the owner closes a chunked stream without the final 0\r\n\r\n chunk.
Browsers reconnect, but strict clients such as std.http.Client report ReadFailed.
A later change could deliver a final callback, or write the terminating chunk at a chunk boundary.
With a per-request deadline, an application can also finish its stream just before the deadline.

🤖 Generated with Claude Code

`timeout_ms` bounds every request cycle, including long event streams.
A stream route now calls `Context.setRequestTimeout(ms)` to select a longer
deadline for its own request. `Config.max_timeout_ms` bounds the value and
defaults to zero, which disables the override.

The scheduler adopts the value when the callback returns or flushes. Earlier
unsent pipelined responses keep their deadline. A finished request restores
`timeout_ms` for the idle keep-alive cycle and the next request.

The reference server adds `--max-timeout-ms` and an `/extended-wait-only`
route. The continuation suite checks extension, bound and reset in both
execution modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renerocksai and others added 2 commits September 25, 2026 18:38
The deadline of a kept-alive request counted from the end of the preceding
response. Idle time before the next request shortened that request's
deadline, including an extended `setRequestTimeout` deadline. Proxies such
as `tailscale serve` reuse idle backend connections, so a stream that began
after 12 idle seconds of a 15-second budget ended after 3 seconds.

A connection is now idle while no request byte is buffered or active. The
new `Config.idle_timeout_ms` bounds that wait after accept and between
requests; zero selects `timeout_ms`, the previous behavior. The first
received byte ends the idle state and starts the request deadline.

Two continuation groups cover a delayed request on a kept-alive connection,
with and without an extended deadline, and a separate idle bound. The first
group fails against the previous commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@renerocksai
renerocksai merged commit 874a8f3 into main Sep 25, 2026
5 checks passed
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