Files
seaweedFS/weed/util/net_timeout.go
Chris Lu 3183a49698 fix: S3 downloads failing after idle timeout (#7626)
* fix: S3 downloads failing after idle timeout (#7618)

The idle timeout was incorrectly terminating active downloads because
read and write deadlines were managed independently. During a download,
the server writes data but rarely reads, so the read deadline would
expire even though the connection was actively being used.

Changes:
1. Simplify to single Timeout field - since this is a 'no activity timeout'
   where any activity extends the deadline, separate read/write timeouts
   are unnecessary. Now uses SetDeadline() which sets both at once.

2. Implement proper 'no activity timeout' - any activity (read or write)
   now extends the deadline. The connection only times out when there's
   genuinely no activity in either direction.

3. Increase default S3 idleTimeout from 10s to 120s for additional safety
   margin when fetching chunks from slow storage backends.

Fixes #7618

* Update weed/util/net_timeout.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-04 18:31:46 -08:00

125 lines
2.7 KiB
Go

package util
import (
"net"
"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.
type Conn struct {
net.Conn
Timeout time.Duration
isClosed bool
}
// extendDeadline extends the connection deadline from now.
// This implements "no activity timeout" - any activity keeps the connection alive.
func (c *Conn) extendDeadline() error {
if c.Timeout > 0 {
return c.Conn.SetDeadline(time.Now().Add(c.Timeout))
}
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
}