Commit Graph
101 Commits
Author SHA1 Message Date
Zakaria El OrcheandClaude Opus 5 e0ad83eb2f test(core): read bound ports back from the app instead of guessing free ones
Every integration test picked a port by opening ServerSocket(0), closing it and
reusing the number, which races anything else on the machine between the close
and the rebind. FlashApp.port() reports the port the listener actually bound,
so the guess is gone: 18 freePort() helpers deleted, 40 call sites now pass
port(0) and read the result back.

Two ServerSocket(0) uses remain and are correct. ConnectionRunnerTest accepts
on its socket rather than using it to pick a number. H2LoadMeasurementTest
hands its port to an external nghttpd process, which has no equivalent of
port() to read back; that one is now commented to say so.

HttpServerTlsTest's two-listener case reads both back through ports().

HttpServerTest and HttpServerConcurrencyTest also move from @BeforeEach to
@BeforeAll — every test in them is read-only against the same routes, so 11 and
3 boots respectively become 1. HttpServerConcurrencyTest's lazy-compile test
keeps building its own app, since a freshly compiled router is the thing it
tests. HttpServerTest now runs 11 tests in 0.06s.

The http2 interop suites (curl, nghttp, grpcurl, h2spec), the load measurement
and the soak test are skipped without their external binaries or system
property, so those edits are compile-verified here and exercised in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:05:06 +00:00
Zakaria El OrcheandClaude Opus 5 1c207cf94c feat(testing): boot lazily instead of eagerly in beforeEach
beforeEach called ensureStarted(), so every FlashTest field in a class booted
for every test whether or not that test touched it — a class holding four
servers paid for four boots per test. Booting is already lazy on first access,
so the hook was only ever forcing work forward.

Neither hook starts anything now. beforeAll still records that a static field
owns the class-scoped lifecycle, which is what keeps afterEach from tearing a
class-scoped server down after the first test.

This also lets an application read @TempDir inside configure(): JUnit populates
those during instance post-processing, before the first test body but after
extension beforeEach callbacks would have fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:05:06 +00:00
Zakaria El OrcheandClaude Opus 5 fa5a025c93 test(ext-mcp): migrate McpAuthPolicyTest to flash-testing
First migration, chosen because it is the case that drove the harness design:
a Flash app plus a FakeOidcProvider, with tokens audience-bound to the app's
own port, so the port has to be readable after boot.

Drops the racy free-port dance, the hand-rolled HttpClient plumbing and the
per-test teardown; failures now report the response body. 120 to 106 code
lines, and what remains is tool-policy assertions rather than fixture code.

The two boot-rejection tests keep building their app directly — a harness whose
job is to boot an app is the wrong tool for asserting that booting fails — but
port(0) removes freePort() from those too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:40 +00:00
Zakaria El OrcheandClaude Opus 5 424ca31b7a feat(testing): add flash-testing, a JUnit 5 harness for Flash applications
Boots a real app on an OS-assigned port for a test class or a single test and
hands back a client pointed at it:

    @RegisterExtension
    static FlashTest app = FlashTest.of(new BlogApp())
            .mock(UserService.class, new InMemoryUserService());

    app.get("/api/users").expectStatus(200).expectBodyContains("alice");

A field rather than an annotation, because annotation values are compile-time
constants and so could never express a second server wired from the first —
FlashTest.of(new BlogApp(auth.baseUri())). Startup is lazy, so reading
baseUri() boots that server on the spot and declaration order does the wiring,
with no dependence on JUnit's extension ordering. A static field boots once per
class, a non-static field once per test; that is stock JUnit field semantics
rather than an option to configure.

Runs against a real loopback port instead of dispatching in-process. An
in-process dispatcher would be a third copy of the routing/handler/exception
sequence that Http1Connection and Http2StreamDispatcher already duplicate, kept
in sync by hand, and it would let a test pass while the status line,
content-length or HPACK encoding was broken.

mock() installs overrides as the last extension, after everything the
application and its extensions declare, so a fake always wins. Any object is
accepted, so a hand-written fake and a Mockito mock are equally welcome and
this module depends on no mocking library — only flash and junit-jupiter-api.

Teardown cancels the client before stopping the server: HttpClient holds
keep-alive sockets open and ServerLifecycle.stop() spins until the last one
closes, so the default 15s drain would otherwise be paid on every test class.
shutdownNow rather than close(), which blocks until every operation completes
and would hang on a leaked WebSocket.

