add credit based mux window
This commit is contained in:
+35
-16
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user