Files
seaweedfs/weed/util/net_timeout.go
T
Chris Lu 39961ce5d7 util: don't let the activity timeout clobber externally-set conn deadlines (#10212)
* util: don't let the activity timeout clobber externally-set conn deadlines

util.Conn extended the connection deadline at the start of every
Read/Write. net/http's server also sets deadlines directly on the same
conn - abortPendingRead sets one in the past to interrupt the pending
background read after each response. The activity extension raced with
and silently overwrote that interrupt, leaving the read blocked (and the
server's conn.serve goroutine stuck in abortPendingRead) until the full
-idleTimeout (default 30s) expired.

Wedged connections count as active, so the volume server's graceful HTTP
drain waited out its whole 30s StopTimeout on shutdown - observed as
weed mini taking ~30s to exit in the FUSE integration tests after any
filer->volume traffic.

Track externally-set deadlines, suspend the activity extension while one
is in force, and serialize deadline updates with a mutex. Activity still
extends both directions at once: a long write-only response must keep
the read deadline alive too, or the server's background read would time
out and cancel the in-flight request.

* util: extend read/write deadlines independently when one side is external

A server-configured WriteTimeout keeps an external write deadline in
force for the whole request, which previously suspended the activity
extension entirely - leaving the read deadline stale from before the
request and letting net/http's background read time out mid-response.
Extend each direction independently instead.
2026-07-02 17:03:00 -07:00

183 lines
4.7 KiB
Go

package util
import (
"net"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/stats"
)
// Listener wraps a net.Listener, and gives a place to store the timeout
// parameters. On Accept, it will wrap the net.Conn with our own Conn for us.
type Listener struct {
net.Listener
Timeout time.Duration
}
func (l *Listener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
stats.ConnectionOpen()
tc := &Conn{
Conn: c,
Timeout: l.Timeout,
}
return tc, nil
}
// Conn wraps a net.Conn and implements a "no activity timeout".
// Any activity (read or write) resets the deadline, so the connection
// only times out when there's no activity in either direction.
//
// Callers may also set deadlines directly — net/http's server does this
// to interrupt a blocked read (abortPendingRead sets a deadline in the
// past) and to enforce its own Read/Write timeouts. The activity
// extension must never overwrite such an externally-set deadline, or the
// interrupt is lost and the read stays blocked until the activity
// timeout fires (wedging connection drain on shutdown for up to the full
// idle timeout). mu serializes deadline updates, and while an external
// deadline is in force on a direction the activity extension for that
// direction is suspended; it resumes once the caller clears the deadline
// (sets the zero time).
type Conn struct {
net.Conn
Timeout time.Duration
isClosed bool
mu sync.Mutex
externalReadDeadline bool
externalWriteDeadline bool
}
func (c *Conn) SetDeadline(t time.Time) error {
c.mu.Lock()
defer c.mu.Unlock()
c.externalReadDeadline = !t.IsZero()
c.externalWriteDeadline = !t.IsZero()
return c.Conn.SetDeadline(t)
}
func (c *Conn) SetReadDeadline(t time.Time) error {
c.mu.Lock()
defer c.mu.Unlock()
c.externalReadDeadline = !t.IsZero()
return c.Conn.SetReadDeadline(t)
}
func (c *Conn) SetWriteDeadline(t time.Time) error {
c.mu.Lock()
defer c.mu.Unlock()
c.externalWriteDeadline = !t.IsZero()
return c.Conn.SetWriteDeadline(t)
}
// extendDeadline extends the connection deadline from now.
// This implements "no activity timeout" - any activity keeps the
// connection alive in both directions (a long write-only response must
// keep the read deadline alive too, or net/http's background read would
// time out and cancel the in-flight request). Each direction is extended
// independently: an externally-managed deadline on one side (e.g. a
// server WriteTimeout) must not leave the other side's deadline stale.
func (c *Conn) extendDeadline() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.Timeout <= 0 {
return nil
}
if !c.externalReadDeadline && !c.externalWriteDeadline {
return c.Conn.SetDeadline(time.Now().Add(c.Timeout))
}
if !c.externalReadDeadline {
if err := c.Conn.SetReadDeadline(time.Now().Add(c.Timeout)); err != nil {
return err
}
}
if !c.externalWriteDeadline {
if err := c.Conn.SetWriteDeadline(time.Now().Add(c.Timeout)); err != nil {
return err
}
}
return nil
}
func (c *Conn) Read(b []byte) (count int, e error) {
// Extend deadline before reading - any activity keeps connection alive
if err := c.extendDeadline(); err != nil {
return 0, err
}
count, e = c.Conn.Read(b)
if e == nil {
stats.BytesIn(int64(count))
}
return
}
func (c *Conn) Write(b []byte) (count int, e error) {
// Extend deadline before writing - any activity keeps connection alive
if err := c.extendDeadline(); err != nil {
return 0, err
}
count, e = c.Conn.Write(b)
if e == nil {
stats.BytesOut(int64(count))
}
return
}
func (c *Conn) Close() error {
err := c.Conn.Close()
if err == nil {
if !c.isClosed {
stats.ConnectionClose()
c.isClosed = true
}
}
return err
}
func NewListener(addr string, timeout time.Duration) (ipListener net.Listener, err error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return
}
ipListener = &Listener{
Listener: listener,
Timeout: timeout,
}
return
}
func NewIpAndLocalListeners(host string, port int, timeout time.Duration) (ipListener net.Listener, localListener net.Listener, err error) {
listener, err := net.Listen("tcp", JoinHostPort(host, port))
if err != nil {
return
}
ipListener = &Listener{
Listener: listener,
Timeout: timeout,
}
if host != "localhost" && host != "" && host != "0.0.0.0" && host != "127.0.0.1" && host != "[::]" && host != "[::1]" {
listener, err = net.Listen("tcp", JoinHostPort("localhost", port))
if err != nil {
glog.V(0).Infof("skip starting on %s:%d: %v", host, port, err)
return ipListener, nil, nil
}
localListener = &Listener{
Listener: listener,
Timeout: timeout,
}
}
return
}