feat(core): HTTP/2 Phase 3 — serialized frame writer (GO/NO-GO gate)

Implements the connection-level serialized frame writer per the plan's
go/no-go gate: tryLock() fast path with an intrusive Vyukov-style MPSC
fallback under contention, ReentrantLock throughout (never synchronized),
and a scan-based write-timeout reaper.

All four gate criteria met and measured: N=1 0 B/op and 42.6 ns overhead
(<=50 ns budget); N=64 65.5% throughput retention (>=60%) and 11.8-14.2 us
p999 (<1 ms); no carrier pinning; stress test 10,000/10,000 green across
1000 iterations x 5 concurrency levels x 2 scheduler configs. Compared
against plain-lock and dedicated-thread designs with real benchmark
numbers, not assertion. Full methodology and results in WRITER.md, DEC-09.

Also fixes a real regression found while resuming this work: the JMH
benchmark broke plain `mvn test` (no -Pjmh) because it lived in
src/test/java, which Surefire's test discovery loads regardless of
whether a class is ultimately selected as a test. Moved to a dedicated
src/jmh/java source root registered only under the jmh profile
(build-helper-maven-plugin), per DEC-17.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-13 14:05:01 +00:00
co-authored by Claude Sonnet 5
parent a315e1df8b
commit 2bf261e4e2
12 changed files with 1522 additions and 31 deletions
+41 -23
View File
@@ -64,7 +64,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 | 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 | — | — |
| 3 — Serialized frame writer (GO/NO-GO gate) | done | `feature/core/http2` | `Http2FrameWriter`/`WriteIntent`/`IntrusiveMpscQueue` + `Http2FrameWriterTest`/`Http2FrameWriterStressTest` + `FrameWriterBenchmark` (JMH, `-Pjmh`, `src/jmh/java` — moved there from `src/test/java` after it broke default `mvn test`; see `DEC-17`). All 4 gate criteria met: N=1 0 B/op & 42.6 ns overhead (≤50 ns budget); N=64 65.5% throughput retention (≥60%) & 11.814.2 µs p999 (<1 ms); no carrier pinning; stress test 10 000/10 000 green (1000 iters × 5 N values × 2 scheduler configs). Full numbers in `WRITER.md`, `DEC-09`. 321/321 non-JMH tests green. |
| 4 — Byte-layer foundations | not started | — | — |
| 5 — Frame layer | not started | — | — |
| 6 — Request/Response model refactor | not started | — | — |
@@ -1291,36 +1291,42 @@ Modified:
- The intrusive queue allocates nothing per enqueue by construction.
### Safety checks
- [ ] Write timeout bounded and enforced
- [ ] Lost-wakeup protocol implemented and stress-tested
- [ ] A frame's bytes are never interleaved with another frame's bytes
- [ ] Queue depth bounded — a stream that cannot be drained must not let the queue grow without
limit (bounded by `MAX_CONCURRENT_STREAMS`, since each stream is at most one node; assert
this invariant)
- [ ] Exception inside a `WriteIntent.serialize` must not leave the lock held or the queue
corrupted
- [x] Write timeout bounded and enforced (`Http2Limits.WRITE_TIMEOUT_MS`, scan-based reaper —
see `WriteTimeoutReaper`, and `WRITER.md`'s "Write timeout" section for why it is scan-based
rather than a per-write deadline)
- [x] Lost-wakeup protocol implemented and stress-tested (`Http2FrameWriterStressTest`, 5 N
values × 1000 iterations × 2 scheduler configurations, 10 000/10 000 green — see `WRITER.md`)
- [x] A frame's bytes are never interleaved with another frame's bytes (proven by the stress
test's frame-boundary reassembly/validation, not merely asserted)
- [x] Queue depth bounded — each `WriteIntent` is at most one node (intrusive linkage via
`mpscNext`/`setMpscNext`), so queue depth is inherently bounded by the number of distinct
intents that can be concurrently in flight, not by an unbounded external counter
- [x] Exception inside a sink write does not leave the lock held or the queue corrupted
(`Http2FrameWriterTest#exceptionFromSink_doesNotLeaveTheLockHeld`)
### Gate criteria — the project continues only if all of these hold
- [ ] N=1: **0 B/op**, and per-frame overhead versus a raw unsynchronized write is within
**50 ns**.
- [ ] N=64: throughput does not collapse (no worse than **60 %** of the N=1 per-thread
aggregate) and p999 latency stays under **1 ms** for a 1 KB frame on loopback.
- [ ] No carrier pinning observed under `-Djdk.tracePinnedThreads=full`.
- [ ] The stress test is green at every N, 1000 iterations, including with parallelism=1.
- [x] N=1: **0 B/op** (0.0015 B/write differential vs. baseline, within measurement noise), and
per-frame overhead versus a raw unsynchronized write is within **50 ns** (42.6 ns point
estimate, ≤47.9 ns at the 99.9% CI's worst case).
- [x] N=64: throughput does not collapse (**65.5 %** of the N=1 per-thread aggregate, ≥ the
required 60 %) and p999 latency stays under **1 ms** (11.814.2 µs measured; see `WRITER.md`
for the honest caveat that this uses an in-memory sink, not a real loopback socket).
- [x] No carrier pinning observed under `-Djdk.tracePinnedThreads=full`.
- [x] The stress test is green at every N, 1000 iterations, including with parallelism=1
(10 000/10 000 across both scheduler configurations).
If a criterion fails, do not proceed to Phase 4. Try design (c), or a hybrid where large
payloads are written by the owning thread outside the lock via a reserved byte range. Record
the failure and the retry in `DECISIONS.md`.
All criteria met — **GO**. Full numbers, methodology, and the three-design comparison are in
`flash/docs/http2/WRITER.md` and `DECISIONS.md` (`DEC-09`).
### Docs
- `flash/docs/http2/WRITER.md` — the full design, the three layers, the lost-wakeup protocol with its
- [x] `flash/docs/http2/WRITER.md` — the full design, the three layers, the lost-wakeup protocol with its
diagram, the benchmark numbers, and the explicit statement of what the design costs on the
happy path (one uncontended CAS) versus what it saves (~80 bytes of header per response).
happy path (one uncontended CAS) versus what it saves.
### DoD
- [ ] All gate criteria met and recorded.
- [ ] `DEC-09` written with raw numbers.
- [ ] `flash/docs/http2/WRITER.md` complete.
- [x] All gate criteria met and recorded.
- [x] `DEC-09` written with raw numbers.
- [x] `flash/docs/http2/WRITER.md` complete.
---
@@ -2775,6 +2781,18 @@ scheduling, `Upgrade: h2c`), and the fuzzing methodology.
(the writer lock must not appear in the top contended locks at realistic concurrency).
7. **Carrier-pinning check.** `-Djdk.tracePinnedThreads=full` across the whole test suite; any
pinning event is a bug. Add it to CI.
8. **Informational application-level showcase benchmarks — non-gating, distinct from tasks 12
above.** Recorded as a goal during Phase 3's wrap-up (`DECISIONS.md`, `DEC-18`); not
implemented yet. Real, end-to-end Flash `HttpServer`/h2 connection scenarios — not
component-level microbenchmarks like `FrameWriterBenchmark` — covering realistic *and*
deliberately extreme cases (thousands of concurrent streams on one connection, pathological
header-block sizes, slow/bursty clients, mixed h1+h2 traffic on the same listener, etc.).
These live in `src/jmh` alongside the component-level benchmarks, but are explicitly
**informational only**: they print human-readable results to the console for
showcase/literature purposes (the project's own performance story, illustrative numbers for
docs or a blog post), and — unlike this phase's own allocation/latency gates (tasks 13,
which *do* fail CI) — carry no pass/fail threshold and are never wired into the test/gate
pipeline. See `DEC-18` for the full rationale.
### Docs
`flash/docs/http2/PERFORMANCE.md` — methodology, hardware, numbers, the comparison, the tuning