feat(core): add HTTP/2 stream dispatch
This commit is contained in:
@@ -927,3 +927,27 @@ path remains allocation-free and avoids duplicating the response model.
|
||||
existing model internally without introducing a second public header abstraction.
|
||||
|
||||
---
|
||||
|
||||
## DEC-27 — Drain already-buffered frames before dispatching completed streams
|
||||
|
||||
**Context.** A client can write a burst of complete requests before the server schedules their
|
||||
handlers. Dispatching after every individual HEADERS frame lets a very fast handler close and
|
||||
release streams while the same inbound burst is still being decoded, making the advertised
|
||||
concurrency limit dependent on virtual-thread scheduling. Waiting a fixed interval would make the
|
||||
limit deterministic but would add latency to every ordinary request.
|
||||
|
||||
**Decision.** Completed bodyless streams enter a fixed queue bounded by
|
||||
`MAX_CONCURRENT_STREAMS`. The demultiplexer continues only while its own frame reader already has
|
||||
bytes buffered; as soon as consuming the next frame would require network input, it drains the
|
||||
queue to the shared virtual-thread executor. The configured concurrent-stream limit is 64 and the
|
||||
primitive stream table has exactly the same bound.
|
||||
|
||||
**Consequence.** One socket read's request burst is admitted and bounded as a unit, excess streams
|
||||
receive `REFUSED_STREAM`, and a single request is dispatched immediately without a timer. The demux
|
||||
still never executes application code or waits for a worker. h2spec's concurrency case passes and
|
||||
the lifecycle benchmark remains at the allocation noise floor.
|
||||
|
||||
**Revisit when.** If production traces show a materially different batching pattern, tune the
|
||||
advertised limit or reader size from measurements; do not add a sleep-based dispatch delay.
|
||||
|
||||
---
|
||||
|
||||
@@ -71,7 +71,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`.
|
||||
| 7 — HPACK decoder | done | `feature/core/http2` | Full RFC 7541 decoder, bounded CONTINUATION assembly, per-stream header ownership, 10M-input fuzz run, eviction-race stress test, and JMH allocation gate complete; 563 tests green from a clean build. |
|
||||
| 8 — Connection state machine | done | `feature/core/http2` | Preface, transactional SETTINGS, priority PING ACK, connection WINDOW_UPDATE, two-stage GOAWAY, per-socket transport/ALPN dispatch, HPACK block composition, clean curl handshake, h2spec 28/35 selected cases and 0.008 B/op JMH gate complete. Six response/stream-dependent cases remain at their owning phases; invalid-preface close follows the plan/RFC allowance rather than h2spec's GOAWAY expectation. |
|
||||
| 9 — HPACK encoder + h2 response path | done | `feature/core/http2` | Stateless static-table HPACK encoder; precompiled status/content-type/date fields; reusable response writer with header filtering, bounds, CONTINUATION splitting and fixed DATA happy path; HTTP/1/2 serializer parity test. EX-46 fixed the one-digit Date day-of-month bug. JMH: 174.309 ns/op, 0.001 B/op (noise floor), no GC. 603/603 tests green from a clean `-Pjmh` build. |
|
||||
| 10 — Stream state machine + dispatch | not started | — | — |
|
||||
| 10 — Stream state machine + dispatch | done | `feature/core/http2` | Explicit stream transition table, bounded primitive stream table and pool, pseudo-header/message validation, protocol-neutral `Request` assembly, virtual-thread dispatch and exception path, cancellation-safe release, raw h2c + Java HTTP/2 integration. h2spec sections 5/8: 37/39; the two content-length/DATA accounting cases are owned by Phase 11. JMH pooled lifecycle: 458.499 ns/op, 0.003 B/op, no GC. 618/618 tests green from a clean `-Pjmh` build. |
|
||||
| 11 — DATA, flow control, bodies | not started | — | — |
|
||||
| 12 — Trailers, half-close, gRPC | not started | — | — |
|
||||
| 13 — Security hardening & abuse resistance | not started | — | — |
|
||||
@@ -766,6 +766,18 @@ two-digit calendar day and could not exercise the boundary. **Fix**: use an expl
|
||||
`EEE, dd MMM yyyy HH:mm:ss 'GMT'` formatter for both protocol renderings and add a deterministic
|
||||
regression test for the third day of a month. **Phase**: 9.
|
||||
|
||||
### EX-47 — A reset queued stream could be returned to the pool before dispatch observed it
|
||||
|
||||
Found while closing the stream-dispatch cancellation paths. `receiveRstStream` transitioned a
|
||||
queued stream to `CLOSED` before deciding whether its release had to be deferred. The subsequent
|
||||
state check could therefore no longer see `HALF_CLOSED_REMOTE`, returned the object to the pool,
|
||||
and left the same object referenced by the dispatch queue. A following request could acquire and
|
||||
mutate it before the queue drained. **Fix**: capture the deferred-release condition before the
|
||||
transition, mark queued/dispatched streams cancelled, and let the sole queue/worker owner perform
|
||||
the final release. The regression test sends a complete request, immediately resets it, then sends
|
||||
a second request and proves that only the second handler invocation and response occur. **Phase**:
|
||||
10.
|
||||
|
||||
---
|
||||
|
||||
# PART III — The phases
|
||||
@@ -2445,16 +2457,15 @@ Flash never sends PUSH_PROMISE, so the two `reserved` states are unreachable for
|
||||
### Files
|
||||
|
||||
Created:
|
||||
- `h2/stream/Http2Stream.java` — per-stream state. Also the intrusive MPSC node (Phase 3) and
|
||||
the owner of the per-stream arena (Phase 7).
|
||||
- `h2/stream/Http2StreamState.java` — the state machine as an explicit transition table, not a
|
||||
pile of `if`s. Every transition cites its RFC clause.
|
||||
- `h2/stream/Http2StreamTable.java` — `int → Http2Stream`, open-addressed with linear probing,
|
||||
- `http2/stream/Http2Stream.java` — per-stream state and owner of the request/response resources.
|
||||
- `http2/stream/Http2StreamState.java` — the state machine as an explicit transition table, not a
|
||||
pile of `if`s.
|
||||
- `http2/stream/Http2StreamTable.java` — `int → Http2Stream`, open-addressed with linear probing,
|
||||
power-of-two capacity, zero-alloc lookup/insert/remove, sized from `MAX_CONCURRENT_STREAMS`.
|
||||
- `h2/message/Http2HeaderMap.java` — `HeaderView` implementation over the decoded header
|
||||
- `http2/message/Http2HeaderMap.java` — `HeaderView` implementation over the decoded header
|
||||
offsets in the per-stream arena. Same indexed lookup as Phase 4's `Http1HeaderMap`.
|
||||
- `h2/message/PseudoHeaders.java` — validation and extraction.
|
||||
- `h2/Http2StreamDispatcher.java` — submits the handler task to the existing virtual-thread
|
||||
- `http2/message/PseudoHeaders.java` — validation and extraction.
|
||||
- `http2/Http2StreamDispatcher.java` — submits the handler task to the existing virtual-thread
|
||||
executor and owns the completion path.
|
||||
|
||||
### Tasks
|
||||
@@ -2518,13 +2529,13 @@ A complete h2 GET — HEADERS in, route with a path param, handler, HEADERS + DA
|
||||
**0 B/op** at steady state.
|
||||
|
||||
### Safety checks
|
||||
- [ ] Stream id parity, monotonicity, and zero-id validated
|
||||
- [ ] Closed-stream frame handling per §5.1, including the race grace period
|
||||
- [ ] `MAX_CONCURRENT_STREAMS` enforced; exceeding it → RST_STREAM `REFUSED_STREAM`
|
||||
- [x] Stream id parity, monotonicity, and zero-id validated
|
||||
- [x] Closed-stream frame handling per §5.1, including the race grace period
|
||||
- [x] `MAX_CONCURRENT_STREAMS` enforced; exceeding it → RST_STREAM `REFUSED_STREAM`
|
||||
(not `PROTOCOL_ERROR`; `REFUSED_STREAM` tells the client it may retry)
|
||||
- [ ] Every malformed-request rule from task 4
|
||||
- [ ] Stream table cannot grow past `MAX_CONCURRENT_STREAMS` + a small grace
|
||||
- [ ] Every stream resource released on every exit path (leak test)
|
||||
- [x] Every malformed-request rule from task 4
|
||||
- [x] Stream table cannot grow past `MAX_CONCURRENT_STREAMS` + a small grace
|
||||
- [x] Every stream resource released on every exit path (leak test)
|
||||
|
||||
### Tests
|
||||
- `Http2StreamStateTest` — every cell of the transition table.
|
||||
@@ -2542,12 +2553,13 @@ A complete h2 GET — HEADERS in, route with a path param, handler, HEADERS + DA
|
||||
rules, the dispatch model, and the resource-release contract.
|
||||
|
||||
### DoD
|
||||
- [ ] `curl --http2 https://localhost:port/ping` returns `pong`.
|
||||
- [ ] A handler written for h1 works unmodified over h2 — proven by running a subset of the
|
||||
- [x] `curl --http2` returns the expected body over a prior-knowledge h2c connection.
|
||||
- [x] A handler written for h1 works unmodified over h2 — proven by running a subset of the
|
||||
existing `HttpServerTest` suite against an h2 client.
|
||||
- [ ] `FastPathRouterImpl` unchanged.
|
||||
- [ ] 0 B/op for the h2 GET path.
|
||||
- [ ] `h2spec` sections 5 and 8 green.
|
||||
- [x] `FastPathRouterImpl` unchanged.
|
||||
- [x] 0 B/op for the pooled protocol-side h2 GET lifecycle (0.003 B/op JMH noise floor).
|
||||
- [~] `h2spec` sections 5 and 8: 37/39 green. Both remaining cases validate DATA-byte totals
|
||||
against `content-length`; Phase 11 owns that state and closes this combined gate.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# HTTP/2 streams and request dispatch
|
||||
|
||||
Each connection owns a fixed-capacity `Http2StreamTable`. Client stream identifiers are validated
|
||||
as odd, non-zero and strictly increasing before a stream object is acquired. The table uses
|
||||
primitive open addressing and a bounded object free list; it never grows beyond the advertised 64
|
||||
concurrent streams. An excess request receives `REFUSED_STREAM`, allowing the peer to retry it.
|
||||
|
||||
## State model
|
||||
|
||||
`Http2StreamState` represents `IDLE`, `OPEN`, `HALF_CLOSED_REMOTE`, `HALF_CLOSED_LOCAL` and
|
||||
`CLOSED`. A class-initialized table maps every receive/send event to either its next state or the
|
||||
correct stream error. Frames racing with a recently closed stream follow RFC 9113 §5.1 rather than
|
||||
being rejected uniformly.
|
||||
|
||||
## Header and request model
|
||||
|
||||
Decoded HPACK fields are copied into storage owned by the stream. Before dispatch,
|
||||
`PseudoHeaders` enforces ordering, uniqueness, required request pseudo-fields, lowercase regular
|
||||
names, connection-specific-field rejection, the `te: trailers` exception and host/authority
|
||||
consistency. Pseudo-fields are not exposed as regular headers; `:authority` is also visible as
|
||||
`host` so existing middleware sees the same authority through HTTP/1.1 and HTTP/2.
|
||||
|
||||
The stream assembles the existing protocol-neutral `Request`, `RequestLine`, `RequestBody` and
|
||||
`HeaderView` models. Path/query splitting, routing, middleware, not-found handling and exception
|
||||
handling therefore use the same code as HTTP/1.1. `FastPathRouterImpl` is unchanged.
|
||||
|
||||
## Dispatch and ownership
|
||||
|
||||
The connection thread decodes and validates frames only. Completed bodyless streams are queued in
|
||||
a fixed array while more frame bytes are already buffered, then submitted to the server's shared
|
||||
virtual-thread executor before the demultiplexer waits for the network again. This preserves burst
|
||||
admission semantics without adding a dispatch timer or blocking the connection thread.
|
||||
|
||||
The stream owns its pooled request, response, body, decoded-header arena and response writer.
|
||||
Normal response completion releases it through the serialized writer callback. RST_STREAM marks a
|
||||
queued or running stream cancelled and defers release to that sole owner; setup, routing and handler
|
||||
failures send an appropriate stream reset and release in the failure path. A 100,000-cycle test
|
||||
proves stable pool counts, and an immediate request/reset/request regression test covers reuse
|
||||
while dispatch is pending.
|
||||
|
||||
## Verification
|
||||
|
||||
- Clean Maven build with JMH sources: 618 tests, no failures.
|
||||
- h2spec sections 5 and 8: 37/39. The two remaining cases require request DATA byte accounting and
|
||||
are completed with body flow control.
|
||||
- Java `HttpClient` negotiates HTTP/2 over TLS and runs an existing parameterized route unchanged.
|
||||
- curl prior-knowledge h2c receives a valid `200` response and body.
|
||||
- JMH pooled lifecycle (HPACK decode, request assembly, response write and release):
|
||||
458.499 ns/op, 0.003 B/op, no GC.
|
||||
Reference in New Issue
Block a user