Production is meant to run with Netty leak detection off, so refcount
correctness has to come from structure and tests instead.
Structure: the repeated hand-written release patterns move into three
places. ByteBufs.fill/use covers "allocate, write, release on failure"
for every encoder (including LoginStart, Handshake and the encryption
packets, which had no failure handling at all). FrameTransformHandler
owns the borrow/forward/drop/consume protocol for KeepAliveBridge,
PlayerCommandInterceptor and TransferFrameHandler, which no longer call
release(). The cipher handlers extend Netty's MessageToMessageEncoder and
Decoder. A dead active.release() in TabCompletionBridge.close() is gone.
Tests: LeakGate runs every proxy-core test class with PARANOID leak
detection and fails the class if a buffer was leaked; its own test proves
it sees a deliberate leak. Mutation testing (each release() replaced by a
no-op, then the covering tests run) first showed 23 of 47 release sites
unguarded; after the refactor and new tests 32 of 33 remaining sites are
caught and the last was removed by the cipher change. New tests cover
RawRelay (including a real-NIO race where later frames of a read reach the
relay after it closed), TabCompletionBridge, TransferFrameBuffer,
TransferCandidate, the cipher failure paths and the new helpers.
Session.java and ConnectionGate are not part of this commit: a second
mutation pass over them is still running. The full gradle check has not
been re-run on this state yet.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe
The previous commit moved the I/O loops from NioEventLoopGroup to
MultiThreadIoEventLoopGroup. That group's loops queue tasks in a
LinkedBlockingQueue instead of the MPSC queue the deprecated groups use,
and the relay posts one write-completion task per frame. A JFR profile
showed the queue's offer at about 9% of Java CPU samples on burst
traffic. NetworkTransport now builds its loops with MPSC queues, and a
test pins the queue type.
Netty 4.2.2 -> 4.2.18. In the same workload, Buffer.memoryAddress fell
from 22% of Java samples to out of the profile and total Java samples
on the I/O threads dropped from 1460 to 836.
Records for both, plus the negative results (pooled allocator, batched
CFB8 encryption) and the leak-detection trade-off, are in
benchmarks/results/2026-10-05-event-loop-queue-and-netty.md. The earlier
transport record gets a correction note.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe
NetworkTransport picks epoll on Linux, kqueue on macOS and NIO
elsewhere, or when the native library cannot load. The player listener,
session event loops, backend dials and the DNS resolver all take their
channel types from the selected transport, so backend channels keep
sharing the player's event loop. A new `transport: AUTO|NIO|EPOLL|KQUEUE`
setting forces one choice; an unavailable native transport fails
startup instead of falling back silently. The event loops move from the
deprecated NioEventLoopGroup to MultiThreadIoEventLoopGroup.
The distribution ships the linux-x86_64/aarch_64 epoll and
osx-x86_64/aarch_64 kqueue natives. The start scripts and tests pass
--enable-native-access=ALL-UNNAMED.
On the loopback session benchmark, epoll beat NIO on every median and
regressed nowhere. Only window-1 p50 latency separated cleanly (0.060 vs
0.070 ms); see benchmarks/results/2026-10-05-network-transport.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe
Handling successful relay write completions inline, instead of queueing
one event-loop task per frame, showed no effect distinguishable from run
order and noise in two order-reversed A/B runs. The candidate was not
kept; the record and raw output stay for future comparisons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe
Behavior-preserving cleanup of the 1.8k-line Session handler:
- TransferAttempt and PendingTransfer move to their own files.
- CommandRateLimiter holds the proxy command token bucket, with tests.
- SessionChannels owns backend dialing, address parsing, codec install
and frame-decoder lookup that initial routing and transfer duplicated.
- One pauseSource helper replaces the two copies of "buffer both sides,
pause old relay, read until removed".
- Fully qualified names become imports.
Initial routing and transfer now use the same TCP address check, so a
registration with a port above 65535 fails a transfer as
SERVER_UNAVAILABLE instead of as a dial error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe
The PLAY relay called writeAndFlush for every decoded frame. With
TCP_NODELAY, a socket read holding many small packets (a typical server
tick) cost one flush, write syscall and TCP segment per frame. Write each
frame and flush the peer on channelReadComplete. A per-batch event-loop
task flushes frames forwarded outside a socket read, such as delayed tab
completion. Paused-frame replay keeps writeAndFlush.
ProxySessionBenchmark gains --burst plus proxy I/O CPU and TCP segment
counters. On a 4 vCPU Linux container with 32 x 33-byte frames per write,
TCP segments/frame fell from 1.379 to 0.117 and proxy I/O CPU/frame from
4079 to 1827 ns (medians of six paired phases). The existing 1 KiB window
1 and 16 workloads showed no regression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe
RawRelay stops reading while any forwarded write is incomplete. On Linux a
short socket read can stay below the 2 KiB high water mark, so the target
channel stays writable while the relay correctly waits. The test waited for
non-writability and timed out deterministically (writable=true,
pending=1632). Wait for a real pending socket write instead; the bounded
queue and exact byte-stream assertions are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcCN2hYyqmU22X2Xfg1nwe