transport: flatten RetryConfig to value type, replace nil sentinel with Disabled bool

This commit is contained in:
Jay
2026-05-26 14:01:14 -04:00
parent bcbdb79b32
commit c4d35fe6fa
10 changed files with 82 additions and 93 deletions
+16 -5
View File
@@ -36,7 +36,9 @@ func TestApplyPoolOptions(t *testing.T) {
conf := &PoolConfig{}
err := applyPoolOptions(
conf,
WithConnectionConfig(&transport.ConnectionConfig{}),
WithConnectionConfig(&transport.ConnectionConfig{
Retry: transport.RetryConfig{Disabled: true},
}),
)
assert.NoError(t, err)
@@ -57,7 +59,10 @@ func TestWithBufferSizes(t *testing.T) {
func TestWithConnectionConfig(t *testing.T) {
conf := &PoolConfig{}
opt := WithConnectionConfig(&transport.ConnectionConfig{WriteTimeout: 1 * time.Second})
opt := WithConnectionConfig(&transport.ConnectionConfig{
WriteTimeout: 1 * time.Second,
Retry: transport.RetryConfig{Disabled: true},
})
err := applyPoolOptions(conf, opt)
assert.NoError(t, err)
assert.NotNil(t, conf.ConnectionConfig)
@@ -65,7 +70,11 @@ func TestWithConnectionConfig(t *testing.T) {
// invalid config is rejected
conf = &PoolConfig{}
opt = WithConnectionConfig(&transport.ConnectionConfig{WriteTimeout: -1 * time.Second})
opt = WithConnectionConfig(
&transport.ConnectionConfig{
WriteTimeout: -1 * time.Second,
Retry: transport.RetryConfig{Disabled: true},
})
err = applyPoolOptions(conf, opt)
assert.Error(t, err)
}
@@ -88,14 +97,16 @@ func TestValidatePoolConfig(t *testing.T) {
{
name: "valid complete",
conf: PoolConfig{
ConnectionConfig: &transport.ConnectionConfig{},
ConnectionConfig: &transport.ConnectionConfig{
Retry: transport.RetryConfig{Disabled: true},
},
},
},
{
name: "invalid connection config",
conf: PoolConfig{
ConnectionConfig: &transport.ConnectionConfig{
Retry: &transport.RetryConfig{
Retry: transport.RetryConfig{
InitialDelay: 10 * time.Second,
MaxDelay: 1 * time.Second,
},
+11 -30
View File
@@ -20,10 +20,11 @@ type ConnectionConfig struct {
PingInterval time.Duration
IncomingBufferSize int
ErrorsBufferSize int
Retry *RetryConfig
Retry RetryConfig
}
type RetryConfig struct {
Disabled bool
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
@@ -55,16 +56,12 @@ func GetDefaultConnectionConfig() *ConnectionConfig {
PingInterval: 20 * time.Second,
IncomingBufferSize: 100,
ErrorsBufferSize: 10,
Retry: GetDefaultRetryConfig(),
}
}
func GetDefaultRetryConfig() *RetryConfig {
return &RetryConfig{
MaxRetries: 0, // Infinite retries
InitialDelay: 1 * time.Second,
MaxDelay: 60 * time.Second,
JitterFactor: 0.2,
Retry: RetryConfig{
MaxRetries: 0, // Infinite retries
InitialDelay: 1 * time.Second,
MaxDelay: 60 * time.Second,
JitterFactor: 0.2,
},
}
}
@@ -85,7 +82,7 @@ func ValidateConnectionConfig(config *ConnectionConfig) error {
return err
}
if config.Retry != nil {
if !config.Retry.Disabled {
err = validateMaxRetries(config.Retry.MaxRetries)
if err != nil {
return err
@@ -223,19 +220,15 @@ func WithErrorsBufferSize(value int) ConnectionOption {
}
}
func WithoutRetry() ConnectionOption {
func WithRetryDisabled() ConnectionOption {
return func(c *ConnectionConfig) error {
c.Retry = nil
c.Retry.Disabled = true
return nil
}
}
func WithRetryMaxRetries(value int) ConnectionOption {
return func(c *ConnectionConfig) error {
if c.Retry == nil {
c.Retry = GetDefaultRetryConfig()
}
err := validateMaxRetries(value)
if err != nil {
return err
@@ -248,10 +241,6 @@ func WithRetryMaxRetries(value int) ConnectionOption {
func WithRetryInitialDelay(value time.Duration) ConnectionOption {
return func(c *ConnectionConfig) error {
if c.Retry == nil {
c.Retry = GetDefaultRetryConfig()
}
err := validateInitialDelay(value)
if err != nil {
return err
@@ -264,10 +253,6 @@ func WithRetryInitialDelay(value time.Duration) ConnectionOption {
func WithRetryMaxDelay(value time.Duration) ConnectionOption {
return func(c *ConnectionConfig) error {
if c.Retry == nil {
c.Retry = GetDefaultRetryConfig()
}
err := validateMaxDelay(value)
if err != nil {
return err
@@ -280,10 +265,6 @@ func WithRetryMaxDelay(value time.Duration) ConnectionOption {
func WithRetryJitterFactor(value float64) ConnectionOption {
return func(c *ConnectionConfig) error {
if c.Retry == nil {
c.Retry = GetDefaultRetryConfig()
}
err := validateJitterFactor(value)
if err != nil {
return err
+11 -17
View File
@@ -35,18 +35,12 @@ func TestDefaultConnectionConfig(t *testing.T) {
PingInterval: 20 * time.Second,
IncomingBufferSize: 100,
ErrorsBufferSize: 10,
Retry: GetDefaultRetryConfig(),
})
}
func TestDefaultRetryConnectionConfig(t *testing.T) {
conf := GetDefaultRetryConfig()
assert.Equal(t, conf, &RetryConfig{
MaxRetries: 0,
InitialDelay: 1 * time.Second,
MaxDelay: 60 * time.Second,
JitterFactor: 0.2,
Retry: RetryConfig{
MaxRetries: 0,
InitialDelay: 1 * time.Second,
MaxDelay: 60 * time.Second,
JitterFactor: 0.2,
},
})
}
@@ -114,10 +108,10 @@ func TestWithWriteTimeout(t *testing.T) {
func TestWithRetry(t *testing.T) {
t.Run("without retry", func(t *testing.T) {
conf := GetDefaultConnectionConfig()
opt := WithoutRetry()
opt := WithRetryDisabled()
err := applyConnectionOptions(conf, opt)
assert.NoError(t, err)
assert.Nil(t, conf.Retry)
assert.True(t, conf.Retry.Disabled)
})
t.Run("with attempts", func(t *testing.T) {
@@ -209,7 +203,7 @@ func TestValidateConnectionConfig(t *testing.T) {
}{
{
name: "valid empty",
conf: *&ConnectionConfig{},
conf: ConnectionConfig{Retry: RetryConfig{Disabled: true}},
},
{
name: "valid defaults",
@@ -220,7 +214,7 @@ func TestValidateConnectionConfig(t *testing.T) {
conf: ConnectionConfig{
CloseHandler: (func(code int, text string) error { return nil }),
WriteTimeout: time.Duration(30),
Retry: &RetryConfig{
Retry: RetryConfig{
MaxRetries: 0,
InitialDelay: 2 * time.Second,
MaxDelay: 10 * time.Second,
@@ -231,7 +225,7 @@ func TestValidateConnectionConfig(t *testing.T) {
{
name: "invalid - initial delay > max delay",
conf: ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
InitialDelay: 10 * time.Second,
MaxDelay: 1 * time.Second,
},
+3 -3
View File
@@ -102,7 +102,7 @@ func TestConnectionSend(t *testing.T) {
})
t.Run("write timeout disabled when zero", func(t *testing.T) {
config := &ConnectionConfig{WriteTimeout: 0}
config := &ConnectionConfig{WriteTimeout: 0, Retry: RetryConfig{Disabled: true}}
outgoingData := make(chan honeybeetest.MockOutgoingData, 10)
mockSocket := honeybeetest.NewMockSocket()
@@ -148,7 +148,7 @@ func TestConnectionSend(t *testing.T) {
})
t.Run("write timeout sets deadline when positive", func(t *testing.T) {
config := &ConnectionConfig{WriteTimeout: 30 * time.Millisecond}
config := &ConnectionConfig{WriteTimeout: 30 * time.Millisecond, Retry: RetryConfig{Disabled: true}}
outgoingData := make(chan honeybeetest.MockOutgoingData, 10)
mockSocket := honeybeetest.NewMockSocket()
@@ -194,7 +194,7 @@ func TestConnectionSend(t *testing.T) {
})
t.Run("send fails on deadline error", func(t *testing.T) {
config := &ConnectionConfig{WriteTimeout: 1 * time.Millisecond}
config := &ConnectionConfig{WriteTimeout: 1 * time.Millisecond, Retry: RetryConfig{Disabled: true}}
mockSocket := honeybeetest.NewMockSocket()
+9 -7
View File
@@ -69,7 +69,7 @@ func TestNewConnection(t *testing.T) {
{
name: "valid url, valid config",
url: "wss://relay.example.com:8080/path",
config: &ConnectionConfig{WriteTimeout: 30 * time.Second},
config: &ConnectionConfig{WriteTimeout: 30 * time.Second, Retry: RetryConfig{Disabled: true}},
},
{
name: "invalid url",
@@ -82,7 +82,7 @@ func TestNewConnection(t *testing.T) {
name: "invalid config",
url: "ws://example.com",
config: &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
InitialDelay: 10 * time.Second,
MaxDelay: 1 * time.Second,
},
@@ -152,13 +152,13 @@ func TestNewConnectionFromSocket(t *testing.T) {
{
name: "valid socket with valid config",
socket: honeybeetest.NewMockSocket(),
config: &ConnectionConfig{WriteTimeout: 30 * time.Second},
config: &ConnectionConfig{WriteTimeout: 30 * time.Second, Retry: RetryConfig{Disabled: true}},
},
{
name: "invalid config",
socket: honeybeetest.NewMockSocket(),
config: &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
InitialDelay: 10 * time.Second,
MaxDelay: 1 * time.Second,
},
@@ -173,6 +173,7 @@ func TestNewConnectionFromSocket(t *testing.T) {
CloseHandler: func(code int, text string) error {
return nil
},
Retry: RetryConfig{Disabled: true},
},
},
}
@@ -299,7 +300,7 @@ func TestConnect(t *testing.T) {
t.Run("connect retries on dial failure", func(t *testing.T) {
config := &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
MaxRetries: 2,
InitialDelay: 1 * time.Millisecond,
MaxDelay: 5 * time.Millisecond,
@@ -331,7 +332,7 @@ func TestConnect(t *testing.T) {
t.Run("connect fails after max retries", func(t *testing.T) {
config := &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
MaxRetries: 2,
InitialDelay: 1 * time.Millisecond,
MaxDelay: 5 * time.Millisecond,
@@ -382,6 +383,7 @@ func TestConnect(t *testing.T) {
CloseHandler: func(code int, text string) error {
return nil
},
Retry: RetryConfig{Disabled: true},
}
conn, err := NewConnection(context.Background(), "ws://test", config, nil)
assert.NoError(t, err)
@@ -429,7 +431,7 @@ func TestConnect(t *testing.T) {
func TestConnectContextCancellation(t *testing.T) {
t.Run("context cancelled during connect returns before retries exhaust", func(t *testing.T) {
config := &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
MaxRetries: 100,
InitialDelay: 500 * time.Millisecond,
MaxDelay: 1 * time.Second,
+3 -3
View File
@@ -59,7 +59,7 @@ func TestConnectLogging(t *testing.T) {
mockHandler := honeybeetest.NewMockSlogHandler()
config := &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
MaxRetries: 2,
InitialDelay: 1 * time.Millisecond,
MaxDelay: 5 * time.Millisecond,
@@ -101,7 +101,7 @@ func TestConnectLogging(t *testing.T) {
mockHandler := honeybeetest.NewMockSlogHandler()
config := &ConnectionConfig{
Retry: &RetryConfig{
Retry: RetryConfig{
MaxRetries: 3,
InitialDelay: 1 * time.Millisecond,
MaxDelay: 5 * time.Millisecond,
@@ -279,7 +279,7 @@ func TestWriterLogging(t *testing.T) {
t.Run("write deadline error", func(t *testing.T) {
mockHandler := honeybeetest.NewMockSlogHandler()
config := &ConnectionConfig{WriteTimeout: 1 * time.Millisecond}
config := &ConnectionConfig{WriteTimeout: 1 * time.Millisecond, Retry: RetryConfig{Disabled: true}}
deadlineErr := fmt.Errorf("deadline error")
mockSocket := honeybeetest.NewMockSocket()
+6 -6
View File
@@ -7,16 +7,16 @@ import (
)
type RetryManager struct {
config *RetryConfig
config RetryConfig
retryCount int
saturation int
}
func NewRetryManager(config *RetryConfig) *RetryManager {
func NewRetryManager(config RetryConfig) *RetryManager {
// saturationCount: retry count at which base delay meets or exceeds MaxDelay.
// Conservative by two to preserve jitter variance near the boundary.
saturation := 0
if config != nil &&
if !config.Disabled &&
config.InitialDelay > 0 &&
config.InitialDelay <= config.MaxDelay {
ratio := float64(config.MaxDelay) / float64(config.InitialDelay)
@@ -31,7 +31,7 @@ func NewRetryManager(config *RetryConfig) *RetryManager {
}
func (r *RetryManager) ShouldRetry() bool {
if r.config == nil {
if r.config.Disabled {
return false
}
@@ -43,7 +43,7 @@ func (r *RetryManager) ShouldRetry() bool {
}
func (r *RetryManager) CalculateDelay() time.Duration {
if r.config == nil {
if r.config.Disabled {
return time.Second
}
@@ -54,7 +54,7 @@ func (r *RetryManager) CalculateDelay() time.Duration {
// if saturation is reached, calculated backoff will always be higher than
// the maximum delay
if r.config != nil && r.retryCount >= r.saturation {
if r.retryCount >= r.saturation {
return r.config.MaxDelay
}
+13 -13
View File
@@ -7,7 +7,7 @@ import (
)
func TestNewRetryManager(t *testing.T) {
config := &RetryConfig{
config := RetryConfig{
MaxRetries: 0,
}
@@ -16,14 +16,14 @@ func TestNewRetryManager(t *testing.T) {
assert.Equal(t, config, mgr.config)
assert.Equal(t, 0, mgr.retryCount)
// Should accept nil config
mgr = NewRetryManager(nil)
assert.Nil(t, mgr.config)
// Should accept a disabled config
mgr = NewRetryManager(RetryConfig{Disabled: true})
assert.True(t, mgr.config.Disabled)
assert.Equal(t, 0, mgr.retryCount)
}
func TestRecordRetry(t *testing.T) {
mgr := NewRetryManager(nil)
mgr := NewRetryManager(RetryConfig{Disabled: true})
assert.Equal(t, mgr.retryCount, 0)
mgr.RecordRetry()
@@ -34,13 +34,13 @@ func TestRecordRetry(t *testing.T) {
}
func TestShouldRetry(t *testing.T) {
// never retry if config is nil
mgr := NewRetryManager(nil)
// never retry if config is disabled
mgr := NewRetryManager(RetryConfig{Disabled: true})
assert.False(t, mgr.ShouldRetry())
// always retry if max attempt count is zero
mgr = &RetryManager{
config: &RetryConfig{
config: RetryConfig{
MaxRetries: 0,
},
retryCount: 1000,
@@ -49,7 +49,7 @@ func TestShouldRetry(t *testing.T) {
// retry if below max attempt count
mgr = &RetryManager{
config: &RetryConfig{
config: RetryConfig{
MaxRetries: 10,
},
retryCount: 5,
@@ -58,7 +58,7 @@ func TestShouldRetry(t *testing.T) {
// do not retry if above max attempt count
mgr = &RetryManager{
config: &RetryConfig{
config: RetryConfig{
MaxRetries: 10,
},
retryCount: 11,
@@ -68,12 +68,12 @@ func TestShouldRetry(t *testing.T) {
func TestCalculateDelayDisabled(t *testing.T) {
// default delay if retry is disabled
mgr := NewRetryManager(nil)
mgr := NewRetryManager(RetryConfig{Disabled: true})
assert.Equal(t, time.Second, mgr.CalculateDelay())
}
func TestCalculateDelayWithoutJitter(t *testing.T) {
mgr := NewRetryManager(&RetryConfig{
mgr := NewRetryManager(RetryConfig{
MaxRetries: 0,
InitialDelay: 1 * time.Second,
MaxDelay: 5 * time.Second,
@@ -105,7 +105,7 @@ func TestCalculateDelayWithoutJitter(t *testing.T) {
}
func TestCalculateDelayWithJitter(t *testing.T) {
mgr := NewRetryManager(&RetryConfig{
mgr := NewRetryManager(RetryConfig{
MaxRetries: 0,
InitialDelay: 1 * time.Second,
MaxDelay: 5 * time.Second,
+7 -6
View File
@@ -77,7 +77,7 @@ func TestAcquireSocket(t *testing.T) {
},
}
retryMgr := NewRetryManager(&RetryConfig{
retryMgr := NewRetryManager(RetryConfig{
MaxRetries: tc.maxRetries,
InitialDelay: 1 * time.Millisecond,
MaxDelay: 5 * time.Millisecond,
@@ -106,7 +106,8 @@ func TestAcquireSocketGuards(t *testing.T) {
return honeybeetest.NewMockSocket(), nil, nil
},
}
validRetryMgr := NewRetryManager(GetDefaultRetryConfig())
validRetryConfig := GetDefaultConnectionConfig().Retry
validRetryMgr := NewRetryManager(validRetryConfig)
cases := []struct {
name string
@@ -167,7 +168,7 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
// cancel before acquiring socket
cancel()
retryMgr := NewRetryManager(GetDefaultRetryConfig())
retryMgr := NewRetryManager(GetDefaultConnectionConfig().Retry)
_, _, err := AcquireSocket(ctx, retryMgr, mockDialer, "ws://test", nil, nil)
assert.ErrorIs(t, err, context.Canceled)
@@ -186,7 +187,7 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
retryMgr := NewRetryManager(&RetryConfig{
retryMgr := NewRetryManager(RetryConfig{
MaxRetries: 10,
InitialDelay: 1 * time.Second,
MaxDelay: 1 * time.Second,
@@ -230,7 +231,7 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
retryMgr := NewRetryManager(GetDefaultRetryConfig())
retryMgr := NewRetryManager(GetDefaultConnectionConfig().Retry)
done := make(chan error, 1)
go func() {
_, _, err := AcquireSocket(ctx, retryMgr, mockDialer, "ws://test", nil, nil)
@@ -263,7 +264,7 @@ func TestAcquireSocketPassesHeaders(t *testing.T) {
},
}
retryMgr := NewRetryManager(&RetryConfig{MaxRetries: 0})
retryMgr := NewRetryManager(RetryConfig{MaxRetries: 0, InitialDelay: 1 * time.Millisecond, MaxDelay: 5 * time.Millisecond})
_, _, err := AcquireSocket(context.Background(), retryMgr, mockDialer, "ws://test", header, nil)
assert.NoError(t, err)
+3 -3
View File
@@ -94,7 +94,7 @@ func TestWorkerSession(t *testing.T) {
return nil, nil, errors.New("connection refused")
},
}
cc, _ := transport.NewConnectionConfig(transport.WithoutRetry())
cc, _ := transport.NewConnectionConfig(transport.WithRetryDisabled())
pool.ConnectionConfig = cc
var wg sync.WaitGroup
@@ -150,7 +150,7 @@ func TestWorkerSession(t *testing.T) {
return nil, nil, dialCtx.Err()
},
}
cc, _ := transport.NewConnectionConfig(transport.WithoutRetry())
cc, _ := transport.NewConnectionConfig(transport.WithRetryDisabled())
pool.ConnectionConfig = cc
var wg sync.WaitGroup
@@ -175,7 +175,7 @@ func TestWorkerSession(t *testing.T) {
return nil, nil, dialCtx.Err()
},
}
cc, _ := transport.NewConnectionConfig(transport.WithoutRetry())
cc, _ := transport.NewConnectionConfig(transport.WithRetryDisabled())
pool.ConnectionConfig = cc
var wg sync.WaitGroup