Wrote pool config and tests.

This commit is contained in:
Jay
2026-04-15 20:56:22 -04:00
parent f45dc83179
commit 6d61fcd7e7
4 changed files with 401 additions and 107 deletions

169
config.go
View File

@@ -13,6 +13,82 @@ type CloseHandler func(code int, text string) error
type PoolConfig struct { type PoolConfig struct {
ConnectionConfig *ConnectionConfig ConnectionConfig *ConnectionConfig
IdleTimeout time.Duration
}
type PoolOption func(*PoolConfig) error
func NewPoolConfig(options ...PoolOption) (*PoolConfig, error) {
conf := GetDefaultPoolConfig()
if err := applyPoolOptions(conf, options...); err != nil {
return nil, err
}
if err := validatePoolConfig(conf); err != nil {
return nil, err
}
return conf, nil
}
func GetDefaultPoolConfig() *PoolConfig {
return &PoolConfig{
IdleTimeout: 20 * time.Second,
ConnectionConfig: nil,
}
}
func applyPoolOptions(config *PoolConfig, options ...PoolOption) error {
for _, option := range options {
if err := option(config); err != nil {
return err
}
}
return nil
}
func validatePoolConfig(config *PoolConfig) error {
err := validateIdleTimeout(config.IdleTimeout)
if err != nil {
return err
}
if config.ConnectionConfig != nil {
err = validateConnectionConfig(config.ConnectionConfig)
if err != nil {
return err
}
}
return nil
}
func validateIdleTimeout(value time.Duration) error {
if value < 0 {
return errors.InvalidIdleTimeout
}
return nil
}
// When IdleTimeout is set to zero, idle timeouts are disabled.
func WithIdleTimeout(value time.Duration) PoolOption {
return func(c *PoolConfig) error {
err := validateIdleTimeout(value)
if err != nil {
return err
}
c.IdleTimeout = value
return nil
}
}
func WithConnectionConfig(cc *ConnectionConfig) PoolOption {
return func(c *PoolConfig) error {
err := validateConnectionConfig(cc)
if err != nil {
return err
}
c.ConnectionConfig = cc
return nil
}
} }
// Connection Config // Connection Config
@@ -70,7 +146,32 @@ func applyConnectionOptions(config *ConnectionConfig, options ...ConnectionOptio
} }
func validateConnectionConfig(config *ConnectionConfig) error { func validateConnectionConfig(config *ConnectionConfig) error {
err := validateWriteTimeout(config.WriteTimeout)
if err != nil {
return err
}
if config.Retry != nil { if config.Retry != nil {
err = validateMaxRetries(config.Retry.MaxRetries)
if err != nil {
return err
}
err = validateInitialDelay(config.Retry.InitialDelay)
if err != nil {
return err
}
err = validateMaxDelay(config.Retry.MaxDelay)
if err != nil {
return err
}
err = validateJitterFactor(config.Retry.JitterFactor)
if err != nil {
return err
}
if config.Retry.InitialDelay > config.Retry.MaxDelay { if config.Retry.InitialDelay > config.Retry.MaxDelay {
return errors.NewConfigError("initial delay may not exceed maximum delay") return errors.NewConfigError("initial delay may not exceed maximum delay")
} }
@@ -79,7 +180,40 @@ func validateConnectionConfig(config *ConnectionConfig) error {
return nil return nil
} }
// Configuration Options func validateWriteTimeout(value time.Duration) error {
if value < 0 {
return errors.InvalidWriteTimeout
}
return nil
}
func validateMaxRetries(value int) error {
if value < 0 {
return errors.InvalidRetryMaxRetries
}
return nil
}
func validateInitialDelay(value time.Duration) error {
if value <= 0 {
return errors.InvalidRetryInitialDelay
}
return nil
}
func validateMaxDelay(value time.Duration) error {
if value <= 0 {
return errors.InvalidRetryMaxDelay
}
return nil
}
func validateJitterFactor(value float64) error {
if value < 0.0 || value > 1.0 {
return errors.InvalidRetryJitterFactor
}
return nil
}
func WithCloseHandler(handler CloseHandler) ConnectionOption { func WithCloseHandler(handler CloseHandler) ConnectionOption {
return func(c *ConnectionConfig) error { return func(c *ConnectionConfig) error {
@@ -91,8 +225,9 @@ func WithCloseHandler(handler CloseHandler) ConnectionOption {
// When WriteTimeout is set to zero, read timeouts are disabled. // When WriteTimeout is set to zero, read timeouts are disabled.
func WithWriteTimeout(value time.Duration) ConnectionOption { func WithWriteTimeout(value time.Duration) ConnectionOption {
return func(c *ConnectionConfig) error { return func(c *ConnectionConfig) error {
if value < 0 { err := validateWriteTimeout(value)
return errors.InvalidWriteTimeout if err != nil {
return err
} }
c.WriteTimeout = value c.WriteTimeout = value
return nil return nil
@@ -117,9 +252,12 @@ func WithRetryMaxRetries(value int) ConnectionOption {
if c.Retry == nil { if c.Retry == nil {
c.Retry = GetDefaultRetryConfig() c.Retry = GetDefaultRetryConfig()
} }
if value < 0 {
return errors.InvalidRetryMaxRetries err := validateMaxRetries(value)
if err != nil {
return err
} }
c.Retry.MaxRetries = value c.Retry.MaxRetries = value
return nil return nil
} }
@@ -130,9 +268,12 @@ func WithRetryInitialDelay(value time.Duration) ConnectionOption {
if c.Retry == nil { if c.Retry == nil {
c.Retry = GetDefaultRetryConfig() c.Retry = GetDefaultRetryConfig()
} }
if value <= 0 {
return errors.InvalidRetryInitialDelay err := validateInitialDelay(value)
if err != nil {
return err
} }
c.Retry.InitialDelay = value c.Retry.InitialDelay = value
return nil return nil
} }
@@ -143,9 +284,12 @@ func WithRetryMaxDelay(value time.Duration) ConnectionOption {
if c.Retry == nil { if c.Retry == nil {
c.Retry = GetDefaultRetryConfig() c.Retry = GetDefaultRetryConfig()
} }
if value <= 0 {
return errors.InvalidRetryMaxDelay err := validateMaxDelay(value)
if err != nil {
return err
} }
c.Retry.MaxDelay = value c.Retry.MaxDelay = value
return nil return nil
} }
@@ -156,9 +300,12 @@ func WithRetryJitterFactor(value float64) ConnectionOption {
if c.Retry == nil { if c.Retry == nil {
c.Retry = GetDefaultRetryConfig() c.Retry = GetDefaultRetryConfig()
} }
if value < 0.0 || value > 1.0 {
return errors.InvalidRetryJitterFactor err := validateJitterFactor(value)
if err != nil {
return err
} }
c.Retry.JitterFactor = value c.Retry.JitterFactor = value
return nil return nil
} }

View File

@@ -7,9 +7,9 @@ import (
"time" "time"
) )
// Config Tests // Connection Config Tests
func TestNewConfig(t *testing.T) { func TestNewConnectionConfig(t *testing.T) {
conf, err := NewConnectionConfig() conf, err := NewConnectionConfig()
assert.NoError(t, err) assert.NoError(t, err)
@@ -27,9 +27,9 @@ func TestNewConfig(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
// Default Config Tests // Default Tests
func TestDefaultConfig(t *testing.T) { func TestDefaultConnectionConfig(t *testing.T) {
conf := GetDefaultConnectionConfig() conf := GetDefaultConnectionConfig()
assert.Equal(t, conf, &ConnectionConfig{ assert.Equal(t, conf, &ConnectionConfig{
@@ -39,7 +39,7 @@ func TestDefaultConfig(t *testing.T) {
}) })
} }
func TestDefaultRetryConfig(t *testing.T) { func TestDefaultRetryConnectionConfig(t *testing.T) {
conf := GetDefaultRetryConfig() conf := GetDefaultRetryConfig()
assert.Equal(t, conf, &RetryConfig{ assert.Equal(t, conf, &RetryConfig{
@@ -50,9 +50,9 @@ func TestDefaultRetryConfig(t *testing.T) {
}) })
} }
// Config Builder Tests // Builder Tests
func TestSetConfig(t *testing.T) { func TestApplyConnectionOptions(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
err := applyConnectionOptions( err := applyConnectionOptions(
conf, conf,
@@ -75,7 +75,7 @@ func TestSetConfig(t *testing.T) {
assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries) assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries)
} }
// Config Option Tests // Option Tests
func TestWithCloseHandler(t *testing.T) { func TestWithCloseHandler(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
@@ -104,19 +104,20 @@ func TestWithWriteTimeout(t *testing.T) {
opt = WithWriteTimeout(-30) opt = WithWriteTimeout(-30)
err = applyConnectionOptions(conf, opt) err = applyConnectionOptions(conf, opt)
assert.ErrorIs(t, err, errors.InvalidWriteTimeout) assert.ErrorIs(t, err, errors.InvalidWriteTimeout)
assert.ErrorContains(t, err, "write timeout must be positive") assert.ErrorContains(t, err, "write timeout cannot be negative")
} }
func TestWithRetry(t *testing.T) { func TestWithRetry(t *testing.T) {
t.Run("default", func(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
opt := WithRetry() opt := WithRetry()
err := applyConnectionOptions(conf, opt) err := applyConnectionOptions(conf, opt)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, conf.Retry) assert.NotNil(t, conf.Retry)
assert.Equal(t, conf.Retry, GetDefaultRetryConfig()) assert.Equal(t, conf.Retry, GetDefaultRetryConfig())
} })
func TestWithRetryAttempts(t *testing.T) { t.Run("with attempts", func(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
opt := WithRetryMaxRetries(3) opt := WithRetryMaxRetries(3)
err := applyConnectionOptions(conf, opt) err := applyConnectionOptions(conf, opt)
@@ -133,9 +134,9 @@ func TestWithRetryAttempts(t *testing.T) {
err = applyConnectionOptions(conf, opt) err = applyConnectionOptions(conf, opt)
assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries) assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries)
assert.ErrorContains(t, err, "max retry count cannot be negative") assert.ErrorContains(t, err, "max retry count cannot be negative")
} })
func TestWithRetryInitialDelay(t *testing.T) { t.Run("with initial delay", func(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
opt := WithRetryInitialDelay(10 * time.Second) opt := WithRetryInitialDelay(10 * time.Second)
err := applyConnectionOptions(conf, opt) err := applyConnectionOptions(conf, opt)
@@ -152,9 +153,9 @@ func TestWithRetryInitialDelay(t *testing.T) {
opt = WithRetryInitialDelay(-10 * time.Second) opt = WithRetryInitialDelay(-10 * time.Second)
err = applyConnectionOptions(conf, opt) err = applyConnectionOptions(conf, opt)
assert.ErrorIs(t, err, errors.InvalidRetryInitialDelay) assert.ErrorIs(t, err, errors.InvalidRetryInitialDelay)
} })
func TestWithRetryMaxDelay(t *testing.T) { t.Run("with max delay", func(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
opt := WithRetryMaxDelay(10 * time.Second) opt := WithRetryMaxDelay(10 * time.Second)
err := applyConnectionOptions(conf, opt) err := applyConnectionOptions(conf, opt)
@@ -171,9 +172,9 @@ func TestWithRetryMaxDelay(t *testing.T) {
opt = WithRetryMaxDelay(-10 * time.Second) opt = WithRetryMaxDelay(-10 * time.Second)
err = applyConnectionOptions(conf, opt) err = applyConnectionOptions(conf, opt)
assert.ErrorIs(t, err, errors.InvalidRetryMaxDelay) assert.ErrorIs(t, err, errors.InvalidRetryMaxDelay)
} })
func TestWithRetryJitterFactor(t *testing.T) { t.Run("with jitter factor", func(t *testing.T) {
conf := &ConnectionConfig{} conf := &ConnectionConfig{}
opt := WithRetryJitterFactor(0.2) opt := WithRetryJitterFactor(0.2)
@@ -191,11 +192,12 @@ func TestWithRetryJitterFactor(t *testing.T) {
opt = WithRetryJitterFactor(1.1) opt = WithRetryJitterFactor(1.1)
err = applyConnectionOptions(conf, opt) err = applyConnectionOptions(conf, opt)
assert.ErrorIs(t, err, errors.InvalidRetryJitterFactor) assert.ErrorIs(t, err, errors.InvalidRetryJitterFactor)
})
} }
// Config Validation Tests // Validation Tests
func TestValidateConfig(t *testing.T) { func TestValidateConnectionConfig(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
conf ConnectionConfig conf ConnectionConfig

145
config_pool_test.go Normal file
View File

@@ -0,0 +1,145 @@
package honeybee
import (
"git.wisehodl.dev/jay/go-honeybee/errors"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewPoolConfig(t *testing.T) {
conf, err := NewPoolConfig()
assert.NoError(t, err)
assert.Equal(t, conf, &PoolConfig{
IdleTimeout: 20 * time.Second,
ConnectionConfig: nil,
})
// errors propagate
_, err = NewPoolConfig(WithIdleTimeout(-1))
assert.Error(t, err)
}
func TestDefaultPoolConfig(t *testing.T) {
conf := GetDefaultPoolConfig()
assert.Equal(t, conf, &PoolConfig{
IdleTimeout: 20 * time.Second,
ConnectionConfig: nil,
})
}
func TestApplyPoolOptions(t *testing.T) {
conf := &PoolConfig{}
err := applyPoolOptions(
conf,
WithIdleTimeout(15),
WithConnectionConfig(&ConnectionConfig{}),
)
assert.NoError(t, err)
assert.Equal(t, time.Duration(15), conf.IdleTimeout)
assert.Equal(t, 0*time.Second, conf.ConnectionConfig.WriteTimeout)
// errors propagate
err = applyPoolOptions(
conf,
WithIdleTimeout(-1),
)
assert.ErrorIs(t, err, errors.InvalidIdleTimeout)
}
func TestWithIdleTimeout(t *testing.T) {
conf := &PoolConfig{}
opt := WithIdleTimeout(30)
err := applyPoolOptions(conf, opt)
assert.NoError(t, err)
assert.Equal(t, conf.IdleTimeout, time.Duration(30))
// zero allowed
conf = &PoolConfig{}
opt = WithIdleTimeout(0)
err = applyPoolOptions(conf, opt)
assert.NoError(t, err)
assert.Equal(t, conf.IdleTimeout, time.Duration(0))
// negative disallowed
conf = &PoolConfig{}
opt = WithIdleTimeout(-30)
err = applyPoolOptions(conf, opt)
assert.ErrorIs(t, err, errors.InvalidIdleTimeout)
assert.ErrorContains(t, err, "idle timeout cannot be negative")
}
func TestWithConnectionConfig(t *testing.T) {
conf := &PoolConfig{}
opt := WithConnectionConfig(&ConnectionConfig{WriteTimeout: 1 * time.Second})
err := applyPoolOptions(conf, opt)
assert.NoError(t, err)
assert.NotNil(t, conf.ConnectionConfig)
assert.Equal(t, 1*time.Second, conf.ConnectionConfig.WriteTimeout)
// invalid config is rejected
conf = &PoolConfig{}
opt = WithConnectionConfig(&ConnectionConfig{WriteTimeout: -1 * time.Second})
err = applyPoolOptions(conf, opt)
assert.Error(t, err)
}
func TestValidatePoolConfig(t *testing.T) {
cases := []struct {
name string
conf PoolConfig
wantErr error
wantErrText string
}{
{
name: "valid empty",
conf: *&PoolConfig{},
},
{
name: "valid defaults",
conf: *GetDefaultPoolConfig(),
},
{
name: "valid complete",
conf: PoolConfig{
IdleTimeout: 15 * time.Second,
ConnectionConfig: &ConnectionConfig{},
},
},
{
name: "invalid connection config",
conf: PoolConfig{
ConnectionConfig: &ConnectionConfig{
Retry: &RetryConfig{
InitialDelay: 10 * time.Second,
MaxDelay: 1 * time.Second,
},
},
},
wantErrText: "initial delay may not exceed maximum delay",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := validatePoolConfig(&tc.conf)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
})
}
}

View File

@@ -8,8 +8,8 @@ var (
InvalidProtocol = errors.New("URL must use ws:// or wss:// scheme") InvalidProtocol = errors.New("URL must use ws:// or wss:// scheme")
// Configuration Errors // Configuration Errors
InvalidReadTimeout = errors.New("read timeout must be positive") InvalidIdleTimeout = errors.New("idle timeout cannot be negative")
InvalidWriteTimeout = errors.New("write timeout must be positive") InvalidWriteTimeout = errors.New("write timeout cannot be negative")
InvalidRetryMaxRetries = errors.New("max retry count cannot be negative") InvalidRetryMaxRetries = errors.New("max retry count cannot be negative")
InvalidRetryInitialDelay = errors.New("initial delay must be positive") InvalidRetryInitialDelay = errors.New("initial delay must be positive")
InvalidRetryMaxDelay = errors.New("max delay must be positive") InvalidRetryMaxDelay = errors.New("max delay must be positive")