[4/4] Expose the transport round trip time - #11
Open
SendableMetatype wants to merge 11 commits into
Open
Conversation
SendableMetatype
marked this pull request as ready for review
August 2, 2026 18:35
reconnect(freshToken) replaces only the socket to the signaling service. The signaling instance, its handlers, and everything built on it (server channel, WebRTC factories, live peer connections) survive, so a signaling drop no longer requires tearing the transport down. Liveness is now detectable on idle servers: a WebSocket protocol ping every 15 seconds guarantees inbound pongs on a healthy socket, so isChannelAlive(maxSilence) also catches silently half open TCP. Per channel scheduled tasks are tracked and cancelled on channel inactive, so reconnects no longer leak ping loops. TURN credential pushes are applied for the lifetime of the socket instead of only during connect, and the JSON RPC endpoint refreshes credentials every 30 minutes, so late joining peers no longer receive expired relay credentials. Pending RPC requests fail fast when the socket dies. The frame aggregator limit is raised to 128 KB for batched RPC frames.
Each in flight JSON RPC request now records the WebSocket channel it was written to. When a socket dies, onChannelInactive fails only the requests that were sent on that socket: during a reconnect the old channel's inactive event can no longer fail requests already written to the replacement socket, which previously left those callers with a spurious ClosedChannelException while the reply was still on its way.
The object or array check introduced with the params support tested the message envelope, which is always a JSON object, so the array branch could never run: array form params fell into the object branch, where getAsJsonObject throws on an array and the delivery was lost. The check now tests the params element itself. Batched frames are where array form params appear, so those deliveries were being dropped.
The server data plane no longer reaches into libwebrtc. A WebRtcServerBackend interface owns offer negotiation and data channel transfer, with LibWebRtcServerBackend as the single class touching engine types: factory pooling, port allocator defaults, session tracking, and a lifecycle lock that makes close idempotent and safe against in flight accepts, closing every live session before disposing the factories they run on. Child channels attach to backend sessions and the server channel bridges session events into netty. Connection setup runs off the signaling thread so slow negotiation never stalls the socket carrying every other player's signals. Sends go through the engine's async path with watermark backpressure: the channel pauses writes at 2MB of engine backlog, resumes at 512KB, and closes deterministically once netty's pending writes exceed 8MB. Remote candidates buffer until the remote description is applied. Child channels expose the peer's real transport address from the selected ICE candidate pair instead of a placeholder. NetherNet's countdown framing moves out of the channels into NetherNetFramingCodec, a standalone duplex handler placed first in the pipeline. Channels now move raw fragments; the codec fragments to the negotiated maximum message size, honoring the peer's advertised a=max-message-size, and reassembles with a 16MB cap on both the accumulation and completion paths. Covered by unit tests. The client channel keeps direct engine access but gains the same hardening: handshake retries are attempt scoped so stale engine callbacks cannot mutate a replacement attempt, signal ids are re validated on the event loop, and remote candidates buffer until the answer is applied. The server handshake reaper is cancelled by the child's close future rather than from engine observer callbacks, so it fires only for a connection that is still open but never activated.
Session.send checked the closed flag and then called into the engine unguarded, so a session closed between the two could throw into the caller's write path despite the documented drop contract; the native call now honors the contract under the race too.
accept registered the session before creating its peer connection, so an exception from engine setup retained the tracked session until backend shutdown. Setup is now guarded and a failed session is closed on the way out, which also unregisters it. Session teardown closed both data channels and the peer connection in a single try block, so one throwing close skipped the resources behind it. Each resource now closes under its own guard.
NetherNetHttpSignaling serves NetherNet's direct connection model on
TCP under the Bedrock port: GET /v1/join as the capability check and
POST /v1/join/{networkId} exchanging the SDP offer for a full ICE
answer in one round trip, per Mojang's onboarding guide and matching
the go-nethernet reference (one shot connections, 1 MiB offer cap,
uint64 network id validation, 502 after the negotiation timeout with
the half negotiated child reaped). The listener owns a single thread
accept group because bind() runs on the server channel's event loop.
The backend gains a full ICE accept mode: no trickle candidates in
either direction, the answer reported once after the local description
applies and gathering completes, re read from the engine so it carries
every candidate, and terminated with a=end-of-candidates.
Server signaling implementations can now report the peer's transport
address (the HTTP front end knows it from the request) used as the
child's initial remote address, and request full ICE answers. A new
NETHER_SERVER_ANSWER_DECORATOR channel option lets consumers transform
answers before signaling, the seam for the server identity assertion
that HTTP signaled clients require; TLS is a Supplier<SslContext>
consulted per connection so certificate rotation needs no rebind.Mojang's onboarding guide instructs removing a=identity before the SDP reaches setRemoteDescription: WebRTC implementations may reject unknown attributes, and the assertion is signaling layer metadata (validated above the backend, not in it). Current client offers always carry one. The engine tolerated it in live testing, but stripping removes the dependence on that tolerance. The unstripped offer stays available above the backend for validation.
A decorator failure previously fell back to the undecorated answer, which a peer requiring the decoration only refuses after parsing and negotiation. The exchange now fails explicitly instead (a 400 on HTTP), matching the reasoning that gates the whole listener on the decorator being available: an unasserted answer is never useful, and an explicit error is the fastest fallback signal.
…ative state NetherNetServerChannel gains a backend supplier constructor: the backend (and with it the PeerConnectionFactory pool) is created in doBind only after the signaling endpoint bound successfully. Previously consumers had to create the pool before channel construction, so a signaling endpoint that could not bind, such as a taken TCP port on shared hosting, meant disposing freshly created factories milliseconds after creation. That teardown races engine initialization inside libwebrtc and aborts the JVM with a pure virtual call. Offers racing a failed bind are dropped safely, doClose tolerates a backend that never materialized, and NetherNetChannelFactory gains a supplier overload building the LibWebRtcServerBackend on demand. The eager constructors are unchanged.
Channels sample the selected ICE candidate pair's currentRoundTripTime every 3 seconds while active, through a new WebRtcSession.requestRtt seam (default: no measurement) implemented with peer connection stats. The value is published as NetherNetChannel#rttMillis for consumers like Geyser's ping. It is measured by the transport's STUN checks below any client processing, matching what RakNet's connected ping reports. The sampler starts before channelActive fires (so a synchronous close from a handler still cancels it) and on the already active registration path, and is cancelled on close. Failed samples keep the last good value; negative means no measurement yet. Sampling logic shared between the server backend and the client channel lives in the WebRtcRtt helper.
SendableMetatypeforce-pushed
the
nethernet-rtt
branch
from
August 5, 2026 10:08
d312c4f to
69c5c07Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #10; the content only depends on #9. This branch contains the 10 commits of the PRs below it (#7, #9, #10); review the 1 commit after 598f1b9. As lower PRs merge I will rebase, so the diff collapses to this layer only.
Channels sample the selected ICE candidate pair's currentRoundTripTime every 3 seconds while active, through a WebRtcSession.requestRtt seam implemented with peer connection stats, published as NetherNetChannel#rttMillis. It measures the transport's STUN checks below any client processing, matching what RakNet's connected ping reports, so consumers get Java parity ping semantics. Failed samples keep the last good value; negative means no measurement yet.
In production through Geyser for several weeks: reported values match wire RTT within a few milliseconds.