Files
Flash5/flash/docs/http2/TRANSPORT.md
T
Zakaria El OrcheandClaude Sonnet 5 a315e1df8b 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>
2026-08-13 12:03:44 +00:00

8.8 KiB

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 ThreadLocals 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 ThreadLocals (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)

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.