Skip to content

feat(client): multiplex HTTP/2 requests and clear connections - #13

Merged
takeokunn merged 45 commits into
mainfrom
takeokunn-http-stack-phase5-p2
Oct 25, 2026
Merged

takeokunn merged 45 commits into
mainfrom
takeokunn-http-stack-phase5-p2

Conversation

@takeokunn

Copy link
Copy Markdown
Contributor

Concurrent HTTP/2 calls share a per-connection scheduler that serializes stream admission and connection-wide state while response streams progress independently. Clearing the client pool invokes the HTTP/2 manager's clear callback. The GOAWAY boundary and draining transition are captured under the same scheduler lock used for stream admission, so a racing request is either included in GOAWAY's boundary or rejected before HEADERS are sent; subsequent calls open a fresh connection. Tests cover active-stream clearing, overlapping requests, and the GOAWAY/open race.

@takeokunn
takeokunn force-pushed the takeokunn-http-stack-phase5-p2 branch from 5560097 to 071ee76 Compare October 23, 2026 17:53
takeokunn added a commit that referenced this pull request Oct 23, 2026
The child-merge test kept three decoded payload trees and their deep copies
live while building canonical inputs. Main and PR #13 CI exhausted the SBCL
heap in `accepted-payload`, before `merge-pending` ran. Build canonical inputs
one ticket at a time and retain the payload-integrity and exact native-union
checks.
@takeokunn
takeokunn force-pushed the takeokunn-http-stack-phase5-p2 branch from 73173fc to c6ca298 Compare October 23, 2026 19:34
takeokunn added a commit that referenced this pull request Oct 24, 2026
The child-merge test kept three decoded payload trees and their deep copies
live while building canonical inputs. Main and PR #13 CI exhausted the SBCL
heap in `accepted-payload`, before `merge-pending` ran. Build canonical inputs
one ticket at a time and retain the payload-integrity and exact native-union
checks.
@takeokunn
takeokunn force-pushed the takeokunn-http-stack-phase5-p2 branch from 44c4300 to 9d0d37f Compare October 24, 2026 03:19
The successful END_STREAM assertion now expects three connection-level WINDOW_UPDATE frames instead of four.
The successful response path returns stream and connection credit for earlier DATA, then connection credit only for END_STREAM DATA.
The scheduler reports the unexpected stream ID directly, so the public boundary test expects 3.
On SBCL, `%h2-scheduler-open-entry` waits for peer capacity before creating
the next request's entry. The zero-capacity test completes stream 1, then
receives DATA on stream 3 while the second request is waiting. No request
stream is active, so `%h2-connection-wait-for-stream-capacity` is the matching
path and reports `(frame-type stream-id)`. eed5954 changed only this test
assertion from `(0 3)` to `3`. Keep `(0 3)` for this state. When any
scheduler entry without an error remains, preserve scalar stream-ID detail
for unknown DATA, as in the existing batch path.

The non-SBCL path in `ping-http2-connection` signals `http-protocol-error`
with operation `:http2-client` and detail `:eof` when the peer closes while
`ping-http2-connection` awaits an ACK. On SBCL, requests and PING share the
scheduler reader, which may record EOF while a PING call waits. Store that
reader failure on the pending PING waiter and propagate EOF as the same
public protocol error. If the PING write fails before the reader records
EOF, propagate the write error.
The direct PING path reports unexpected frames with :http2-client and the frame
as its detail. When a PING is pending with no non-error request entry, the
scheduler must preserve that behavior; otherwise DATA on a retired stream is
discarded and the PING call reports the later EOF instead of the DATA-frame
error. Keep draining retired DATA while a non-error request entry remains
active.
The child-merge test kept three decoded payload trees and their deep copies
live while building canonical inputs. Main and PR #13 CI exhausted the SBCL
heap in `accepted-payload`, before `merge-pending` ran. Build canonical inputs
one ticket at a time and retain the payload-integrity and exact native-union
checks.
The #-SBCL ping-http2-connection path reports a malformed PING received while
waiting for its acknowledgement as :http2-client with the received frame as
detail. The #+SBCL scheduler instead reported :http2-control and a
(stream-id payload-length) summary for the malformed frame.

Keep :http2-control and summary detail for malformed PING frames without a
pending waiter. While a PING waiter is pending, report :http2-client and the
received frame as detail. Document the PING ACK-wait error and the existing
unknown-DATA detail distinction in the API reference and changelog.
RFC 9113 Section 5.1 closes a stream after both endpoints send END_STREAM and forbids sending frames other than PRIORITY on a closed stream. Section 6.9.1 defines separate stream and connection flow-control windows. These bodyless GET requests end their request streams in HEADERS. Both responses in http2-public-connection-reuses-session end in DATA: the first uses h2-response-wire, which sets END_STREAM on nonempty DATA, and the second DATA frame has flags 9 (END_STREAM and PADDED). Once each response ends, the stream is closed, so only connection-level flow credit is returned.

