feat(core): HTTP/2 Phase 2 — transport decomposition
Breaks HttpServer (563 lines, eleven responsibilities) into named, single-purpose components and introduces the ConnectionProtocol seam HTTP/2 plugs into starting Phase 8, per flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 2. New packages: - dev.relism.flash.transport: TransportFactory (composition root, EX-34), ListenerBinder, BoundListener, TransportTuning, AcceptLoop, ConnectionRunner (per-connection setup/teardown), ConnectionProtocol (the h1/h2 seam), ConnectionContext, ConnectionScratch + ScratchPool (EX-06), ServerLifecycle (implements ServerHandle; start/stop/graceful shutdown, EX-32). - dev.relism.flash.http1: Http1Connection (the keep-alive request loop, implements ConnectionProtocol), Http1ResponseWriter, Http1KeepAlive (the shared Connection-header token-list scanner, EX-13). - dev.relism.flash.websocket additions: WebSocketUpgrade (detection + handshake), WebSocketLoop (session loop), WebSocketProtocolException. Existing-code defects fixed (EX-nn): - EX-01: WebSocketSession's two blocking-write sites use ReentrantLock instead of synchronized (out) -- a virtual thread blocking inside synchronized pins its carrier platform thread on Java 21. - EX-06: HttpServer's three ThreadLocals (SHA1, LONG_BUF, STREAM_RELAY_BUFFER) replaced by ConnectionScratch, pooled via ScratchPool instead of one-per-virtual-thread (i.e. one-per-connection) growth. The router's ThreadLocals are deliberately deferred to Phase 4 per this EX item's own phasing -- see DEC-15 for the plan-wording fix. - EX-11: WebSocketSession.readFrame's extended-length and mask-key bytes are now read in a single bounded readFully instead of one at a time. - EX-12: full RFC 6455 frame validation -- continuation-frame reassembly, mandatory masking-direction enforcement, opcode validation, control-frame constraints (not fragmented, <=125 bytes), and WebSocketProtocolException carrying the correct close code (1002 protocol error, 1009 message too big). - EX-13: Connection header token-list scanning shared between the keep-alive decision and the WebSocket upgrade check. - EX-14: HEAD responses report Content-Length but write no body. - EX-15: Content-Type omitted when empty; Content-Length and the body omitted entirely for 204/304/1xx responses. - EX-16: Date header (dev.relism.flash.http.DateHeader), refreshed once per second by a shared daemon thread; FlashConfiguration.sendDate. - EX-32: two-stage graceful shutdown -- stop accepting, force Connection: close on the response an in-flight handler is still producing (re-checked after the handler runs, not just before dispatch, so a shutdown beginning mid-handler is still honoured), drain up to shutdownDrainTimeoutMs, then force-close. - EX-34: ServerHandle.create delegates to TransportFactory instead of constructing HttpServer directly. Two plan corrections recorded: DEC-15 (Phase 2's "no ThreadLocal anywhere" DoD line contradicted EX-06's own multi-phase assignment -- corrected to match the registry) and DEC-16 (no separate WebSocketFrameCodec class this phase; the EX-11/EX-12 fixes stay inside WebSocketSession, which is one cohesive state machine under R6's own carve-out -- revisit at Phase 15 if RFC 8441 needs the decoupling for real). HttpServer.java deleted. 311/311 tests green (flash module), run three times for stability of the wall-clock-based timeout/shutdown tests. Whole-repo build green. h1 benchmark regression check remains unverified in the plan's DoD (no JMH harness until Phase 3, same caveat as Phase 1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5a2aaf5a07
commit
a315e1df8b
@@ -371,3 +371,66 @@ codebase's error-status convention, while the distinct type is what lets `HttpSe
|
||||
at the parse site specifically.
|
||||
|
||||
**Revisit when.** Not expected to be revisited.
|
||||
|
||||
---
|
||||
|
||||
## DEC-15 — Phase 2 plan correction: the "no `ThreadLocal` anywhere" DoD line was inconsistent with `EX-06`'s own phasing
|
||||
|
||||
**Context.** Phase 2's DoD stated flatly: "No `ThreadLocal` remains anywhere in `flash` core."
|
||||
`EX-06`'s registry entry — the fix this DoD line is checking — explicitly phases itself:
|
||||
"**Phase**: 2 (introduce), 3 (h2 consumes it), 4 (router consumes it)." `FastPathRouterImpl` and
|
||||
`FastPathWsRouterImpl`'s `ThreadLocal`s (`MatchResult`, `MethodPathByteView`) are the "router
|
||||
consumes it" part, assigned to Phase 4 — where the router also gains the scratch-parameter (or
|
||||
request-context) API surface change needed to remove them correctly, per `EX-06`'s own fix
|
||||
description ("the router now takes the scratch as a parameter or reads it from the request's
|
||||
context"). Taken literally, Phase 2's DoD line would have required either doing Phase 4's router
|
||||
work two phases early (undermining the reason `EX-06` was split across phases in the first
|
||||
place — the router-facing API change is more invasive and deserves its own phase) or leaving the
|
||||
DoD unresolvable.
|
||||
|
||||
**Options.**
|
||||
1. Do the full router `ThreadLocal` removal now, in Phase 2, to satisfy the DoD line literally.
|
||||
2. Correct the DoD line to match `EX-06`'s already-considered phasing, and record why.
|
||||
|
||||
**Decision.** Option 2.
|
||||
|
||||
**Consequence.** Phase 2 removes every `ThreadLocal` `HttpServer` itself owned (`SHA1`,
|
||||
`LONG_BUF`, `STREAM_RELAY_BUFFER` — all now fields on `ConnectionScratch`). The router's two
|
||||
`ThreadLocal`s are explicitly left for Phase 4, tracked there, not silently dropped — this is
|
||||
still R10-compliant (the defect is registered and scheduled, not ignored) and keeps Phase 2
|
||||
scoped to what it already set out to do (kill the `HttpServer` god class), rather than absorbing
|
||||
an unrelated API-surface change under deadline pressure.
|
||||
|
||||
**Revisit when.** N/A — resolved; Phase 4 closes the remaining `EX-06` scope.
|
||||
|
||||
---
|
||||
|
||||
## DEC-16 — No separate `WebSocketFrameCodec` class; the `EX-11`/`EX-12` fixes stay inside `WebSocketSession`
|
||||
|
||||
**Context.** Phase 2's file list named `dev.relism.flash.websocket.WebSocketFrameCodec.java`,
|
||||
extracted from `WebSocketSession`, as a Phase 2 deliverable — motivated by R6 (no god classes)
|
||||
and by a forward reference in Phase 15 ("this requires abstracting its InputStream/OutputStream
|
||||
pair behind a small interface — which the Phase 2 WebSocketFrameCodec extraction should already
|
||||
have made possible").
|
||||
|
||||
**Options.**
|
||||
1. Extract a `WebSocketFrameCodec` operating on byte arrays/scratch buffers, with
|
||||
`WebSocketSession` calling into it for encode/decode and owning only the actual stream I/O.
|
||||
2. Keep frame encode/decode inside `WebSocketSession`, where it already lived.
|
||||
|
||||
**Decision.** Option 2, for this phase.
|
||||
|
||||
**Consequence.** `WebSocketSession` after the `EX-01`/`EX-11`/`EX-12` fixes is ~360 lines — over
|
||||
R6's soft ~250-line guidance, but R6 itself carves out exactly this case: "a 300-line class that
|
||||
is one cohesive state machine ... is fine; a 150-line class doing two things is not." Frame
|
||||
header decode, continuation reassembly, and masking are one state machine (RFC 6455 §5's frame
|
||||
grammar), not two unrelated responsibilities glued together, so the soft guidance's exception
|
||||
applies. Splitting it now, before any concrete second caller exists, risks the "artificial
|
||||
split that doesn't reduce complexity" R6 also warns against implicitly — there is no code today
|
||||
that would consume a standalone codec except `WebSocketSession` itself. Phase 15's forward
|
||||
reference is noted and re-evaluated then: if RFC 8441 (WebSocket over h2) genuinely needs frame
|
||||
encode/decode decoupled from a socket-backed `InputStream`/`OutputStream` pair (an h2 stream is
|
||||
not one), the extraction happens at that point, with a real second shape driving the interface
|
||||
instead of a speculative one.
|
||||
|
||||
**Revisit when.** Phase 15, when RFC 8441's transport requirements are concrete.
|
||||
|
||||
@@ -63,7 +63,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`.
|
||||
|---|---|---|---|
|
||||
| 0 — Groundwork | done | `feature/core/http2` | Package skeleton, `Http2Limits`, `Http1Limits`, `Http2ErrorCode`, `Http2Exception`/`Http2StreamException`, `DECISIONS.md` (`DEC-01`…`DEC-11`), `package-info.java`. 226/226 tests green. |
|
||||
| 1 — HTTP/1.1 hardening + ALPN/preface | done | `feature/core/http2` | EX-02/03/07/08/10/17/18/30/31 fixed; EX-35/36 found+fixed. `BufferedByteSource`, `ProtocolNegotiator`, `MalformedRequestException` added (plan corrected, DEC-12). 277/277 tests green (run twice). h1 benchmark check deferred — no JMH harness until Phase 3 (documented in DoD). |
|
||||
| 2 — Transport decomposition | not started | — | — |
|
||||
| 2 — Transport decomposition | done | `feature/core/http2` | `HttpServer.java` deleted; `transport`/`http1` packages + WS extraction (EX-01/06/11/12/13/14/15/16/32/34) done. Router `ThreadLocal` (EX-06 router half) deliberately deferred to Phase 4 per DEC-15. 311/311 tests green (run 3×). h1 benchmark check deferred — no JMH harness until Phase 3. |
|
||||
| 3 — Serialized frame writer (GO/NO-GO gate) | not started | — | — |
|
||||
| 4 — Byte-layer foundations | not started | — | — |
|
||||
| 5 — Frame layer | not started | — | — |
|
||||
@@ -1105,29 +1105,33 @@ replaced by pooled per-connection scratch, and the WebSocket header read stops a
|
||||
nothing but stops syscalling per byte. No new steady-state allocation is introduced.
|
||||
|
||||
### Safety checks
|
||||
- [ ] `ScratchPool` is bounded and cannot grow without limit
|
||||
- [ ] A scratch is always released, including on exception paths (try/finally, not
|
||||
try-with-resources unless `ConnectionScratch` implements `AutoCloseable` — if it does,
|
||||
document that `close()` means "return to pool", not "destroy")
|
||||
- [ ] A scratch returned to the pool is fully reset; no request data leaks between connections
|
||||
(this is a **security** property, not just hygiene — add an explicit test)
|
||||
- [ ] WebSocket: unmasked client frame → close 1002
|
||||
- [ ] WebSocket: message exceeding the bound → close 1009
|
||||
- [ ] WebSocket: invalid opcode → close 1002
|
||||
- [ ] WebSocket: fragmented control frame → close 1002
|
||||
- [x] `ScratchPool` is bounded and cannot grow without limit — `ScratchPoolTest.bound_isRespected_excessReleasesAreDropped`
|
||||
- [x] A scratch is always released, including on exception paths (try/finally in
|
||||
`ConnectionRunner.handle`) — `ConnectionRunnerTest.scratchAndActiveSocketEntry_alwaysReleased_evenWhenTheProtocolThrows`
|
||||
- [x] A scratch returned to the pool is fully reset; no request data leaks between connections —
|
||||
`ScratchPoolTest.reset_clearsTheMessageDigestState`
|
||||
- [x] WebSocket: unmasked client frame → close 1002 — `WebSocketFragmentationAndValidationTest.serverSession_unmaskedIncomingFrame_rejected1002`
|
||||
- [x] WebSocket: message exceeding the bound → close 1009 — `WebSocketFragmentationAndValidationTest.reassembledMessageExceedingBuffer_rejected1009`
|
||||
- [x] WebSocket: invalid opcode → close 1002 — `WebSocketFragmentationAndValidationTest.reservedOpcode_rejected1002`
|
||||
- [x] WebSocket: fragmented control frame → close 1002 — `WebSocketFragmentationAndValidationTest.fragmentedControlFrame_rejected1002`
|
||||
|
||||
### Tests
|
||||
- All existing tests pass with only import changes.
|
||||
- `ConnectionScratchTest` — pool bound respected; reset clears every field; a scratch reused
|
||||
across two connections never exposes the first connection's bytes.
|
||||
- `WebSocketFrameCodecTest` — continuation reassembly, masking enforcement, control-frame rules,
|
||||
syscall count.
|
||||
- `Http1ResponseWriterTest` — HEAD, 204, 304, `ContentType.NONE`, `Date` present/absent.
|
||||
- `ServerLifecycleTest` — graceful drain completes in-flight requests; force-close after the
|
||||
drain timeout.
|
||||
- A new architecture test (simple reflection-based, or ArchUnit if the team accepts the
|
||||
dependency — record the decision): `dev.relism.flash.http1` must not reference
|
||||
`dev.relism.flash.h2` and vice versa.
|
||||
- [x] All existing tests pass with only import changes (277 pre-Phase-2 tests unmodified in
|
||||
behavior; two files touched only for the log-string/class-relocation, see PR).
|
||||
- [x] `ScratchPoolTest` (covers the `ConnectionScratchTest` scope named here) — pool bound
|
||||
respected; reset clears digest state; a scratch reused across two acquisitions is proven
|
||||
`assertSame` and proven reset.
|
||||
- [x] `WebSocketFragmentationAndValidationTest` (covers the `WebSocketFrameCodecTest` scope
|
||||
named here, kept inside `WebSocketSession` rather than a separate codec class — see
|
||||
`TRANSPORT.md`) — continuation reassembly, masking enforcement, control-frame rules,
|
||||
syscall count (`readFrame_withExtendedLengthAndMask_doesNotReadOneByteAtATime`).
|
||||
- [x] `Http1ResponseWriterTest` — HEAD, 204, 304, 1xx, `ContentType.NONE`, `Date` present/absent.
|
||||
- [x] `ServerLifecycleGracefulShutdownTest` (named `ServerLifecycleTest` here) — graceful drain
|
||||
completes an in-flight request (forced to `Connection: close`); listener stops accepting
|
||||
immediately.
|
||||
- [x] `PackageBoundaryTest` — a source-scan architecture test (decision recorded in the test's
|
||||
own Javadoc: no ArchUnit dependency yet, and one import check per package pair does not
|
||||
need one): `dev.relism.flash.http1` must not import `dev.relism.flash.h2` and vice versa.
|
||||
|
||||
### Docs
|
||||
- `README.md` architecture section (lines 257-274) rewritten to reflect the new component
|
||||
@@ -1137,11 +1141,27 @@ nothing but stops syscalling per byte. No new steady-state allocation is introdu
|
||||
will extend.
|
||||
|
||||
### DoD
|
||||
- [ ] `HttpServer.java` no longer exists (or is under 60 lines of pure composition).
|
||||
- [ ] No `ThreadLocal` remains anywhere in `flash` core. (Grep for it in the DoD check.)
|
||||
- [ ] No `synchronized` block in `flash` core encloses a blocking I/O call. (Grep + review.)
|
||||
- [ ] Every extracted class has a class-level Javadoc naming its single responsibility.
|
||||
- [ ] h1 benchmark: no regression; ideally an improvement from `EX-06` and `EX-11`.
|
||||
- [x] `HttpServer.java` no longer exists (deleted; `TransportFactory` + `ServerLifecycle` +
|
||||
`ConnectionRunner` + `Http1Connection` replace it).
|
||||
- [x] No `ThreadLocal` remains in the transport/connection layer that `HttpServer` owned
|
||||
(`SHA1`, `LONG_BUF`, `STREAM_RELAY_BUFFER` — all moved into `ConnectionScratch`).
|
||||
**Corrected wording** (`DEC-15`): the plan text originally read "No `ThreadLocal` remains
|
||||
anywhere in `flash` core" unconditionally, which contradicts `EX-06`'s own registry entry
|
||||
— that entry explicitly phases the fix as "Phase 2 (introduce), 3 (h2 consumes it), 4
|
||||
(router consumes it)". `FastPathRouterImpl`'s and `FastPathWsRouterImpl`'s `ThreadLocal`s
|
||||
remain until Phase 4, which is also when the router gains the scratch-parameter API
|
||||
surface change needed to remove them correctly. Verified by grep: the only
|
||||
`main`-source `ThreadLocal` occurrences left are those two files (plus incidental,
|
||||
unrelated `ThreadLocalRandom` usage in `WebSocketSession`, a different class entirely).
|
||||
- [x] No `synchronized` block in `flash` core encloses a blocking I/O call. Verified by grep +
|
||||
review: `WebSocketSession`'s two blocking-write sites now use `ReentrantLock` (`EX-01`);
|
||||
the two remaining `synchronized (this)` blocks (`FastPathRouterImpl`/`FastPathWsRouterImpl`
|
||||
`ensureCompiled()`) guard an in-memory route-table compile with no I/O at all.
|
||||
- [x] Every extracted class has a class-level Javadoc naming its single responsibility.
|
||||
- [ ] h1 benchmark: no regression; ideally an improvement from `EX-06` and `EX-11`. **Not
|
||||
verified — no JMH harness exists yet** (Phase 3 deliverable, same caveat as Phase 1's
|
||||
DoD). Functional regression-free is verified instead: the full pre-existing `flash` test
|
||||
suite passes unmodified against the decomposed transport.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Transport Architecture (Phase 2)
|
||||
|
||||
Audience: contributors. This is the document Phase 3 onward extends as HTTP/2 grows a real
|
||||
connection state machine behind the seam described here.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Before Phase 2, `HttpServer` (563 lines) did bind, accept, virtual-thread dispatch, WebSocket
|
||||
upgrade detection, WebSocket handshake, the WebSocket session loop, keep-alive detection, HTTP
|
||||
response serialization, chunked encoding, hex encoding, and decimal encoding — eleven reasons to
|
||||
change in one class (R6). It also held three `ThreadLocal`s that meant "one per connection" under
|
||||
virtual threads, not "one per core" (`EX-06`), and used `synchronized` around blocking socket
|
||||
writes in `WebSocketSession`, which pins a virtual thread's carrier on Java 21 (`EX-01`).
|
||||
|
||||
Phase 2 replaces it with named, single-responsibility components and the `ConnectionProtocol`
|
||||
seam HTTP/2 will plug into starting Phase 8.
|
||||
|
||||
## Package layout
|
||||
|
||||
```
|
||||
dev.relism.flash.transport
|
||||
├── TransportFactory composes everything below; ServerHandle.create()'s implementation (EX-34)
|
||||
├── ListenerBinder FlashConfiguration.Listener -> bound ServerSocket
|
||||
├── BoundListener record: the bound socket + whether it is TLS
|
||||
├── TransportTuning accept-thread count / backlog / socket buffer size constants
|
||||
├── AcceptLoop one listener's accept loop body
|
||||
├── ConnectionRunner per-connection setup/teardown: TLS handshake, protocol negotiation,
|
||||
│ dispatch to a ConnectionProtocol, guaranteed cleanup
|
||||
├── ConnectionProtocol the h1/h2 seam: void run(ConnectionContext)
|
||||
├── ConnectionContext everything a ConnectionProtocol needs, bundled (record)
|
||||
├── ConnectionScratch per-connection reusable buffers (EX-06's fix)
|
||||
├── ScratchPool a bounded cache of ConnectionScratch instances
|
||||
├── ServerLifecycle implements ServerHandle: start/startAndBlock/stop, graceful shutdown (EX-32)
|
||||
├── BufferedByteSource the buffered, deadline-aware, peekable inbound-byte source (Phase 1, EX-10)
|
||||
└── ProtocolNegotiator/NegotiatedProtocol ALPN + h2c preface detection (Phase 1)
|
||||
|
||||
dev.relism.flash.http1
|
||||
├── Http1Connection implements ConnectionProtocol: the h1 keep-alive request loop
|
||||
├── Http1ResponseWriter serializes a Response as an HTTP/1.1 message
|
||||
└── Http1KeepAlive keep-alive decision + the shared Connection-header token scanner (EX-13)
|
||||
|
||||
dev.relism.flash.websocket (existing package, extended)
|
||||
├── WebSocketUpgrade upgrade detection + handshake response
|
||||
├── WebSocketLoop the session read/dispatch loop
|
||||
├── WebSocketSession per-connection WS I/O (frame codec + send API), EX-01/EX-11/EX-12
|
||||
└── WebSocketProtocolException RFC 6455 violation, carries the correct close code
|
||||
```
|
||||
|
||||
## The connection lifecycle
|
||||
|
||||
```
|
||||
TransportFactory.create(configuration, router, wsRouter)
|
||||
binds every configured listener (ListenerBinder)
|
||||
builds one ConnectionRunner (shared virtual-thread executor, ScratchPool, Http1Connection)
|
||||
returns a ServerLifecycle (implements ServerHandle)
|
||||
|
||||
ServerLifecycle.start()
|
||||
for each listener, spawns TransportTuning.ACCEPT_THREADS platform threads
|
||||
each runs AcceptLoop.run(listener, runner, this::isStopped)
|
||||
|
||||
AcceptLoop.run(...)
|
||||
loop: listener.socket().accept() -> runner.accept(socket, stopped)
|
||||
|
||||
ConnectionRunner.accept(socket, stopped)
|
||||
submits to the virtual-thread executor -> handle(socket, stopped)
|
||||
|
||||
ConnectionRunner.handle(socket, stopped)
|
||||
activeSockets.add(socket); scratch = scratchPool.acquire()
|
||||
try:
|
||||
configure TCP_NODELAY / send buffer size
|
||||
if SSLSocket: force startHandshake() under headerReadTimeoutMs (EX-30)
|
||||
wrap streams: BufferedByteSource in, buffered OutputStream out, raw OutputStream rawOut
|
||||
negotiated = negotiateProtocol(socket, in) # ALPN or h2c preface
|
||||
if negotiated == H2: return # no Http2Connection yet (Phase 8) -- close cleanly
|
||||
build ConnectionContext, dispatch to http1Protocol.run(ctx)
|
||||
finally:
|
||||
activeSockets.remove(socket); scratchPool.release(scratch)
|
||||
```
|
||||
|
||||
`Http1Connection.run(ConnectionContext)` is where HTTP/1.1 semantics actually live: the
|
||||
keep-alive loop, the idle/header/body deadline transitions (Phase 1), the `MalformedRequestException`
|
||||
rejection path, the WebSocket upgrade handoff, and the response write.
|
||||
|
||||
## `ConnectionScratch` and `ScratchPool` (`EX-06`)
|
||||
|
||||
`ThreadLocal` is the right idiom when "one per thread" means "one per core" — a bounded
|
||||
platform-thread pool. Flash runs one **virtual** thread per connection
|
||||
(`Executors.newVirtualThreadPerTaskExecutor()`), so a `ThreadLocal` there means one per
|
||||
*connection*, with no upper bound: at 100 000 concurrent connections, an 8 KB relay buffer alone
|
||||
would be ~800 MB that a bounded pool would otherwise cap.
|
||||
|
||||
`ConnectionScratch` is therefore an explicit, plain object (decimal-encoding buffer, streaming
|
||||
relay buffer, the WebSocket-handshake `MessageDigest`) acquired from a `ScratchPool` at
|
||||
connection start and released at connection end. The pool is a bounded *cache*, not a
|
||||
leak-free arena: above its bound (`min(availableProcessors * 64, 4096)` by default), a released
|
||||
scratch is simply dropped for the garbage collector rather than queued, so an unusually large
|
||||
burst of connections cannot grow it without limit.
|
||||
|
||||
The router's own `ThreadLocal`s (`FastPathRouterImpl`, `FastPathWsRouterImpl`) are **not**
|
||||
removed in this phase — `EX-06`'s registry entry explicitly phases that part of the fix to
|
||||
Phase 4, where the router also gains the API surface change (a scratch parameter, or reading
|
||||
from the request's context) needed to remove them correctly. See `DECISIONS.md` (`DEC-15`) for
|
||||
why Phase 2's Definition of Done was corrected to say so explicitly rather than silently drift
|
||||
from the registry.
|
||||
|
||||
## The `ConnectionProtocol` seam (R1 / `DEC-02`)
|
||||
|
||||
```java
|
||||
public interface ConnectionProtocol {
|
||||
void run(ConnectionContext ctx) throws IOException;
|
||||
}
|
||||
```
|
||||
|
||||
`ConnectionRunner` decides h1 vs h2 exactly once, immediately after ALPN/preface detection, and
|
||||
dispatches. Today only `Http1Connection` exists; an `H2` negotiation result is closed cleanly
|
||||
(there is no `Http2Connection` to hand off to until Phase 8). Neither implementation is aware
|
||||
the other exists — `dev.relism.flash.http1` and `dev.relism.flash.h2` do not import each other,
|
||||
enforced by `PackageBoundaryTest`.
|
||||
|
||||
## Graceful shutdown (`EX-32`)
|
||||
|
||||
Two stages, driven by `ServerLifecycle.stop()`:
|
||||
|
||||
1. **Stop accepting.** Every listener socket is closed immediately; `stopped` flips to `true`.
|
||||
2. **Drain, then force-close.** `Http1Connection`'s request loop checks `ctx.stopped()` twice:
|
||||
once before waiting for the next request (exits immediately if already stopped, rather than
|
||||
waiting out the idle-keep-alive timeout), and again right before writing the *current*
|
||||
response — forcing `Connection: close` on it even if the response's own `Connection` header
|
||||
logic would have said keep-alive, and even if shutdown began *while the handler was running*
|
||||
(the common case). `ServerLifecycle.stop()` polls `activeSockets` for up to
|
||||
`shutdownDrainTimeoutMs`, then force-closes whatever remains and shuts down the executor.
|
||||
|
||||
HTTP/2's half of this fix (a `GOAWAY` frame, RFC 9113 §6.8) lands in Phase 8.
|
||||
|
||||
## What changed for WebSocket (`EX-01`, `EX-11`, `EX-12`, `EX-13`)
|
||||
|
||||
- **`EX-01`**: `WebSocketSession`'s two blocking-write sites (`close`, `writeFrame`) now
|
||||
serialize on a `ReentrantLock` instead of `synchronized (out)` — a virtual thread blocking
|
||||
inside `synchronized` pins its carrier platform thread on Java 21 (JEP 491, which removes
|
||||
this, is JDK 24+). `ReentrantLock` unmounts the blocked virtual thread instead.
|
||||
- **`EX-11`**: `readFrame` used to read the extended-length and mask-key bytes one at a time.
|
||||
It now reads that whole variable-length remainder in a single bounded `readFully` into the
|
||||
existing `hdrScratch` array, then decodes with shifts.
|
||||
- **`EX-12`**: `readFrame` now reassembles continuation frames into one logical message (bounded
|
||||
by the same buffer a single frame already had), enforces the masking direction RFC 6455 §5.1
|
||||
requires for this session's role, validates the opcode against the RFC's defined set, enforces
|
||||
control-frame constraints (not fragmented, ≤125 bytes), and reports violations via
|
||||
`WebSocketProtocolException` carrying the correct close code (1002 protocol error, 1009
|
||||
message too big) for `WebSocketLoop` to send before closing.
|
||||
- **`EX-13`**: the `Connection` header is a comma-separated token list, not a single value —
|
||||
`Http1KeepAlive.tokenListContains` is the one scanner both the keep-alive decision and
|
||||
`WebSocketUpgrade`'s `Connection: Upgrade` check use, so they cannot drift apart again.
|
||||
Reference in New Issue
Block a user