merge: PR #92 — make pipe idle timeout configurable via idleTimeout metadata

Merge beezly/fix/configurable-pipe-timeout with conflict resolution
in pipe.go (PipeOption/WithReadTimeout functional options pattern).

Fix pipe_test.go: update 4 pipeHalf() calls to pass readTimeout arg
matching the new signature.
This commit is contained in:
ginuerzh
2026-05-21 21:38:32 +08:00
4 changed files with 55 additions and 15 deletions
+3 -3
View File
@@ -428,7 +428,7 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
start := time.Now() start := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), addr) log.Infof("%s <-> %s", conn.RemoteAddr(), addr)
// xnet.Transport(conn, cc) // xnet.Transport(conn, cc)
xnet.Pipe(ctx, conn, cc) xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.idleTimeout))
log.WithFields(map[string]any{ log.WithFields(map[string]any{
"duration": time.Since(start), "duration": time.Since(start),
}).Infof("%s >-< %s", conn.RemoteAddr(), addr) }).Infof("%s >-< %s", conn.RemoteAddr(), addr)
@@ -694,7 +694,7 @@ func (h *httpHandler) handleUpgradeResponse(ctx context.Context, rw io.ReadWrite
} }
// return xnet.Transport(rw, backConn) // return xnet.Transport(rw, backConn)
return xnet.Pipe(ctx, rw, backConn) return xnet.Pipe(ctx, rw, backConn, xnet.WithReadTimeout(h.md.idleTimeout))
} }
func (h *httpHandler) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { func (h *httpHandler) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
@@ -893,7 +893,7 @@ func (h *httpHandler) authenticate(ctx context.Context, conn net.Conn, req *http
req.Write(cc) req.Write(cc)
// xnet.Transport(conn, cc) // xnet.Transport(conn, cc)
xnet.Pipe(ctx, conn, cc) xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.idleTimeout))
return return
case "file": case "file":
f, _ := os.Open(pr.Value) f, _ := os.Open(pr.Value)
+6
View File
@@ -21,6 +21,7 @@ const (
type metadata struct { type metadata struct {
readTimeout time.Duration readTimeout time.Duration
idleTimeout time.Duration
keepalive bool keepalive bool
compression bool compression bool
probeResistance *probeResistance probeResistance *probeResistance
@@ -57,6 +58,11 @@ func (h *httpHandler) parseMetadata(md mdata.Metadata) error {
h.md.readTimeout = 0 h.md.readTimeout = 0
} }
h.md.idleTimeout = mdutil.GetDuration(md, "idleTimeout")
if h.md.idleTimeout < 0 {
h.md.idleTimeout = 0
}
if m := mdutil.GetStringMapString(md, "http.header", "header"); len(m) > 0 { if m := mdutil.GetStringMapString(md, "http.header", "header"); len(m) > 0 {
hd := http.Header{} hd := http.Header{}
for k, v := range m { for k, v := range m {
+42 -8
View File
@@ -12,15 +12,47 @@ import (
const ( const (
// tcpWaitTimeout implements a TCP half-close timeout. // tcpWaitTimeout implements a TCP half-close timeout.
tcpWaitTimeout = 10 * time.Second tcpWaitTimeout = 10 * time.Second
readTimeout = 30 * time.Second
) )
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser) error { // PipeOption configures the behaviour of Pipe.
type PipeOption func(*pipeOptions)
type pipeOptions struct {
readTimeout time.Duration
}
// WithReadTimeout sets an idle read timeout on each pipe half. When a read
// blocks for longer than d the connection is closed. A value of 0 disables
// the timeout entirely, relying on TCP keepalives or context cancellation to
// detect dead connections.
func WithReadTimeout(d time.Duration) PipeOption {
return func(o *pipeOptions) {
o.readTimeout = d
}
}
// Pipe 在两个连接之间建立双向数据通道
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser, opts ...PipeOption) error {
var options pipeOptions
for _, opt := range opts {
opt(&options)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errCh := make(chan error, 2) errCh := make(chan error, 2)
go func() { errCh <- pipeHalf(ctx, rw1, rw2) }() // 启动两个方向的传输
go func() { errCh <- pipeHalf(ctx, rw2, rw1) }() go func() {
errCh <- pipeHalf(ctx, rw1, rw2, options.readTimeout)
}()
go func() {
errCh <- pipeHalf(ctx, rw2, rw1, options.readTimeout)
}()
// 等待第一个错误或完成
var firstErr error var firstErr error
completed := 0 completed := 0
for completed < 2 { for completed < 2 {
@@ -49,7 +81,7 @@ func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser) error {
} }
// pipeHalf 单向管道传输 // pipeHalf 单向管道传输
func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser) error { func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser, readTimeout time.Duration) error {
defer func() { defer func() {
// 传输完成后执行TCP半关闭 // 传输完成后执行TCP半关闭
halfClose(src, dst) halfClose(src, dst)
@@ -70,9 +102,11 @@ func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser) error {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
// 设置读取超时 // 设置读取超时 (仅当配置了超时时)
if rd, ok := src.(interface{ SetReadDeadline(time.Time) error }); ok { if readTimeout > 0 {
rd.SetReadDeadline(time.Now().Add(readTimeout)) if rd, ok := src.(interface{ SetReadDeadline(time.Time) error }); ok {
rd.SetReadDeadline(time.Now().Add(readTimeout))
}
} }
// 读取数据 // 读取数据
+4 -4
View File
@@ -278,7 +278,7 @@ func Test_pipeHalf(t *testing.T) {
src := newPipeConn([]byte("hello world")) src := newPipeConn([]byte("hello world"))
dst := newPipeConn(nil) dst := newPipeConn(nil)
err := pipeHalf(ctx, src, dst) err := pipeHalf(ctx, src, dst, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -294,7 +294,7 @@ func Test_pipeHalf_ReadError(t *testing.T) {
src.readErr = readErr src.readErr = readErr
dst := newPipeConn(nil) dst := newPipeConn(nil)
err := pipeHalf(ctx, src, dst) err := pipeHalf(ctx, src, dst, 0)
if err == nil { if err == nil {
t.Error("expected error, got nil") t.Error("expected error, got nil")
} }
@@ -306,7 +306,7 @@ func Test_pipeHalf_WriteError(t *testing.T) {
dst := newPipeConn(nil) dst := newPipeConn(nil)
dst.writeClosed = true dst.writeClosed = true
err := pipeHalf(ctx, src, dst) err := pipeHalf(ctx, src, dst, 0)
if err == nil { if err == nil {
t.Error("expected error, got nil") t.Error("expected error, got nil")
} }
@@ -320,7 +320,7 @@ func Test_pipeHalf_ContextCanceled(t *testing.T) {
src.eofAfter = -1 src.eofAfter = -1
dst := newPipeConn(nil) dst := newPipeConn(nil)
err := pipeHalf(ctx, src, dst) err := pipeHalf(ctx, src, dst, 0)
if err == nil { if err == nil {
t.Error("expected context error, got nil") t.Error("expected context error, got nil")
} }