Return connection credit for every DATA frame so other streams can continue. Return stream credit only before response END_STREAM. For scheduler-delivered body callbacks, carry END_STREAM in the queued event so terminal DATA returns connection credit only. http2-public-single-blocked-upload-keeps-response exercises that callback path; http2-sends-window-updates-after-data verifies stream credit for nonterminal DATA.
RFC 9113 Section 5.1 bars frames other than PRIORITY on a closed stream, and Section 6.9.1 defines separate stream and connection flow-control windows. Both bodyless GET request streams end in HEADERS. In http2-public-connection-reuses-session, both responses end with DATA: h2-response-wire sets END_STREAM on the first nonempty DATA frame, and the second DATA frame has flags 9 (END_STREAM and PADDED). Once each response ends, the stream is closed. Expect one connection-level WINDOW_UPDATE per terminal frame and no extra frames after the second update. http2-sends-window-updates-after-data checks stream-level credit for nonterminal DATA.
RFC 9113 Section 5.1 prohibits frames other than PRIORITY after a stream is closed; Section 6.9.1 defines distinct stream and connection flow-control windows. This test processes four response DATA frames: three are nonterminal and stream 3 second DATA frame ends that stream. The HTTP/2 transport returns connection credit for each DATA frame and stream credit only before that stream is closed. Assert four stream-0 updates, two stream-1 updates, and one stream-3 update, omitting stream credit on the closed stream.
In `http2-public-gap-producer-chunks`, `t/support.lisp:114-120` builds
nonempty response DATA with END_STREAM, putting the client stream in
half-closed (remote). RFC 9113 §5.1
(https://www.rfc-editor.org/rfc/rfc9113.html#section-5.1) does not require
maintaining a receive window in that state, and §6.9.1
(https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1) distinguishes
stream and connection windows.
The test expects the client behavior in
`http2/transport-read-response.lisp:141-149`: after terminal DATA, only
connection credit is restored, with a connection-level WINDOW_UPDATE on
stream 0 and no stream-level update. In the expected frame list, type 8 is
WINDOW_UPDATE; the final type-8 frame remains on stream 0, while the removed
type-8 frame was on stream 1.
The successful case at `t/tests-http2-open-stream.lisp:2865-2868` feeds a
response wire sequence to the client: HEADERS with PADDED and PRIORITY flags,
then padded DATA with flags 9 (PADDED and END_STREAM). The client request is
a bodyless GET. `http2/transport-request.lisp:255-260` passes the empty-body
condition to the header encoder, which sets END_STREAM on the request
HEADERS (`http2/transport-headers.lisp:156-168`). The stream is therefore
half-closed (local) before the response; receiving response END_STREAM
transitions it to closed (RFC 9113 §5.1,
https://www.rfc-editor.org/rfc/rfc9113.html#section-5.1). Section 6.9.1
(https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1) distinguishes
stream and connection windows. `http2/transport-read-response.lisp:141-149`
restores connection credit and skips stream credit after terminal DATA. Keep
the type-8 WINDOW_UPDATE on stream 0 in the successful-case expectation and
remove the stream-1 update.
The test's callback runs during the request upload's wait for WINDOW_UPDATE credit. At each callback, the old checks required all scripted peer frames, including the trailing WINDOW_UPDATE frames, to have been consumed and all 65,536 request bytes to have been sent. In the fixture, response callbacks arrive before the trailing WINDOW_UPDATE frames let the last request byte be sent. Assert that each callback occurs before all scripted peer frames are consumed, and that the sent bytes form a prefix of the request body and are fewer than its full length. Existing post-completion checks continue to verify the full upload and DATA frame layout.

RFC 9113 §5.1 permits the local endpoint to keep sending after the peer has half-closed its side of the stream; §§6.9 and 6.9.1 define WINDOW_UPDATE credit and the connection/stream windows. The scheduler queues a callback for each response DATA frame at http2/transport-connection.lisp:1717-1743 and runs queued callbacks while upload waits for credit at http2/transport-connection.lisp:1910-1917. The corrected callback checks and retained post-completion checks are at t/tests-http2-response-boundaries.lisp:454-480.

References:
- https://www.rfc-editor.org/rfc/rfc9113.html#section-5.1
- https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9
- https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1
Share a retained HTTP/2 connection across concurrent high-level single-request calls with the same key. Keep batch calls exclusive. After a batch writes request HEADERS, record each stream ID in scheduler state. When the batch reader consumes initial peer SETTINGS, mark the shared session started and clear the first-frame guard. A request refused after GOAWAY retires the connection once no active streams remain.

Clamp per-request header and body limits to connection caps. Document and test connection-wide response-dispatch failures; RFC 9113 §5.4.1 permits an endpoint to treat a stream error as a connection error. Keep max-connections as a retained-entry limit because the manager does not define whether calls wait or fail at a total-connection cap.

The terminal-DATA test expects connection-level credit only. RFC 9113 §5.1 allows an endpoint to stop maintaining the receive window for a half-closed (remote) stream, while §6.9.1 distinguishes stream and connection flow control. The scheduler returns connection credit after END_STREAM at http2/transport-connection.lisp:1751.

The native-client test counts one TLS connection for the first two requests, one after client clear, and one after pool clear. Its final overlapping pair reuses the last connection, so the total is three. This follows the same-key multiplexing contract and default origin key in docs/src/reference/api.md:624-626, 632.
A concurrent request can register the shared connection between the initial lookup and the opening claim. Recheck the manager before invoking the open callback so the caller leases that entry.
Keep the client path covered when concurrent first requests race while the HTTP/2 session is being opened.
Cover a close after PING bytes are written but before the write returns, late callback delivery, and scheduler error and closed-state branches.
Add cases for serial fallback reuse, peer-reset session reuse, control-frame handling, retired-stream dispatch, and scheduler callback delivery, flow-control updates, and retirement.
@takeokunn
takeokunn force-pushed the takeokunn-http-stack-phase5-p2 branch from dd67ff0 to fc8ca5c Compare October 24, 2026 19:38
@takeokunn
takeokunn marked this pull request as ready for review October 25, 2026 03:32
@takeokunn
takeokunn merged commit 48d5a01 into main Oct 25, 2026
2 checks passed
@takeokunn
takeokunn deleted the takeokunn-http-stack-phase5-p2 branch October 25, 2026 03:32
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