refactor: phase 2 integrated new dialer into worker
This commit is contained in:
+23
-15
@@ -15,6 +15,21 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Errors
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
ErrConnectionClosed = errors.New("connection closed")
|
||||
ErrWriteFailed = errors.New("write failed")
|
||||
ErrNilSocket = errors.New("socket cannot be nil")
|
||||
ErrSocketExists = errors.New("socket already exists")
|
||||
ErrFailedWriteDeadline = errors.New("failed to set write deadline")
|
||||
ErrPeerClosedClean = errors.New("peer closed connection cleanly")
|
||||
ErrPeerClosedUnexpected = errors.New("peer closed connection unexpectedly")
|
||||
ErrReadError = errors.New("read error")
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Types
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -40,22 +55,19 @@ type ConnectionConfig struct {
|
||||
}
|
||||
|
||||
func NewConnectionConfig(opts ...ConnectionOption) (*ConnectionConfig, error) {
|
||||
conf := &ConnectionConfig{
|
||||
cfg := &ConnectionConfig{
|
||||
WriteTimeout: 30 * time.Second,
|
||||
PingInterval: 20 * time.Second,
|
||||
IncomingBufferSize: 100,
|
||||
ErrorsBufferSize: 10,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(conf)
|
||||
o(cfg)
|
||||
}
|
||||
|
||||
err := ValidateConnectionConfig(*conf)
|
||||
if err != nil {
|
||||
if err := ValidateConnectionConfig(*cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
type ConnectionOption func(*ConnectionConfig)
|
||||
@@ -90,21 +102,17 @@ func ValidateConnectionConfig(c ConnectionConfig) error {
|
||||
if c.WriteTimeout < 0 {
|
||||
return fmt.Errorf("invalid write timeout: %v", c.WriteTimeout)
|
||||
}
|
||||
|
||||
if c.PingInterval < 0 {
|
||||
return fmt.Errorf("invalid ping interval: %v", c.PingInterval)
|
||||
}
|
||||
|
||||
if c.IncomingBufferSize < 1 {
|
||||
return fmt.Errorf("invalid incoming buffer size: %d",
|
||||
c.IncomingBufferSize)
|
||||
}
|
||||
|
||||
if c.ErrorsBufferSize < 1 {
|
||||
return fmt.Errorf("invalid errors buffer size: %d",
|
||||
c.ErrorsBufferSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -142,7 +150,7 @@ func NewConnection(
|
||||
ctx context.Context, socket types.Socket, config *ConnectionConfig, handler slog.Handler,
|
||||
) (*Connection, error) {
|
||||
if socket == nil {
|
||||
return nil, NewConnectionError(ErrNilSocket)
|
||||
return nil, ErrNilSocket
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
@@ -197,7 +205,7 @@ func (c *Connection) Send(data []byte) error {
|
||||
defer c.writeMu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return NewConnectionError(ErrConnectionClosed)
|
||||
return ErrConnectionClosed
|
||||
}
|
||||
|
||||
// setup
|
||||
@@ -206,7 +214,7 @@ func (c *Connection) Send(data []byte) error {
|
||||
if c.logger != nil {
|
||||
c.logger.Error("write deadline error", "error", err)
|
||||
}
|
||||
return NewConnectionError(fmt.Errorf("%w: %w", ErrFailedWriteDeadline, err))
|
||||
return fmt.Errorf("%w: %w", ErrFailedWriteDeadline, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +225,7 @@ func (c *Connection) Send(data []byte) error {
|
||||
if c.logger != nil {
|
||||
c.logger.Error("write error", "error", err)
|
||||
}
|
||||
return NewConnectionError(fmt.Errorf("%w: %w", ErrWriteFailed, err))
|
||||
return fmt.Errorf("%w: %w", ErrWriteFailed, err)
|
||||
}
|
||||
|
||||
c.outgoingCount.Add(1)
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestNewConnection(t *testing.T) {
|
||||
// Accessors
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func TestConnectionIncoming(t *testing.T) {
|
||||
func TestConnection_Incoming(t *testing.T) {
|
||||
conn, _, _, _ := setupTestConnection(t)
|
||||
|
||||
incoming := conn.Incoming()
|
||||
@@ -125,7 +125,7 @@ func TestConnectionIncoming(t *testing.T) {
|
||||
assert.Equal(t, testData, received)
|
||||
}
|
||||
|
||||
func TestConnectionErrors(t *testing.T) {
|
||||
func TestConnection_Errors(t *testing.T) {
|
||||
t.Run("clean close by peer", func(t *testing.T) {
|
||||
mockSocket := honeybeetest.NewMockSocket()
|
||||
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
|
||||
@@ -194,7 +194,7 @@ func TestConnectionErrors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectionHeartbeat(t *testing.T) {
|
||||
func TestConnection_Heartbeat(t *testing.T) {
|
||||
t.Run("pinger sends ping frames", func(t *testing.T) {
|
||||
pingCount := atomic.Int32{}
|
||||
socket, _, _ := honeybeetest.SetupTestSocket(t)
|
||||
@@ -248,7 +248,7 @@ func TestConnectionHeartbeat(t *testing.T) {
|
||||
// Send
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func TestConnectionSend(t *testing.T) {
|
||||
func TestConnection_Send(t *testing.T) {
|
||||
t.Run("writes message to socket", func(t *testing.T) {
|
||||
conn, _, _, outgoingData := setupTestConnection(t)
|
||||
defer conn.Close()
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package transport
|
||||
|
||||
import "errors"
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
// URL Errors
|
||||
InvalidProtocol = errors.New("URL must use ws:// or wss:// scheme")
|
||||
|
||||
// Configuration Errors
|
||||
InvalidWriteTimeout = errors.New("write timeout cannot be negative")
|
||||
InvalidPingInterval = errors.New("ping interval cannot be negative")
|
||||
InvalidBufferSize = errors.New("buffer size must be greater than zero")
|
||||
|
||||
InvalidRetryMaxRetries = errors.New("max retry count cannot be negative")
|
||||
InvalidRetryInitialDelay = errors.New("initial delay must be positive")
|
||||
InvalidRetryMaxDelay = errors.New("max delay must be positive")
|
||||
InvalidRetryJitterFactor = errors.New("jitter factor must be between 0.0 and 1.0")
|
||||
InvalidDelays = errors.New("initial delay may not exceed maximum delay")
|
||||
|
||||
// Socket Errors
|
||||
ErrNilDialFunc = errors.New("dial func cannot be nil")
|
||||
|
||||
// Connection Errors
|
||||
ErrConnectionClosed = errors.New("connection closed")
|
||||
ErrWriteFailed = errors.New("write failed")
|
||||
ErrNilSocket = errors.New("socket cannot be nil")
|
||||
ErrSocketExists = errors.New("socket already exists")
|
||||
ErrFailedWriteDeadline = errors.New("failed to set write deadline")
|
||||
ErrPeerClosedClean = errors.New("peer closed connection cleanly")
|
||||
ErrPeerClosedUnexpected = errors.New("peer closed connection unexpectedly")
|
||||
ErrReadError = errors.New("read error")
|
||||
)
|
||||
|
||||
func NewConfigError(err error) error {
|
||||
return fmt.Errorf("configuration error: %w", err)
|
||||
}
|
||||
|
||||
func NewConnectionError(err error) error {
|
||||
return fmt.Errorf("connection error: %w", err)
|
||||
}
|
||||
+14
-9
@@ -2,6 +2,7 @@ package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -12,6 +13,14 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Errors
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
ErrNilDialFunc = errors.New("dial func cannot be nil")
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Types
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -51,11 +60,11 @@ func (d *GorillaDialer) DialContext(
|
||||
// Retry Dialer
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func AcquireSocket(
|
||||
func DialWithRetry(
|
||||
ctx context.Context,
|
||||
mgr *RetryManager,
|
||||
dialFn func(ctx context.Context) (types.Socket, error),
|
||||
errCh chan<- error,
|
||||
dialFn func(context.Context) (types.Socket, error),
|
||||
onError func(error),
|
||||
handler slog.Handler,
|
||||
) (types.Socket, error) {
|
||||
if component.FromContext(ctx) == nil {
|
||||
@@ -88,12 +97,8 @@ func AcquireSocket(
|
||||
return socket, nil
|
||||
}
|
||||
|
||||
if errCh != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case errCh <- err:
|
||||
}
|
||||
if onError != nil {
|
||||
onError(err)
|
||||
}
|
||||
|
||||
if mgr == nil {
|
||||
|
||||
+21
-25
@@ -28,7 +28,7 @@ func TestNewGorillaDialer(t *testing.T) {
|
||||
assert.NotNil(t, dialer.Dialer.NetDialContext)
|
||||
}
|
||||
|
||||
func TestAcquireSocket(t *testing.T) {
|
||||
func TestDialWithRetry(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mockRuns []error
|
||||
@@ -85,7 +85,7 @@ func TestAcquireSocket(t *testing.T) {
|
||||
JitterFactor: 0.0,
|
||||
})
|
||||
|
||||
socket, err := AcquireSocket(
|
||||
socket, err := DialWithRetry(
|
||||
context.Background(), retryMgr, dial, nil, nil)
|
||||
|
||||
assert.Equal(t, tc.wantRetryCount, retryMgr.RetryCount())
|
||||
@@ -103,17 +103,17 @@ func TestAcquireSocket(t *testing.T) {
|
||||
dial := func(ctx context.Context) (types.Socket, error) {
|
||||
return nil, errors.New("dial failed")
|
||||
}
|
||||
_, err := AcquireSocket(context.Background(), nil, dial, nil, nil)
|
||||
_, err := DialWithRetry(context.Background(), nil, dial, nil, nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAcquireSocketNoRetry(t *testing.T) {
|
||||
func TestDialWithRetry_NoRetry(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
dial := func(ctx context.Context) (types.Socket, error) {
|
||||
return honeybeetest.NewMockSocket(), nil
|
||||
}
|
||||
socket, err := AcquireSocket(context.Background(), nil, dial, nil, nil)
|
||||
socket, err := DialWithRetry(context.Background(), nil, dial, nil, nil)
|
||||
assert.NotNil(t, socket)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
@@ -122,12 +122,12 @@ func TestAcquireSocketNoRetry(t *testing.T) {
|
||||
dial := func(ctx context.Context) (types.Socket, error) {
|
||||
return nil, errors.New("dial failed")
|
||||
}
|
||||
_, err := AcquireSocket(context.Background(), nil, dial, nil, nil)
|
||||
_, err := DialWithRetry(context.Background(), nil, dial, nil, nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAcquireSocketGuards(t *testing.T) {
|
||||
func TestDialWithRetry_Guards(t *testing.T) {
|
||||
validRetryConfig, _ := NewRetryConfig()
|
||||
validRetryMgr := NewRetryManager(validRetryConfig)
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestAcquireSocketGuards(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
socket, err := AcquireSocket(
|
||||
socket, err := DialWithRetry(
|
||||
context.Background(), tc.retryMgr, tc.dialFn, nil, nil)
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -156,7 +156,7 @@ func TestAcquireSocketGuards(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireSocketContextCancellation(t *testing.T) {
|
||||
func TestDialWithRetry_ContextCancellation(t *testing.T) {
|
||||
t.Run("context cancelled during sleep returns before next attempt", func(t *testing.T) {
|
||||
dialCount := atomic.Int32{}
|
||||
dial := func(ctx context.Context) (types.Socket, error) {
|
||||
@@ -181,7 +181,7 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := AcquireSocket(ctx, retryMgr, dial, nil, nil)
|
||||
_, err := DialWithRetry(ctx, retryMgr, dial, nil, nil)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
|
||||
// dial count is 2 because the first retry is always immediate
|
||||
assert.Equal(t, int32(2), dialCount.Load())
|
||||
case <-time.After(honeybeetest.TestTimeout):
|
||||
t.Fatal("AcquireSocket did not return after context cancellation")
|
||||
t.Fatal("DialWithRetry did not return after context cancellation")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -217,7 +217,7 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
|
||||
retryMgr := NewRetryManager(retryCfg)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := AcquireSocket(ctx, retryMgr, dial, nil, nil)
|
||||
_, err := DialWithRetry(ctx, retryMgr, dial, nil, nil)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
@@ -229,13 +229,13 @@ func TestAcquireSocketContextCancellation(t *testing.T) {
|
||||
case err := <-done:
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
case <-time.After(honeybeetest.TestTimeout):
|
||||
t.Fatal("AcquireSocket did not return after context cancellation")
|
||||
t.Fatal("DialWithRetry did not return after context cancellation")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestAcquireSocketDialErrors(t *testing.T) {
|
||||
func TestDialWithRetry_DialErrors(t *testing.T) {
|
||||
t.Run("receive one error per failed dial", func(t *testing.T) {
|
||||
dialErr1 := errors.New("attempt 1 failed")
|
||||
dialErr2 := errors.New("attempt 2 failed")
|
||||
@@ -252,7 +252,8 @@ func TestAcquireSocketDialErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
var gotErrors []error
|
||||
onError := func(err error) { gotErrors = append(gotErrors, err) }
|
||||
|
||||
retryCfg, _ := NewRetryConfig(
|
||||
WithMaxRetries(3),
|
||||
@@ -263,17 +264,11 @@ func TestAcquireSocketDialErrors(t *testing.T) {
|
||||
retryMgr := NewRetryManager(retryCfg)
|
||||
|
||||
go func() {
|
||||
_, err := AcquireSocket(context.Background(), retryMgr, dial, errCh, nil)
|
||||
_, err := DialWithRetry(context.Background(), retryMgr, dial, onError, nil)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
var gotErrors []error
|
||||
honeybeetest.Eventually(t, func() bool {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
gotErrors = append(gotErrors, err)
|
||||
default:
|
||||
}
|
||||
return len(gotErrors) == 2
|
||||
}, "expected errors")
|
||||
|
||||
@@ -286,7 +281,8 @@ func TestAcquireSocketDialErrors(t *testing.T) {
|
||||
return honeybeetest.NewMockSocket(), nil
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
called := false
|
||||
onError := func(err error) { called = true }
|
||||
|
||||
retryCfg, _ := NewRetryConfig(
|
||||
WithMaxRetries(3),
|
||||
@@ -296,9 +292,9 @@ func TestAcquireSocketDialErrors(t *testing.T) {
|
||||
)
|
||||
retryMgr := NewRetryManager(retryCfg)
|
||||
|
||||
_, err := AcquireSocket(context.Background(), retryMgr, dial, errCh, nil)
|
||||
_, err := DialWithRetry(context.Background(), retryMgr, dial, onError, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, errCh, 0)
|
||||
assert.False(t, called)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var InvalidProtocol = errors.New("URL must use ws:// or wss:// scheme")
|
||||
|
||||
func ParseURL(urlStr string) (*url.URL, error) {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user