Lives at the top level, not under flash-extensions/, which holds things you
install() onto an app; this carries junit-jupiter-api at compile scope and
nothing installable should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:40 +00:00
Zakaria El OrcheandClaude Opus 5 4a85a27648 feat(core): expose bound ports, service override, FlashApplication and close hooks
Four small seams, each useful on its own, that together make a Flash app
testable without hand-rolled scaffolding.

- ServerHandle/ServerLifecycle/FlashApp gain port()/ports(). Listeners already
  bind in the FlashApp constructor, so port(0) resolved to a real port that
  nothing could read back; every integration test worked around this by
  opening a ServerSocket(0), closing it and reusing the number, which races
  anything else on the machine.

- FlashContext.override() replaces a binding instead of rejecting it. The
  duplicate-is-an-error rule stays everywhere else; this is the single
  deliberate exception, for swapping a service out in tests. A replacement is
  logged at INFO so misuse in production is visible.

- FlashApplication + FlashApp.apply() name an application independently of the
  port it runs on, so the same one can be booted twice. It takes FlashApp
  rather than FlashRegistrar because ws() and mount() live there. Being a
  functional interface, a lambda and a named class are the same thing.

- FlashContext.onClose() runs cleanup at stop(), children first and then in
  reverse registration order. stop() previously closed sockets and the
  executor and never touched the service graph, so a pooled DataSource was
  only ever released by JVM exit — invisible with one app per process, a leak
  per test class once a suite boots and stops many.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:23 +00:00
