mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
212 lines
5.8 KiB
Go
212 lines
5.8 KiB
Go
package pool
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDialConn_HangingDial_RetriesWithPerAttemptTimeout(t *testing.T) {
|
|
var calls atomic.Int32
|
|
var sawDeadline atomic.Int32
|
|
|
|
const (
|
|
dialTimeout = 50 * time.Millisecond
|
|
backoff = 10 * time.Millisecond
|
|
retries = 3
|
|
)
|
|
|
|
p := NewConnPool(&Options{
|
|
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
calls.Add(1)
|
|
|
|
// Ensure each attempt has a deadline (pool applies DialTimeout per attempt).
|
|
if dl, ok := ctx.Deadline(); ok {
|
|
rem := time.Until(dl)
|
|
// Very generous bounds to avoid flakes.
|
|
if rem > 5*time.Millisecond && rem <= 2*dialTimeout {
|
|
sawDeadline.Add(1)
|
|
}
|
|
}
|
|
|
|
// Simulate a TCP connect hang: block until the context cancels.
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
},
|
|
// PoolSize is set high so the pool's tryDial probe (triggered when
|
|
// dialErrorsNum == PoolSize) does not fire during this test and race
|
|
// the assertion on `calls`.
|
|
PoolSize: 100,
|
|
MaxConcurrentDials: 1,
|
|
DialTimeout: dialTimeout,
|
|
DialerRetries: retries,
|
|
DialerRetryTimeout: backoff,
|
|
})
|
|
defer p.Close()
|
|
|
|
// Use a parent context with a bounded timeout so this test fails fast (instead of hanging)
|
|
// when dialConn does not apply per-attempt DialTimeout via context.
|
|
parentBudget := dialTimeout*time.Duration(retries) + backoff*time.Duration(retries-1) + 250*time.Millisecond
|
|
ctx, cancel := context.WithTimeout(context.Background(), parentBudget)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
_, err := p.dialConn(ctx, true)
|
|
elapsed := time.Since(start)
|
|
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if got := calls.Load(); got != retries {
|
|
t.Fatalf("expected %d dial attempts, got %d", retries, got)
|
|
}
|
|
if got := sawDeadline.Load(); got != retries {
|
|
t.Fatalf("expected deadline on all attempts, got %d/%d", got, retries)
|
|
}
|
|
|
|
// Each attempt should wait ~dialTimeout, plus backoff between attempts.
|
|
// Allow wide bounds for CI noise.
|
|
min := dialTimeout*time.Duration(retries) + backoff*time.Duration(retries-1)
|
|
if elapsed < min/2 {
|
|
t.Fatalf("dialConn returned too quickly (%v < %v), retries/backoff may not have occurred", elapsed, min/2)
|
|
}
|
|
if elapsed > 5*min {
|
|
t.Fatalf("dialConn took too long (%v > %v), likely hung beyond expected timeouts", elapsed, 5*min)
|
|
}
|
|
}
|
|
|
|
func TestDialConn_DoesNotExtendEarlierParentDeadline(t *testing.T) {
|
|
var calls atomic.Int32
|
|
|
|
p := NewConnPool(&Options{
|
|
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
calls.Add(1)
|
|
dl, ok := ctx.Deadline()
|
|
if !ok {
|
|
return nil, errors.New("expected deadline")
|
|
}
|
|
// Parent deadline should win (be soon).
|
|
if time.Until(dl) > 100*time.Millisecond {
|
|
return nil, errors.New("deadline was unexpectedly extended")
|
|
}
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
},
|
|
PoolSize: 100,
|
|
MaxConcurrentDials: 1,
|
|
DialTimeout: 500 * time.Millisecond,
|
|
DialerRetries: 1,
|
|
})
|
|
defer p.Close()
|
|
|
|
parent, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := p.dialConn(parent, true)
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if got := calls.Load(); got != 1 {
|
|
t.Fatalf("expected 1 dial attempt, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestDialConn_ContextCancelStopsFurtherRetries(t *testing.T) {
|
|
var calls atomic.Int32
|
|
|
|
p := NewConnPool(&Options{
|
|
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
n := calls.Add(1)
|
|
if n == 1 {
|
|
// First attempt fails immediately; test cancels context to stop retries.
|
|
return nil, errors.New("dial failed")
|
|
}
|
|
return nil, errors.New("unexpected extra attempt")
|
|
},
|
|
PoolSize: 100,
|
|
MaxConcurrentDials: 1,
|
|
DialTimeout: 5 * time.Second,
|
|
DialerRetries: 5,
|
|
DialerRetryTimeout: 5 * time.Second,
|
|
})
|
|
defer p.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
// Cancel immediately after the first attempt fails by wrapping dialConn call.
|
|
// We do it via a goroutine so dialConn has a chance to enter the backoff select.
|
|
go func() {
|
|
// Give dialConn a moment to start. This avoids a race where ctx is already canceled
|
|
// before the first attempt and we wouldn't be testing the retry stop path.
|
|
time.Sleep(5 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
_, _ = p.dialConn(ctx, true)
|
|
|
|
if got := calls.Load(); got != 1 {
|
|
t.Fatalf("expected dialer to be called once after cancel, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestDialConn_DialTimeoutDisabled_DoesNotSetDeadline(t *testing.T) {
|
|
var calls atomic.Int32
|
|
|
|
p := NewConnPool(&Options{
|
|
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
calls.Add(1)
|
|
if _, ok := ctx.Deadline(); ok {
|
|
return nil, errors.New("unexpected deadline when DialTimeout disabled")
|
|
}
|
|
return nil, errors.New("dial failed")
|
|
},
|
|
PoolSize: 100,
|
|
MaxConcurrentDials: 1,
|
|
DialTimeout: 0,
|
|
DialerRetries: 1,
|
|
})
|
|
defer p.Close()
|
|
|
|
_, err := p.dialConn(context.Background(), true)
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if got := calls.Load(); got != 1 {
|
|
t.Fatalf("expected 1 dial attempt, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestDialConn_NoBackoffAfterLastAttempt(t *testing.T) {
|
|
var calls atomic.Int32
|
|
backoff := 300 * time.Millisecond
|
|
|
|
p := NewConnPool(&Options{
|
|
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
calls.Add(1)
|
|
return nil, errors.New("dial failed")
|
|
},
|
|
PoolSize: 100,
|
|
MaxConcurrentDials: 1,
|
|
DialTimeout: 5 * time.Second,
|
|
DialerRetries: 1,
|
|
DialerRetryTimeout: backoff,
|
|
})
|
|
defer p.Close()
|
|
|
|
start := time.Now()
|
|
_, err := p.dialConn(context.Background(), true)
|
|
elapsed := time.Since(start)
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if got := calls.Load(); got != 1 {
|
|
t.Fatalf("expected 1 dial attempt, got %d", got)
|
|
}
|
|
// If we slept after the last attempt, this will be ~backoff.
|
|
if elapsed >= backoff/2 {
|
|
t.Fatalf("dialConn took too long (%v); likely slept after last attempt (backoff=%v)", elapsed, backoff)
|
|
}
|
|
}
|