add credit based mux window

This commit is contained in:
iceBear67
2026-07-15 14:59:32 +00:00
parent ada07e0e36
commit bbe4efbe16
15 changed files with 651 additions and 139 deletions
+35 -16
View File
@@ -154,25 +154,43 @@ connections (9 + 9 + 2), confirming the algorithm.
simplicity and correctness.
* **Client:** goroutine-per-concern. One goroutine reads each connection
(control or worker); `WriteFrame` is mutex-serialized so many stream goroutines
can share a worker connection safely. A per-stream mutex guards the small
"buffer until the destination is connected, then write directly" handoff so
the forwarded handshake never races ahead of later bytes.
can share a worker connection safely. Each stream has two goroutines: `run`
pumps destination → hub, and `writeLoop` is the only writer to the
destination, draining a per-stream queue fed by the worker readLoop. The
readLoop itself never writes to a destination, so a stalled destination can
never block frame dispatch for other streams.
## 6. Back-pressure
## 6. Back-pressure & flow control
There is no per-stream credit window. Flow is governed by TCP back-pressure on
each worker connection:
Two mechanisms operate at different granularities:
* Hub → player: if a player socket's write queue fills, the hub pauses the
worker connection socket and resumes on drain.
* Destination → hub: the client's `WriteFrame` blocks when the worker socket is
congested, which naturally stops the client reading the destination.
* **Per-stream credit windows** (PROTOCOL.md §7.3; the windows are exchanged
at session establishment): each stream direction has an independent byte
budget equal to the receiver's advertised window (default 256 KiB). A sender
that exhausts a
stream's window pauses *only that stream's source* — the hub pauses the one
player socket, the client parks the one destination-reader goroutine. Credit
is granted back (`WND` frames, batched at half-window) as bytes are actually
written to the terminal socket. The result: a slow player or slow destination
jams its own stream at a bounded buffer size and nothing else. This is what
eliminates head-of-line blocking between streams.
* **Aggregate TCP back-pressure** on each worker connection: when the shared
socket itself is congested (total bandwidth, not one stream), the hub parks
all sending players until it drains, and the client's `WriteFrame` blocks.
This is fair — when the pipe is genuinely full, everyone should slow down.
The consequence is head-of-line blocking *within* a worker connection: one very
slow player can stall other streams sharing that connection. `maxConn` spreads
streams across connections to mitigate this. For interactive Minecraft traffic
(small client→server packets, bursty server→client chunk data) this is a good
trade for a near-zero-overhead mux.
The window also bounds memory: a stream can hold at most one window of
undelivered data per direction (the client's pre-connect handshake buffer is
covered by the same bound).
Per-stream flow control is mandatory: the hub rejects a session whose Rekey
lacks the STREAM_FC flag, and the client rejects a hub that does not echo it —
peers that predate the mechanism cannot connect at all.
What remains (by design) is TCP-level head-of-line blocking: a lost packet on
a worker connection stalls all its streams for one retransmit. That is inherent
to mux-over-TCP; the connection pool is the mitigation, and a datagram
transport (QUIC) would be the escape hatch if it ever matters.
## 7. Failure & recovery
@@ -190,7 +208,8 @@ trade for a near-zero-overhead mux.
## 8. Known limitations
1. No AEAD — payload integrity/authenticity is not cryptographically guaranteed.
2. No per-stream flow control (see §6).
2. TCP-level head-of-line blocking within a worker connection (lost packets;
see §6) — per-stream flow control removes the application-level variant only.
3. Single-event-loop hub (see §5) bounds throughput to one core.
4. `Intent 18` is reserved but only stubbed (the hub logs and closes).
5. Pattern ownership is last-writer-wins; two clients registering the identical