fix(xnet): allow installing a retransmission timer on TCP connections (rebased) (#199)

* fix(xnet): install a retransmission timer on TCP connections

No connection created through x/xnet had one. Neither NewTCPPool nor the
dial path in StackGo set ConnConfig.LossRecovery and ConnConfig.Nanotime,
so tcp.Handler ran with loss recovery disabled: nothing noticed a lost
segment, and the connection simply stopped: the sender waiting for an
ACK that cannot arrive, the receiver for data nobody will resend.

TCPPoolConfig.NanoTime already documents itself as "passed to each
tcp.Conn for retransmission timing (RFC 6298)", including the
time.Now fallback when it is nil; this makes that true. Each connection
gets its own tcp.RTO, which shadows that connection's send sequence space
and so cannot be shared. That is 80 bytes per connection, allocated once
at pool construction, against buffers measured in kilobytes.

The test drops exactly one data segment and requires the byte to arrive
anyway. It depends on the FIN-WAIT-1 retransmission fix, since the server
closes after writing.

* merge main fixes

* claude: fix up test to use ltesto.Sched and enable ltesto.Sched multigoro

* move test and simplify top comment

---------

Co-authored-by: Derek den Haas <i.pestano@easyflor.nl>
This commit is contained in:
Pat Whittingslow
2026-09-08 05:02:12 -03:00
committed by GitHub
parent 80ea9256e7
commit 97b625de47
4 changed files with 336 additions and 30 deletions
+11 -2
View File
@@ -42,8 +42,9 @@ type TCPPoolConfig struct {
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().
// Used for pool timeout tracking. If nil, defaults to time.Now().UnixNano().
// Retransmission timing is not driven by this clock: a [tcp.Policy] carries
// its own. See NewPolicy.
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.
@@ -56,6 +57,9 @@ type TCPPoolConfig struct {
// 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
// NewPolicy if non-nil creates a [tcp.Policy] for each [tcp.Conn] used by the configured Listener.
// NewPolicy should not return reused policies unless the algorithm is stateless. See [tcp.Policy] for more information.
NewPolicy func() tcp.Policy
}
func NewTCPPool(cfg TCPPoolConfig) (*TCPPool, error) {
@@ -88,6 +92,11 @@ func NewTCPPool(cfg TCPPoolConfig) (*TCPPool, error) {
Logger: cfg.ConnLogger,
RWBackoff: cfg.NewBackoff(),
}
if cfg.NewPolicy != nil {
// One Policy per connection: it shadows that connection's send
// sequence space and so cannot be shared.
conncfg.Policy = cfg.NewPolicy()
}
err := pool.conns[i].Configure(conncfg)
if err != nil {
return nil, err