deps: bump fatedier/yamux to v0.3.0 (#5595)

Update github.com/fatedier/yamux from v0.2.0 to v0.3.0. go.mod and go.sum are
the only dependency changes.

v0.3.0 adds a public Stream.Reset() and fixes several stream and session
lifecycle problems: a read or write blocked on a shutting-down session returns
instead of spinning, the stream close lifecycle is safe to retry, failed stream
opens release their tracking state, and ConnectionWriteTimeout is now enforced
as a write deadline on the underlying transport instead of only bounding how
long a caller waits for its own frame.

The last change is observable in frp. When a tcpMux connection stalls, the
write now fails at the transport and the session is torn down, so a new stream
returns ErrSessionShutdown; before, the caller timed out while the session
stayed blocked but alive until a keepalive ping also timed out. All frp
transports implement SetWriteDeadline (TCP, TLS, WebSocket, WSS and KCP); KCP
reports its own timeout error instead of the mapped one. Reads on an
established stream now return ErrSessionShutdown instead of io.EOF when the
session dies; a graceful stream close still returns io.EOF, and frp data paths
already treat any read error as the end of the stream.

Adapt TestDispatcherYamuxWriteTimeoutInterruptsRead to the new behaviour: the
delayed test connection now honours SetWriteDeadline, and the test asserts that
both yamux sessions close after the timed-out write instead of expecting the
session to stay usable.
This commit is contained in:
fatedier
2026-11-03 21:28:54 +08:00
committed by GitHub
parent e6e0aaf9d4
commit 967a7505ae
3 changed files with 39 additions and 19 deletions
+36 -16
View File
@@ -18,6 +18,7 @@ import (
"errors"
"io"
"net"
"os"
"sync"
"sync/atomic"
"testing"
@@ -30,18 +31,45 @@ import (
)
// Delay one real TCP write without injecting an error. The stream's write
// failure must come from yamux's ConnectionWriteTimeout.
// failure must come from yamux's ConnectionWriteTimeout, which yamux enforces
// as a write deadline on the underlying connection, so the delayed write has
// to observe that deadline the way a real socket does.
type delayedTCPConn struct {
net.Conn
armed atomic.Bool
entered chan struct{}
release chan struct{}
deadlineMu sync.Mutex
deadline time.Time
}
func (c *delayedTCPConn) SetWriteDeadline(t time.Time) error {
c.deadlineMu.Lock()
c.deadline = t
c.deadlineMu.Unlock()
return c.Conn.SetWriteDeadline(t)
}
func (c *delayedTCPConn) Write(p []byte) (int, error) {
if c.armed.Swap(false) {
close(c.entered)
<-c.release
c.deadlineMu.Lock()
deadline := c.deadline
c.deadlineMu.Unlock()
var timeout <-chan time.Time
if !deadline.IsZero() {
timer := time.NewTimer(time.Until(deadline))
defer timer.Stop()
timeout = timer.C
}
select {
case <-c.release:
case <-timeout:
// Same value a real socket returns; yamux maps any timeout error
// to ErrConnectionWriteTimeout.
return 0, os.ErrDeadlineExceeded
}
}
return c.Conn.Write(p)
}
@@ -206,20 +234,12 @@ func TestDispatcherYamuxWriteTimeoutInterruptsRead(t *testing.T) {
if observed.closeCalls.Load() != 1 {
t.Fatalf("control closed %d times, want once", observed.closeCalls.Load())
}
if client.IsClosed() || server.IsClosed() {
t.Fatal("yamux session unexpectedly closed")
}
fresh, freshPeer, freshObserved := loopbackControlPair(t, client, server, protocol)
for range 5 {
if err := fresh.WriteMsg(&ReqWorkConn{}); err != nil {
t.Fatal(err)
}
loopbackReadReq(t, freshPeer)
for len(freshObserved.writes) > 0 {
<-freshObserved.writes
}
}
t.Log("Done/readLoop exited without peer FIN; Close once; same yamux session decrypted five fresh messages")
// yamux enforces ConnectionWriteTimeout on the transport, so the
// failed write ends the whole session instead of leaving it blocked
// but alive.
regressionAwait(t, client.CloseChan(), "client yamux session close")
regressionAwait(t, server.CloseChan(), "server yamux session close")
t.Log("Done/readLoop exited without peer FIN; Close once; write timeout closed both yamux sessions")
})
}
}