FILE / ScuroNeko/mtg

mtglib/conns.go

Исходный файл и его история в репозитории.
FILE db2e6031a3aeec840ef2c2b51ae2e3288355f416
Files
mtg/mtglib/conns.go
T
Alexey Dolotov 4627910238 fix: use shared idle tracker for relay connections
connIdleTimeout previously set per-direction deadlines independently.
During media downloads the client→telegram direction can be idle at the
application level while telegram→client is actively streaming data.
After IdleTimeout (default 1 min) the idle direction's ReadDeadline
fires, tearing down the entire relay and breaking media transfers.

Replace the per-direction timeout with a shared atomic timestamp that
both pump goroutines update on any successful Read or Write. When a
ReadDeadline fires on the idle direction, we check the shared tracker:
if the other direction was recently active, we retry instead of closing.
The connection is only torn down when both directions are idle for the
full timeout period.

This matches the documented IdleTimeout contract: "if we have any
message which will pass to either direction, a timer is reset."

Overhead: one atomic.Int64 (8 bytes) per connection pair, one
atomic.Store (~1 ns) per Read/Write with data, zero extra goroutines.

Fixes #423
2026-03-30 10:34:16 +03:00

165 lines
3.1 KiB
Go

package mtglib
import (
"bytes"
"context"
"fmt"
"io"
"net"
"sync/atomic"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/pires/go-proxyproto"
)
type connTraffic struct {
essentials.Conn
streamID string
stream EventStream
ctx context.Context
}
func (c connTraffic) Read(b []byte) (int, error) {
n, err := c.Conn.Read(b)
if n > 0 {
c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), true))
}
return n, err //nolint: wrapcheck
}
func (c connTraffic) Write(b []byte) (int, error) {
n, err := c.Conn.Write(b)
if n > 0 {
c.stream.Send(c.ctx, NewEventTraffic(c.streamID, uint(n), false))
}
return n, err //nolint: wrapcheck
}
type connRewind struct {
essentials.Conn
buf bytes.Buffer
active io.Reader
}
func (c *connRewind) Read(p []byte) (int, error) {
return c.active.Read(p)
}
func (c *connRewind) Rewind() {
c.active = io.MultiReader(&c.buf, c.Conn)
}
func newConnRewind(conn essentials.Conn) *connRewind {
rv := &connRewind{
Conn: conn,
}
rv.active = io.TeeReader(conn, &rv.buf)
return rv
}
type connProxyProtocol struct {
essentials.Conn
sourceAddr net.Addr
headersWritten bool
}
func (c *connProxyProtocol) Write(p []byte) (int, error) {
if !c.headersWritten {
headers := proxyproto.HeaderProxyFromAddrs(2, c.sourceAddr, c.RemoteAddr())
toSend, err := headers.Format()
if err != nil {
panic(err)
}
if _, err := c.Conn.Write(toSend); err != nil {
return 0, fmt.Errorf("cannot send proxy protocol header: %w", err)
}
c.headersWritten = true
}
return c.Conn.Write(p)
}
func newConnProxyProtocol(source, target essentials.Conn) *connProxyProtocol {
return &connProxyProtocol{
Conn: target,
sourceAddr: source.RemoteAddr(),
}
}
// idleTracker is a shared idle tracker for a pair of relay connections.
// Both directions update the same timestamp so that activity in one direction
// prevents the other (idle) direction from timing out.
type idleTracker struct {
lastActive atomic.Int64 // unix nanos
timeout time.Duration
}
func newIdleTracker(timeout time.Duration) *idleTracker {
t := &idleTracker{timeout: timeout}
t.touch()
return t
}
func (t *idleTracker) touch() {
t.lastActive.Store(time.Now().UnixNano())
}
func (t *idleTracker) isIdle() bool {
last := time.Unix(0, t.lastActive.Load())
return time.Since(last) >= t.timeout
}
type connIdleTimeout struct {
essentials.Conn
tracker *idleTracker
}
func (c connIdleTimeout) Read(b []byte) (int, error) {
for {
c.SetReadDeadline(time.Now().Add(c.tracker.timeout)) //nolint: errcheck
n, err := c.Conn.Read(b)
if n > 0 {
c.tracker.touch()
return n, err //nolint: wrapcheck
}
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && !c.tracker.isIdle() { //nolint: errorlint
continue
}
return 0, err //nolint: wrapcheck
}
return 0, nil
}
}
func (c connIdleTimeout) Write(b []byte) (int, error) {
c.SetWriteDeadline(time.Now().Add(c.tracker.timeout)) //nolint: errcheck
n, err := c.Conn.Write(b)
if n > 0 {
c.tracker.touch()
}
return n, err //nolint: wrapcheck
}