feat(core): HTTP/2 Phase 6 — Request/Response model refactor
Pools Request/RequestBody/RequestLine/Response per connection (EX-20..EX-24), following the same reset()/dev-mode-guard idiom Http1HeaderMap already used. HeaderMap splits into HeaderView (interface) + Http1HeaderMap (impl, DEC-22). Response gains byte-level structured headers, PreEncodedHeader, and ResponseSerializer as the single source of truth for a response's header sequence, consumed by Http1ResponseWriter's single-bulk-write rewrite (EX-27). ByteTemplate gets O(1) slot lookup plus a buffer-writing overload (EX-28). Multipart audited: three resource-exhaustion gaps found and fixed — unbounded buffered part size, part count, and per-part header parsing (EX-38..EX-40) — and boundary length confirmed already bounded (EX-41). Re-measuring RequestPipelineBenchmark after the pooling work surfaced one more per-request allocation underneath it (RequestParser building fresh RequestByteViews every call) and, while checking the phase's own DoD text, an unbounded Response.header(...) loop hazard neither had a limit — both fixed (EX-42, EX-43). The h1 zero-alloc contract now holds: parseAndRoute measures 0.008 B/op (JMH noise floor), down from Phase 4's 120.008 B/op (DEC-20, DEC-23). MESSAGE-MODEL.md records the pooling model; README gains an "Object lifetime" section documenting the do-not-retain-past-the-handler contract. 503/503 tests green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0e1bbed42c
commit
d882ea255c
@@ -260,6 +260,46 @@ that got the request this far has already completed, never a forced handshake.
|
||||
`WebSocketSession` mirrors this exactly (`isSecure()`, `sslSession()`) by delegating to the
|
||||
upgrading `Request` — no separate TLS state is tracked for WS.
|
||||
|
||||
## Object lifetime
|
||||
|
||||
`Request` and `Response` are **pooled per connection**, not allocated per request: one instance is
|
||||
created per connection and repositioned (`reset()`) over each new request/response in turn — the
|
||||
same idiom Java NIO buffers use, applied to the whole request/response model
|
||||
(`flash/docs/http2/MESSAGE-MODEL.md` has the full design record). This is what makes a warm h1
|
||||
request/response cycle 0 B/op.
|
||||
|
||||
**Do not retain a `Request` or `Response` past the handler that received it.** A reference kept in
|
||||
a field, a captured closure, a `CompletableFuture` continuation, or a background thread and read
|
||||
*after* the handler returns will observe whatever the *next* request on that connection
|
||||
repositioned the same instance to — not the request you thought you had:
|
||||
|
||||
```java
|
||||
// WRONG — captures `req`, reads it after the handler has returned
|
||||
app.get("/slow", (req, res) -> {
|
||||
CompletableFuture.runAsync(() -> log(req.header("X-Trace-Id"))); // may log the NEXT request's header
|
||||
return "ok";
|
||||
});
|
||||
```
|
||||
|
||||
Copy out whatever you need before returning or handing work off asynchronously — every accessor
|
||||
that returns a `String` (`header`, `param`, `query`, `path`, …) gives you an independent heap copy
|
||||
that's safe to keep as long as you like:
|
||||
|
||||
```java
|
||||
app.get("/slow", (req, res) -> {
|
||||
String traceId = req.header("X-Trace-Id"); // copy now, safe to retain
|
||||
CompletableFuture.runAsync(() -> log(traceId));
|
||||
return "ok";
|
||||
});
|
||||
```
|
||||
|
||||
Run with `-Dflash.env=dev` and a use-after-return access throws `IllegalStateException` immediately
|
||||
at the offending call site instead of silently reading the wrong request's data — turn this on in
|
||||
tests and local development. It's a no-op in production beyond a single `boolean` field read.
|
||||
|
||||
`req.body()`/`RequestBody` follows the same rule — materialise (`.bytes()`) or fully consume
|
||||
(`.stream()`) it inside the handler; don't stash the `RequestBody` itself for later.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user