mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
6ba06acdcb
* make backoff explicit API * fix tests to use explicit backoff * fix examples with explicit tcp
198 lines
5.4 KiB
Go
198 lines
5.4 KiB
Go
package xnet
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/tcp"
|
|
)
|
|
|
|
// TCPPool implements tcp.pool.
|
|
type TCPPool struct {
|
|
mu sync.Mutex
|
|
naqcuired int
|
|
conns []tcp.Conn
|
|
userData []any
|
|
acquiredAt []int64
|
|
closingAt []int64
|
|
abortedAt []int64
|
|
nextISS tcp.Value
|
|
_now func() int64
|
|
estbTimeout time.Duration
|
|
closingTimeout time.Duration
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func _() {
|
|
var l tcp.Listener
|
|
l.Reset(0, &TCPPool{}) // compile time guarantee of interface implementation.
|
|
}
|
|
|
|
type TCPPoolConfig struct {
|
|
// PoolSize determines the maximum number of active incoming TCP connections to the pool.
|
|
PoolSize uint16
|
|
QueueSize int
|
|
TxBufSize int
|
|
RxBufSize int
|
|
|
|
Logger *slog.Logger
|
|
ConnLogger *slog.Logger
|
|
|
|
// NanoTime returns the current monotonic time in nanoseconds.
|
|
// Used for pool timeout tracking and passed to each [tcp.Conn] for
|
|
// retransmission timing (RFC 6298). If nil, defaults to time.Now().UnixNano().
|
|
NanoTime func() int64
|
|
// EstablishedTimeout sets the timeout for a TCP connection since it is acquired until it is established.
|
|
// If the connection does not establish in this time it will be closed by the pool.
|
|
EstablishedTimeout time.Duration
|
|
// ClosingTimeout sets the timeout for a TCP connection to close and be returned to Pool.
|
|
// If the connection is not closed in this time it will be aborted by the pool.
|
|
ClosingTimeout time.Duration
|
|
// NewUserData is used to create user data used for each individual TCP connection and returned on GetTCP.
|
|
NewUserData func() any
|
|
// NewBackoff returns the backoff to use for every newly configured TCP connection. Must be non-nil.
|
|
// This should always return a static(non-method) function unless you know what you are doing.
|
|
NewBackoff func() lneto.BackoffStrategy
|
|
}
|
|
|
|
func NewTCPPool(cfg TCPPoolConfig) (*TCPPool, error) {
|
|
if cfg.EstablishedTimeout <= 0 || cfg.ClosingTimeout <= 0 {
|
|
return nil, lneto.ErrInvalidConfig
|
|
} else if cfg.NewBackoff == nil {
|
|
return nil, lneto.ErrMissingHALConfig
|
|
}
|
|
n := int(cfg.PoolSize)
|
|
pool := &TCPPool{
|
|
acquiredAt: make([]int64, n),
|
|
closingAt: make([]int64, n),
|
|
abortedAt: make([]int64, n),
|
|
conns: make([]tcp.Conn, n),
|
|
userData: make([]any, n),
|
|
_now: cfg.NanoTime,
|
|
estbTimeout: cfg.EstablishedTimeout,
|
|
closingTimeout: cfg.ClosingTimeout,
|
|
logger: cfg.Logger,
|
|
}
|
|
allocPerConn := cfg.TxBufSize + cfg.RxBufSize
|
|
bufSpace := make([]byte, n*allocPerConn)
|
|
for i := range pool.conns {
|
|
bufoff := i * allocPerConn
|
|
txOff := bufoff + cfg.RxBufSize
|
|
conncfg := tcp.ConnConfig{
|
|
RxBuf: bufSpace[bufoff:txOff],
|
|
TxBuf: bufSpace[txOff : txOff+cfg.TxBufSize],
|
|
TxPacketQueueSize: cfg.QueueSize,
|
|
Logger: cfg.ConnLogger,
|
|
RWBackoff: cfg.NewBackoff(),
|
|
}
|
|
err := pool.conns[i].Configure(conncfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.NewUserData != nil {
|
|
pool.userData[i] = cfg.NewUserData()
|
|
}
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
func (p *TCPPool) NumberOfAcquired() int {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.naqcuired
|
|
}
|
|
|
|
func (p *TCPPool) GetTCP() (conn *tcp.Conn, userData any, SuggestedISS tcp.Value) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.debug("TCPPool:get")
|
|
for i := range p.conns {
|
|
if p.acquiredAt[i] == 0 {
|
|
p.acquiredAt[i] = p.now()
|
|
p.nextISS += 1000
|
|
p.naqcuired++
|
|
return &p.conns[i], p.userData[i], p.nextISS
|
|
}
|
|
}
|
|
return nil, nil, 0
|
|
}
|
|
|
|
func (p *TCPPool) PutTCP(conn *tcp.Conn) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.debug("TCPPool:put", slog.Uint64("lport", uint64(conn.LocalPort())))
|
|
for i := range p.conns {
|
|
if &p.conns[i] == conn {
|
|
// p.mu.Lock()
|
|
p.conns[i].Abort()
|
|
p.acquiredAt[i] = 0
|
|
p.abortedAt[i] = 0
|
|
p.closingAt[i] = 0
|
|
p.naqcuired--
|
|
// p.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
panic("conn does not belong to this pool")
|
|
}
|
|
|
|
func (p *TCPPool) CheckTimeouts() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.debug("TCPPool:checktimeouts", slog.Int("acq", p.naqcuired))
|
|
for i := range p.conns {
|
|
conn := &p.conns[i]
|
|
st := conn.State()
|
|
if st == tcp.StateEstablished {
|
|
continue
|
|
}
|
|
// p.mu.Lock()
|
|
acq := p.acquiredAt[i]
|
|
// p.mu.Unlock()
|
|
if acq == 0 {
|
|
continue
|
|
} else if st.IsPreestablished() && p.since(acq) > p.estbTimeout {
|
|
// Was acquired and did not reach establishment state so we close.
|
|
// This is part of a syn-flood defense mechanism.
|
|
conn.Close()
|
|
} else if st.IsClosed() || st.IsClosing() {
|
|
// p.mu.Lock()
|
|
if p.closingAt[i] == 0 {
|
|
p.closingAt[i] = p.now()
|
|
} else if p.abortedAt[i] == 0 && p.since(p.closingAt[i]) > p.closingTimeout {
|
|
p.abortedAt[i] = p.now()
|
|
conn.Abort()
|
|
} else if p.abortedAt[i] != 0 && p.since(p.abortedAt[i]) > 10*time.Second {
|
|
println("connection aborted and still not returned to TCPPool")
|
|
println("source", conn.LocalPort(), "remote", conn.RemotePort(), "state", conn.State().String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *TCPPool) since(t int64) time.Duration {
|
|
return time.Duration(p.now() - t)
|
|
}
|
|
|
|
func (p *TCPPool) now() int64 {
|
|
if p._now == nil {
|
|
return time.Now().UnixNano()
|
|
}
|
|
return p._now()
|
|
}
|
|
|
|
func (p *TCPPool) trace(msg string, attrs ...slog.Attr) {
|
|
p.log(slog.LevelDebug-2, msg, attrs...)
|
|
}
|
|
func (p *TCPPool) debug(msg string, attrs ...slog.Attr) {
|
|
p.log(slog.LevelDebug, msg, attrs...)
|
|
}
|
|
func (p *TCPPool) log(lvl slog.Level, msg string, attrs ...slog.Attr) {
|
|
if p.logger != nil {
|
|
p.logger.LogAttrs(context.Background(), lvl, msg, attrs...)
|
|
}
|
|
}
|