Zakaria El OrcheandClaude Sonnet 5 74169e40f4 feat(core): add any() route registration, warn on undocumented OpenAPI routes, log unhandled 500s
Publish Maven packages / publish (push) Failing after 3m35s
FlashRegistrar#any() registers a handler under every HTTP method for
verb-indifferent handlers (e.g. a reverse proxy). OpenApiExtension now warns
when a class-based handler has no @ApiOperation instead of silently omitting
it from the spec. AbstractRouter's default production exception handler now
logs unhandled exceptions server-side instead of only returning a JSON 500.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 11:56:51 +00:00
Zakaria El OrcheandClaude Sonnet 5 3d743b34ce fix(build): resolve fpr-core from Gitea, not the dead maven.relism.dev
Publish Maven packages / publish (push) Successful in 2m3s
maven.relism.dev is down. fpr-core's source (FastPathRouter) now publishes
to Gitea's own Maven registry via its own publish-maven.yml, same pattern
this project already uses for itself — point <repositories> there and bump
the pinned fpr-core version to the first build actually published there
(1.1.0-36216cd). Full reactor build + test suite green against the new
resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:10:50 +00:00
Relism f8e0a1d3fa Merge pull request 'fix(http2): close a streaming response body on every exit path' (#13) from hotfix/http2-response-writer-stream-leak into master
Publish Maven packages / publish (push) Successful in 2m9s
Reviewed-on: #13
2026-08-14 23:02:30 +00:00
Relism 5954cac66c Merge pull request 'fix(websocket): mask outgoing CLOSE frames in client mode' (#12) from hotfix/ws-close-frame-masking into master
Publish Maven packages / publish (push) Canceled after 7s
Reviewed-on: #12
2026-08-14 23:02:12 +00:00
Zakaria El OrcheandClaude Sonnet 5 e5bec59410 fix(http2): close a streaming response body on every exit path
Http2ResponseWriter.streamBody (a streaming response's InputStream, driven
across startFlowControlled/resume as flow-control windows allow) was never
closed anywhere -- not on clean EOF, not on a write failure, not when the
stream is abandoned (RST_STREAM from the peer, connection teardown). Same
bug Http1ResponseWriter had before cf16be0, just never given the same fix: a
handler stream that releases a held resource (a pooled backend connection,
for a reverse proxy) from close() leaks it under any real amount of stream
resets or aborted connections.

- appendData closes streamBody once `end` is reached (clean completion) and
  on any IOException from the read itself, mirroring Http1ResponseWriter's
  relayAndClose/writeChunkedAndClose reasoning.
- New Http2ResponseWriter#abort(), called from Http2Stream#cancel() --
  symmetric with that method's existing http2Body.cancel() for the inbound
  leg, now covering the outbound one too. cancel() is the single hook every
  abandoned-stream path (RST_STREAM handling and connection teardown in
  Http2Connection, plus Http2StreamDispatcher) already goes through, so this
  covers every abort case without adding a new one.

closeStreamBodyQuietly() is idempotent (nulls streamBody after closing), so
the appendData and abort() close paths can't double-close or race.

Four new Http2ResponseWriterTest cases: normal completion in one call,
completion across a resume() (multiple flow-control windows), abort() while
still streaming, and abort() as a no-op on a non-streaming response. 697/697
flash-module tests green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa
2026-08-14 22:51:16 +00:00
Zakaria El OrcheandClaude Sonnet 5 901da954a9 fix(websocket): mask outgoing CLOSE frames in client mode
WebSocketSession.close(int) hand-wrote a raw, always-unmasked 4-byte CLOSE
frame, bypassing writeFrame's maskOutgoing handling that sendText/send/
sendPong already go through correctly. A client-mode session (maskOutgoing
true — WS-client usage, e.g. Pathway's UpstreamWebSocketConnector relaying a
proxied client's close to a backend) therefore sent an RFC-6455-invalid
unmasked frame.

This was latent until this same HTTP/2 branch's readFrame rewrite added the
receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects
the malformed frame with WebSocketProtocolException("client frame must be
masked") before ever exposing it as a CLOSE, silently dropping the close
instead of relaying it — reproduced end to end via Pathway's
ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend.

close(int) now builds its 2-byte payload and calls the same writeFrame path
every other outgoing frame uses, so masking (or not) follows maskOutgoing
automatically. Existing server-mode close_setsClosedAndWritesFrame is
unchanged (byte-for-byte identical output — no mask bit, no key). Added
close_masksWhenActingAsClient (mirrors the existing sendText coverage) and a
round-trip regression, readFrame_acceptsCloseFrameWrittenByAClientSession,
that reproduces the actual bug: a client session's close() output fed
straight into a server session's readFrame().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa
2026-08-14 22:46:59 +00:00
Relism 787ae610d4 Merge pull request 'fix(ext): update stale HeaderMap references to Http1HeaderMap' (#11) from hotfix/header-map-rename into master
Publish Maven packages / publish (push) Successful in 2m36s
Reviewed-on: #11
2026-08-14 18:27:13 +00:00
Zakaria El OrcheandClaude Sonnet 5 b5ff5a4c1f fix(ext): update stale HeaderMap references to Http1HeaderMap
HeaderMap was split into the HeaderView interface and Http1HeaderMap
impl in the HTTP/2 model refactor, but two extension-module tests
(flash-ext-jackson, flash-ext-view-jte) still referenced the old
class name, breaking the CI build on master after the HTTP/2 PR merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCmqb7XGb3JQvCWf589yma
2026-08-14 18:25:42 +00:00
Relism 2078dea54f Merge pull request 'feat(core): HTTP/2 support, correctness fixes, and doc reorganization' (#10) from feature/core/http2 into master
Publish Maven packages / publish (push) Failing after 53s
Reviewed-on: #10
2026-08-14 18:20:30 +00:00
Zakaria El Orche a0dda8e47a refactor(core): remove out-of-scope HTTP/2 client/proxy, reorganize docs, refresh README
HttpProxy and Http2Client (719 LOC) shipped a reverse-proxy adapter and outbound HTTP/2
client from flash core with zero callers anywhere in the server itself — only each
other and their own tests. An HTTP/1.1+2 server framework has no business bundling an
outbound client; that capability belongs in its own flash-extensions/flash-ext-*
module if/when it's needed. Removed, along with the now-dead src/bench load driver
that depended on Http2Client (no replacement client written here — flagged as
follow-up work, not silently dropped).

docs/http2/ had accumulated core, cross-protocol documentation alongside genuine
HTTP/2-protocol internals: HTTP1-HARDENING, TRANSPORT, MESSAGE-MODEL,
TRAILERS-AND-STREAMING and BYTES all describe machinery HTTP/1.1 and HTTP/2 share, not
HTTP/2 specifically. Moved to a new docs/core/, leaving docs/http2/ to the protocol
layers, wire internals and operational docs that are actually HTTP/2-specific.
CLEARTEXT-AND-PROXY.md renamed to CLEARTEXT.md and its now-removed upstream-client
section cut, matching the source removal above.

README.md: removed the "HTTP/2 upstream proxy" section (documented the deleted
HttpProxy/Http2Client), the flash-bench module row and build command (not a module
that exists in this repo), and fixed every doc link to the new docs/core/ paths.
Added the new FlashConfiguration.maxConnections field to the configuration reference.

src/bench/ (a load-test harness distinct from the JMH suite, not wired into any Maven
profile or CI) is committed here for the first time.
2026-08-14 18:13:03 +00:00
Zakaria El Orche cf16be08c0 fix(core): fix HTTP/2 rate-limiter false positives, connection-flood OOM, and a streaming-body leak
Targeted stress testing under this branch's HTTP/2 work surfaced four independent
production bugs, each verified with a before/after load test and a regression test:

- Http2Limits.MAX_STREAMS_CREATED_PER_INTERVAL (400/10s) rejected legitimate
  high-concurrency HTTP/2 clients as if they were CVE-2023-44487 rapid-reset abuse —
  h2load's default pattern alone triggered 40-92% request failure. Raised to 100,000,
  matching MAX_STREAMS_PER_CONNECTION's existing lifetime budget; the RST_STREAM-rate
  counter remains the precise defence against the actual attack signature.

- Flash had no connection-admission control anywhere: AcceptLoop accepted every TCP
  connection unconditionally, so a connection flood (h2load -c 400) ran the JVM out of
  heap and crashed with OutOfMemoryError, killing even unrelated daemon threads.
  TransportLimits.defaultMaxConnections() auto-scales a cap from Runtime.maxMemory();
  ConnectionRunner.accept() enforces it before any per-connection state (TLS handshake
  included) is created. Verified surviving 42x the admission limit under both cleartext
  and TLS load with bounded RSS.

- Http1ResponseWriter never closed a handler's streaming response body on a write
  failure (e.g. the client disconnecting mid-transfer) — only on a clean EOF. A handler
  whose stream releases a held resource (a pooled backend connection, for a reverse
  proxy) from close() leaks it under any real amount of client disconnects. Now closed
  on every exit path, matching InputStream#close()'s own idempotency contract.

- Http2StreamState.transition() called the enum's values() every state transition;
  values() clones a fresh array on every call. Cached once, removing ~10.76% of
  allocations measured live under load.

695 -> 698 tests (three new regression tests), all passing.
2026-08-14 18:12:46 +00:00
Zakaria El Orche 825bdfc942 docs(core): document HTTP/2 operation and architecture 2026-08-13 21:39:37 +00:00
Zakaria El Orche 3679eed74a feat(core): add HTTP/2 performance gates 2026-08-13 21:29:21 +00:00
Zakaria El Orche 6386264a1e test(core): add HTTP/2 compliance suite 2026-08-13 20:51:35 +00:00
Zakaria El Orche f3011ffdf6 feat(core): add WebSocket over HTTP/2 2026-08-13 20:18:05 +00:00
Zakaria El Orche 3c1eb0d0df feat(core): add HTTP/2 cleartext proxy support 2026-08-13 20:00:59 +00:00
Zakaria El Orche 5755ef77fe feat(core): harden HTTP/2 abuse resistance 2026-08-13 19:40:52 +00:00
Zakaria El Orche ee90ac44ff feat(core): add HTTP trailers and push streaming 2026-08-13 19:23:26 +00:00
Zakaria El Orche 8d5340a0b4 feat(core): add HTTP/2 flow-controlled bodies 2026-08-13 19:00:19 +00:00
Zakaria El Orche c96d51f7ea feat(core): add HTTP/2 stream dispatch 2026-08-13 18:33:04 +00:00
Zakaria El Orche 9391f80f76 feat(core): add HTTP/2 response path 2026-08-13 18:04:22 +00:00
Zakaria El Orche cfa192e689 feat(core): add HTTP/2 connection state machine 2026-08-13 17:49:20 +00:00
Zakaria El Orche 95c33e7bf2 feat(core): add HPACK decoder 2026-08-13 17:17:29 +00:00
Zakaria El Orche f47f53c355 feat(core): add HPACK coding primitives 2026-08-13 16:48:47 +00:00
Zakaria El Orche 885c450f6b refactor(core): unify HTTP protocol package boundaries 2026-08-13 16:24:23 +00:00
Zakaria El OrcheandClaude Sonnet 5 d882ea255c 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>
2026-08-13 15:26:08 +00:00
Zakaria El OrcheandClaude Sonnet 5 0e1bbed42c feat(core): HTTP/2 Phase 5 — frame layer
Implements HTTP/2 frame reading, validation, and writing: FrameType (the
10 RFC 9113 types + per-type validation descriptor), FrameFlags (with
the deliberate END_STREAM/ACK bit collision documented), FrameHeader (a
flyweight, never allocated per frame), Http2FrameReader (length-prefixed
reader over BufferedByteSource, mirroring RequestParser's buffer/
compaction discipline), FrameValidator (table-driven, specific RFC error
code per violation -- not a uniform code per type), Padding (RFC 9113
6.1/6.2), and FrameWriteBuffer (beginFrame/endFrame length back-patching
over Phase 4's ByteWriter).

All 10 frame types round-trip correctly; every RFC-mandated rejection
has its own test asserting the specific error code; the reader is
fuzz-tested against 10,000,000 random inputs (~14s). The zero-alloc
contract is measured, not asserted: reading + validating + consuming a
frame is 0.002 B/op, writing one is ~10^-4 B/op -- both indistinguishable
from zero (DEC-21).

Found and fixed EX-37 while writing Http2FrameReaderTest: BufferedByteSource's
deadline mechanism (EX-07's actual fix) NPE'd against a null socket, which
every isolated unit test in this codebase uses -- it had zero dedicated
test coverage of its own. Fixed to treat a null socket as "no OS-level
timeout to bound" rather than a misuse, and given BufferedByteSourceTest,
which did not exist before.

449/449 tests green, both with and without -Pjmh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:25:12 +00:00
Zakaria El OrcheandClaude Sonnet 5 704a00a551 feat(core): HTTP/2 Phase 4 — byte-layer foundations
Builds dev.relism.flash.bytes: ByteScan (scanning/comparison/hashing,
scalar + SWAR, property-tested against each other on every boundary and
20,000 random fuzz trials each), ArrayBackedByteView/SegmentedByteView
capability hierarchy, PooledSlice/SlicePool, ByteWriter, Pairs.

Cashes in the allocation and scanning wins the existing code left on the
table: EX-04 (word-at-a-time router matching, verified directly against
fpr-core's own ByteCompare), EX-05 (pooled views replacing per-call
anonymous ByteView allocations in HeaderMap/QueryParams/PathParams),
EX-09 (HeaderMap index built once per reset() instead of rescanning per
lookup), EX-19 (reusable PathParams on the router's per-connection
scratch), EX-25/EX-26 (single-allocation String construction), EX-33
(SWAR header-terminator scan in RequestParser).

Also closes EX-06's router half, missing from this phase's own EX-item
list in the plan (same class of omission DEC-12 recorded for Phase 1):
FastPathRouterImpl/FastPathWsRouterImpl's ThreadLocals (unbounded under
one-virtual-thread-per-connection) are replaced by an opaque,
caller-owned per-connection scratch object (AbstractRouter#newScratch),
not by extending ConnectionScratch as its own Javadoc originally assumed
-- that would have created transport's first dependency on routing in
the reverse direction. Full rationale in DEC-19.

Every optimization is measured, not asserted (DEC-20): SWAR scan 35.4%
faster than scalar, kept; EX-04's word-path 32.1% faster than
byte-at-a-time at the mechanism level, kept for its real future
consumers even though today's router doesn't yet route through it
(MethodPathByteView stays deliberately non-array-backed, per the plan's
own text). Router matching itself is ~0 B/op including parametric
routes. The full h1 pipeline is not literally 0 B/op yet -- 120 B/op is
Request/RequestBody/RequestLine construction, honestly attributed to
Phase 6's explicit scope rather than hidden.

395/395 tests green, both with and without -Pjmh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:07:29 +00:00
Zakaria El OrcheandClaude Sonnet 5 2bf261e4e2 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>
2026-08-13 14:05:01 +00:00
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
Zakaria El OrcheandClaude Sonnet 5 5a2aaf5a07 feat(core): HTTP/2 Phase 1 — HTTP/1.1 hardening and protocol negotiation
Fixes the request-smuggling and resource-exhaustion debt in the existing
HTTP/1.1 parser, and adds the ALPN/h2c-preface negotiation seam so a
connection's protocol is decided once, before any request is parsed, per
flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 1.

Existing-code defects fixed (EX-nn):
- EX-02: reject Content-Length + Transfer-Encoding together (RFC 9112 6.1
  CL.TE/TE.CL smuggling), and conflicting duplicate Content-Length values.
- EX-03: strict, overflow-safe Content-Length parsing, replacing a parser
  that silently skipped non-digit bytes ("5abc" -> 5, "-1" -> 1).
- EX-07: header-read / idle-keep-alive / body-read timeouts enforced by an
  absolute deadline (dev.relism.flash.transport.BufferedByteSource), not
  merely Socket#setSoTimeout, which never trips against a peer trickling
  one byte per read within the window.
- EX-08: header count / name length / value length / request-line length
  bounds (Http1Limits), 431 on violation.
- EX-10: ChunkedInputStream now reads through BufferedByteSource instead
  of the raw unbuffered socket stream, and the header-parser's read-ahead
  bytes are handed over via a zero-copy prependOnce() instead of a
  SequenceInputStream/ByteArrayInputStream pair.
- EX-17: HttpStatus's status-code bound is computed from values() instead
  of a hand-maintained constant that silently threw
  ArrayIndexOutOfBoundsException when a code above it was added; added
  421, 431, 505, 507, 511 and others HTTP/2 and this hardening need.
- EX-18: bare-CR desync and obsolete line folding rejected.
- EX-30: the TLS handshake is forced explicitly, under a timeout, before
  any protocol decision -- SSLSocket#getApplicationProtocol() returned
  null until the handshake had run, and nothing previously forced it.
- EX-31: TLS 1.2 cipher suites on the RFC 9113 Appendix A blocklist are
  filtered out of a listener's enabled set whenever it offers h2 via ALPN.
- EX-35 (found in this phase): Transfer-Encoding values listing multiple
  codings ("gzip, chunked") were silently treated as not chunked at all,
  corrupting the message boundary -- only the whole value was compared.
- EX-36 (found in this phase): a header line with no ':' was silently
  skipped instead of rejected.

New:
- dev.relism.flash.transport.BufferedByteSource: the single buffered,
  deadline-aware, peekable view over a connection's inbound bytes.
- dev.relism.flash.transport.ProtocolNegotiator/NegotiatedProtocol: ALPN
  and h2c prior-knowledge detection. In this phase an H2 result is always
  closed cleanly -- there is no Http2Connection to hand off to until
  Phase 8. FlashConfiguration.http2Enabled gates the h2c preface peek.
- dev.relism.flash.exceptions.MalformedRequestException: a typed,
  status-carrying rejection distinct from HttpException, caught at the
  parse site so a malformed request never reaches the handler chain or
  the user's exception handler, and the connection is always closed.

Two small plan-document corrections recorded as DEC-12 (Phase 1's Files
list omitted BufferedByteSource.java and MalformedRequestException.java;
the request-line-length check description pointed at the wrong offset).
DEC-13/DEC-14 record the deadline and exception-hierarchy designs.

277/277 tests green (flash module), run twice for stability of the new
wall-clock-based HttpServerTimeoutTest cases. Whole-repo build green.
h1 benchmark regression check is left unverified in the plan's DoD: no
JMH harness exists yet (Phase 3 deliverable).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:40:03 +00:00
Zakaria El OrcheandClaude Sonnet 5 db6e4a4d0c feat(core): HTTP/2 Phase 0 — groundwork (limits, error model, decision log)
Establishes the package layout, limits/error model and decision-log
convention that every later HTTP/2 phase depends on, per
flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 0.

- dev.relism.flash.h2: package-info (architecture overview), Http2ErrorCode
  (the 14 RFC 9113 §7 codes with precomputed 4-byte wire encodings),
  Http2Exception (connection error -> GOAWAY) and Http2StreamException
  (stream error -> RST_STREAM), neither extending IOException, both with
  stack-trace capture disabled on the hot rejection path.
- Http2Limits: every bound Phase 0 requires (concurrent streams, frame
  size, header list size, CONTINUATION/reset/settings/ping rate bounds,
  flow-control windows, HPACK table size/string length, assembly and idle
  timeouts), each documented with the attack or RFC clause it addresses.
- dev.relism.flash.http.Http1Limits: the h1 bounds needed by EX-03 (strict
  Content-Length) and EX-08 (header count/size limits).
- flash/docs/http2/DECISIONS.md seeded with DEC-01..DEC-11 (the ten
  decisions implied by the plan itself, plus DEC-11 recording that commits
  keep scope `core` rather than adding `h2` to AGENTS.md).
- flash/docs/http2/IMPLEMENTATION-PLAN.md: added the Progress Ledger
  (tracks phase status across sessions) and checked off Phase 0's DoD.

19 new tests, full flash module suite green (226/226).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:59:49 +00:00
Relism 8f1f30b973 Merge pull request 'fix(data): fire transaction synchronizations, and scope them to their transaction' (#9) from fix/tx-synchronizations into master
Publish Maven packages / publish (push) Successful in 1m53s
Reviewed-on: #9
2026-08-12 23:37:36 +00:00
Zakaria El OrcheandClaude Opus 5 c5179146c0 docs(data): write the data-layer docs in English
The three data modules' docs were in Italian, so the synchronization contract
added in the previous commit went in as Italian too, to match its file. English
is the project's language for docs, comments and READMEs alike, and a file half
in each is worse than either — so all three are translated, not just the new
section.

Content is otherwise unchanged, except the "synchronizations run on
commit/rollback" line in the two backend READMEs, which was vague before and is
now accurate about which hook sees the session/connection still bound, pointing
at flash-ext-data-core's README for the full contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:36:11 +00:00
Zakaria El OrcheandClaude Opus 5 0194470c1f feat(data): actually invoke TxSynchronization.beforeCommit, and document the contract
beforeCommit(boolean) has been part of the TxSynchronization API from the
start and was invoked by nothing, in either manager — anyone implementing it
got silence, the same failure class as the dropped afterCommit callbacks in
the previous commit. Left out of that one because fixing it is a design
decision rather than a restored behaviour; this is that decision, written
down.

It now runs immediately before the real commit, with the transaction still
active and its session/connection still bound, which is the whole reason to
have a hook on this side of the commit: it can still write through the same
resource and land in the same atomic unit. Skipped when the transaction is
already rollback-only, since there is no commit to precede.

Throwing from it vetoes the commit: the transaction rolls back, the surviving
callbacks hear ROLLED_BACK, and the exception propagates. Without that, a hook
running before the commit would be strictly less useful than one running
after. A commit that fails on its own now takes the same path instead of
completing silently with no callback at all, and a rollback that also fails is
attached as a suppressed exception rather than replacing the one that explains
the failure.

Documented on the interface itself and in flash-ext-data-core/docs/README.md:
which hook sits on which side of the commit, what each may still touch, what
throwing does, and the per-transaction scoping rule from the previous commit.

Tests: 6 more (3 per manager) — runs inside the transaction with the resource
still bound, receives the read-only flag, skipped on a rollback-only
transaction, and vetoes the commit when it throws. 37 across the two managers
now, all green, reactor verify passes the 80% Jacoco gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:33:05 +00:00
Zakaria El OrcheandClaude Opus 5 7299490d0d fix(data): fire transaction synchronizations, and scope them to their transaction
Two defects in the same mechanism, both silent.

1. HibernateTxManager#commit fired synchronizations *after* its finally block,
   where cleanupIfIdle() had already called ResourceRegistry.cleanup() and
   removed the ThreadLocal list holding them. fireSynchronizations() then read
   a freshly initialized empty list and did nothing. No afterCommit callback
   had ever run on the Hibernate path: no exception, no log, just silence.
   rollback() twenty lines below had the order right, which is what makes this
   an ordering slip rather than a design choice.

   Found in production, from the far end: an admin write landed in Postgres
   while the in-memory cache it was registered to refresh never heard about it,
   so the change only took effect when the process restarted and re-read the
   database at boot.

2. Synchronizations were a single flat per-thread list, fired from index 0 by
   whichever transaction completed first. A REQUIRES_NEW inner transaction
   therefore fired the *suspended* outer transaction's callbacks too — early,
   with the inner transaction's outcome, for a transaction that might still
   roll back. Each new transaction now records how many synchronizations were
   already registered when it began, and fires only its own tail.

Both managers get the fix and the same callback ordering: unbind the session or
connection first, so a callback that opens its own transaction (a cache reload,
an outbox drain) gets a fresh one instead of joining the transaction that just
committed, then fire, then clean up.

Also fixes JdbcTxStatus rejecting a null connection, which turned the two
propagations that deliberately produce a connectionless status — SUPPORTS with
no active transaction, and NOT_SUPPORTED — into an NPE inside begin(). The
Hibernate manager always allowed it, and resource() already reports the real
mistake with a message that names it.

Tests: 16 new across the two managers, kept deliberately parallel since the two
are interchangeable behind TxManager — synchronization firing, ordering,
per-transaction scoping, callbacks opening their own transaction, and the
previously untested SUPPORTS/NOT_SUPPORTED/MANDATORY propagations. Nothing
covered afterCommit before, which is how both defects shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:23:47 +00:00
Zakaria El OrcheandClaude Sonnet 5 88ac3c3d1f fix(build): don't skip deploying flash-extensions' own POM
Publish Maven packages / publish (push) Successful in 2m2s
Its maven-deploy-plugin was configured with <skip>true</skip>, so the
first successful publish-maven.yml run deployed flash-parent, flash,
and every flash-ext-* jar to Gitea's Maven registry, but not
flash-extensions itself — the pom-packaging aggregator every
flash-ext-* submodule's effective POM inherits from via <parent>.
Remote consumers (Pathway's Docker build, resolving flash-ext-* from
the registry instead of a local ~/.m2 install) couldn't resolve that
parent POM and failed with "artifacts could not be resolved:
flash-extensions:pom (absent)".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 20:30:12 +00:00
Zakaria El OrcheandClaude Sonnet 5 3f0b49fa36 ci: use a real PAT (PACKAGES_TOKEN) instead of GITEA_TOKEN for deploy
Publish Maven packages / publish (push) Successful in 1m55s
Root cause of the persistent 401s found: Gitea's own per-job GITEA_TOKEN
cannot publish to any package registry at all — a known, still-
unimplemented limitation (go-gitea/gitea#23642), not a settings.xml
auth-format issue as first assumed. Confirmed by testing: GITEA_TOKEN
authenticated fine against the plain API but every registry write
endpoint rejected it regardless of scope or header style.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 20:23:18 +00:00
Zakaria El OrcheandClaude Sonnet 5 a19c59770d ci: add a debug step for the persistent 401 on deploy
Publish Maven packages / publish (push) Failing after 28s
Basic auth didn't fix it either — same 401 as the httpHeaders form.
Before guessing again: confirm GITEA_TOKEN actually reaches this step
non-empty, and check whether it authenticates against the plain API at
all (both header styles), independent of Maven/wagon-http.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:54:47 +00:00
Zakaria El OrcheandClaude Sonnet 5 3bc5952dc7 ci: switch Maven auth to basic (username/password), not httpHeaders
Publish Maven packages / publish (push) Failing after 27s
Still 401 after granting packages:write — the httpHeaders form from
Gitea's docs is honored by Maven's resolver (dependency reads) but
apparently not reliably by the wagon-http provider maven-deploy-plugin
uploads through, which deployed unauthenticated. Basic auth is
wagon-http's oldest, always-supported path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:53:12 +00:00
Zakaria El OrcheandClaude Sonnet 5 9d19de50cb ci: request packages:write — repo's default Actions token is read-only
Publish Maven packages / publish (push) Failing after 26s
Deploy failed with 401 Unauthorized: this repo's default Actions token
permission mode is Restricted (read-only on packages, not Permissive),
so the auto-provided GITEA_TOKEN couldn't push to the Maven registry
without explicitly requesting write access.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:50:58 +00:00
Zakaria El OrcheandClaude Sonnet 5 4710cf8633 ci: replace actions/checkout with a plain git clone
Publish Maven packages / publish (push) Failing after 27s
actions/checkout@v4 is a Node-based action; the maven:3.9-eclipse-temurin-21
container it was running in has no Node, so it failed with "node: executable
file not found in \$PATH". A shell git clone needs neither Node nor any
action runtime, just git (installed here via apt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:47:40 +00:00
Zakaria El OrcheandClaude Sonnet 5 6a0242d654 ci: use the global ubuntu-latest runner, not the org-scoped one
Publish Maven packages / publish (push) Failing after 59s
The "org" runner label is scoped to the Pixel-Services organization —
Flash5 lives under the separate Relism account, which can only reach
the global runner (labels ubuntu-latest/ubuntu-24.04/ubuntu-22.04).
This job just runs Maven inside a container, no Docker access needed,
so ubuntu-latest is a fine fit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:44:24 +00:00
Zakaria El OrcheandClaude Sonnet 5 0365c1f222 ci: fix runner label (org, not docker)
Publish Maven packages / publish (push) Canceled after 0s
The org's Gitea Actions runner is registered under the label "org" —
"docker" doesn't match anything online, so the publish job never got
picked up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:41:26 +00:00
Relism c235e67158 Merge pull request 'ci: publish Maven packages to Gitea registry on push to master' (#8) from ci/publish-maven into master
Publish Maven packages / publish (push) Canceled after 0s
2026-08-12 19:39:12 +00:00