You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
drpcwire: introduce MuxWriter for stream multiplexing
Replace per-stream drpcwire.Writer with a shared MuxWriter that uses a
dedicated drain goroutine. This decouples stream writes from transport I/O,
enabling true concurrent stream multiplexing.
Core Architecture:
MuxWriter (drpcwire/mux_writer.go):
- Single instance per Manager, shared across all streams
- Dedicated goroutine continuously drains buffered frames to transport
- Non-blocking WriteFrame: appends to buffer under lock, signals goroutine
- Double-buffer swap (buf/spare) minimizes time spent under lock
Stream changes (drpcstream/stream.go):
- wr field: *drpcwire.Writer -> *drpcwire.MuxWriter
- Removed: ManualFlush option, RawFlush, rawFlushLocked, checkRecvFlush
- sendPacketLocked/rawWriteLocked: WriteFrame only, no Flush
Manager integration (drpcmanager/manager.go):
- Creates MuxWriter with onError: m.terminate
- terminate(): wr.Stop() THEN tr.Close() — Stop makes WriteFrame reject
immediately; transport close unblocks any in-flight Write in the drain
goroutine
- Close(): <-wr.Done() to wait for drain goroutine exit
Key Design Decisions:
1. No explicit flush needed.
The drain goroutine continuously pulls from the buffer. Natural batching
occurs because appends accumulate while the goroutine is mid-Write. The
old cork pattern (delay flush until first recv) is unnecessary — appending
is a memcpy under lock, and the goroutine controls when transport I/O
happens.
2. sync.Cond over channels.
Signal coalescing: multiple WriteFrame calls while run() is in Write
produce a single wakeup. No allocation overhead. Stop uses closed bool +
Broadcast. Consistent with packetQueue.
3. Two-phase shutdown (Stop/Done split to avoid deadlock).
Stop() is non-blocking: sets closed, Broadcast, returns immediately.
Done() returns a channel that closes when run() exits. This split is
critical for the onError path: run() -> Write fails -> sets closed ->
onError -> terminate -> Stop (finds closed=true, noop) -> run() returns.
If Stop blocked until run() exited, this path would self-join.
4. run() owns its lifecycle on write failure.
When Write fails, run() sets closed=true itself before calling onError.
The subsequent onError -> terminate -> Stop path finds closed already set.
No coordination needed; the flag is idempotent.
5. No per-stream FrameWriter wrapper.
Initially considered a per-stream FrameWriter wrapping *MuxWriter, but
the only value was a closed check before append. That check lives in
MuxWriter.WriteFrame directly. Streams hold *MuxWriter and call
WriteFrame.
What this unlocks:
- Concurrent multiplexing: streams no longer serialize on writes
- Simplified stream: all flush/cork complexity removed
- Natural batching from continuous drain
- Direct error propagation: transport write failures fire manager
termination via onError callback
Breaking changes:
- drpcstream.Options.ManualFlush removed
- Stream.RawFlush(), SetManualFlush() removed
- Stream constructor: *drpcwire.Writer -> *drpcwire.MuxWriter
Test coverage: 8 concurrency tests for MuxWriter covering concurrent
WriteFrame, write errors, onError->Stop deadlock path, blocked Write
unblocked by Close, concurrent Stop, abort semantics (Stop discards
buffered data), and write-during-active-drain. A data race in the
initial implementation (reading buf capacity without lock) was caught
by these tests and fixed.
0 commit comments