Update error handling.
This commit is contained in:
@@ -17,8 +17,8 @@ var (
|
|||||||
ErrConnectionUnavailable = errors.New("connection unavailable")
|
ErrConnectionUnavailable = errors.New("connection unavailable")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConfigError(text string) error {
|
func NewConfigError(err error) error {
|
||||||
return fmt.Errorf("configuration error: %s", text)
|
return fmt.Errorf("configuration error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPoolError(err error) error {
|
func NewPoolError(err error) error {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func TestRunReader(t *testing.T) {
|
|||||||
incomingData <- honeybeetest.MockIncomingData{Err: io.EOF}
|
incomingData <- honeybeetest.MockIncomingData{Err: io.EOF}
|
||||||
|
|
||||||
err := <-conn.Errors()
|
err := <-conn.Errors()
|
||||||
assert.Equal(t, io.EOF, err)
|
assert.ErrorIs(t, err, io.EOF)
|
||||||
|
|
||||||
honeybeetest.Eventually(t, func() bool {
|
honeybeetest.Eventually(t, func() bool {
|
||||||
return conn.State() == transport.StateClosed
|
return conn.State() == transport.StateClosed
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ func ValidateConnectionConfig(config *ConnectionConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.Retry.InitialDelay > config.Retry.MaxDelay {
|
if config.Retry.InitialDelay > config.Retry.MaxDelay {
|
||||||
return NewConfigError("initial delay may not exceed maximum delay")
|
return NewConfigError(InvalidDelays)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-11
@@ -91,7 +91,7 @@ func NewConnectionFromSocket(
|
|||||||
socket types.Socket, config *ConnectionConfig, logger *slog.Logger,
|
socket types.Socket, config *ConnectionConfig, logger *slog.Logger,
|
||||||
) (*Connection, error) {
|
) (*Connection, error) {
|
||||||
if socket == nil {
|
if socket == nil {
|
||||||
return nil, NewConnectionError("socket cannot be nil")
|
return nil, NewConnectionError(ErrNilSocket)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config == nil {
|
if config == nil {
|
||||||
@@ -128,11 +128,11 @@ func (c *Connection) Connect(ctx context.Context) error {
|
|||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if c.socket != nil {
|
if c.socket != nil {
|
||||||
return NewConnectionError("connection already has socket")
|
return NewConnectionError(ErrSocketExists)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.closed {
|
if c.closed {
|
||||||
return NewConnectionError("connection is closed")
|
return NewConnectionError(ErrConnectionClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
@@ -150,7 +150,7 @@ func (c *Connection) Connect(ctx context.Context) error {
|
|||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.Error("connection failed", "error", err)
|
c.logger.Error("connection failed", "error", err)
|
||||||
}
|
}
|
||||||
return err
|
return NewConnectionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.socket = socket
|
c.socket = socket
|
||||||
@@ -217,7 +217,7 @@ func (c *Connection) shutdownSetClosed(wait bool) error {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.closed {
|
if c.closed {
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return ErrConnectionClosed
|
return NewConnectionError(ErrConnectionClosed)
|
||||||
}
|
}
|
||||||
c.closed = true
|
c.closed = true
|
||||||
c.state = StateClosed
|
c.state = StateClosed
|
||||||
@@ -277,29 +277,37 @@ func (c *Connection) startReader() {
|
|||||||
default:
|
default:
|
||||||
messageType, data, err := c.socket.ReadMessage()
|
messageType, data, err := c.socket.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.logger != nil {
|
var wrappedErr error
|
||||||
var closeErr *websocket.CloseError
|
var closeErr *websocket.CloseError
|
||||||
if errors.As(err, &closeErr) {
|
if errors.As(err, &closeErr) {
|
||||||
switch closeErr.Code {
|
switch closeErr.Code {
|
||||||
case websocket.CloseNormalClosure, websocket.CloseGoingAway:
|
case websocket.CloseNormalClosure, websocket.CloseGoingAway:
|
||||||
|
if c.logger != nil {
|
||||||
c.logger.Info("connection closed by peer",
|
c.logger.Info("connection closed by peer",
|
||||||
"code", closeErr.Code,
|
"code", closeErr.Code,
|
||||||
"text", closeErr.Text,
|
"text", closeErr.Text,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
wrappedErr = fmt.Errorf("%w: %w", ErrPeerClosedClean, err)
|
||||||
default:
|
default:
|
||||||
|
if c.logger != nil {
|
||||||
c.logger.Error("unexpected close",
|
c.logger.Error("unexpected close",
|
||||||
"code", closeErr.Code,
|
"code", closeErr.Code,
|
||||||
"text", closeErr.Text,
|
"text", closeErr.Text,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
wrappedErr = fmt.Errorf("%w: %w", ErrPeerClosedUnexpected, err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if c.logger != nil {
|
||||||
c.logger.Error("read error", "error", err)
|
c.logger.Error("read error", "error", err)
|
||||||
}
|
}
|
||||||
|
wrappedErr = fmt.Errorf("%w: %w", ErrReadError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-c.done:
|
case <-c.done:
|
||||||
case c.errors <- err:
|
case c.errors <- wrappedErr:
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -316,7 +324,6 @@ func (c *Connection) startReader() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) Send(data []byte) error {
|
func (c *Connection) Send(data []byte) error {
|
||||||
@@ -324,7 +331,7 @@ func (c *Connection) Send(data []byte) error {
|
|||||||
defer c.writeMu.Unlock()
|
defer c.writeMu.Unlock()
|
||||||
|
|
||||||
if c.closed {
|
if c.closed {
|
||||||
return ErrConnectionClosed
|
return NewConnectionError(ErrConnectionClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.config.WriteTimeout > 0 {
|
if c.config.WriteTimeout > 0 {
|
||||||
@@ -333,7 +340,7 @@ func (c *Connection) Send(data []byte) error {
|
|||||||
c.logger.Error("write deadline error", "error", err)
|
c.logger.Error("write deadline error", "error", err)
|
||||||
}
|
}
|
||||||
c.shutdownExternal()
|
c.shutdownExternal()
|
||||||
return fmt.Errorf("failed to set write deadline: %w", err)
|
return NewConnectionError(fmt.Errorf("%w: %w", ErrFailedWriteDeadline, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +348,7 @@ func (c *Connection) Send(data []byte) error {
|
|||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.Error("write error", "error", err)
|
c.logger.Error("write error", "error", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%w: %w", ErrWriteFailed, err)
|
return NewConnectionError(fmt.Errorf("%w: %w", ErrWriteFailed, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func TestDisconnectedConnectionClose(t *testing.T) {
|
|||||||
|
|
||||||
err = conn.Send([]byte("test"))
|
err = conn.Send([]byte("test"))
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.ErrorContains(t, err, "connection closed")
|
assert.ErrorIs(t, err, ErrConnectionClosed)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ func TestConnectionSend(t *testing.T) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
err = conn.Send([]byte("test"))
|
err = conn.Send([]byte("test"))
|
||||||
assert.ErrorContains(t, err, "failed to set write deadline: test error")
|
assert.ErrorIs(t, err, ErrFailedWriteDeadline)
|
||||||
|
|
||||||
honeybeetest.Eventually(t, func() bool {
|
honeybeetest.Eventually(t, func() bool {
|
||||||
return conn.State() == StateClosed
|
return conn.State() == StateClosed
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package transport
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.wisehodl.dev/jay/go-honeybee/honeybeetest"
|
"git.wisehodl.dev/jay/go-honeybee/honeybeetest"
|
||||||
"git.wisehodl.dev/jay/go-honeybee/types"
|
"git.wisehodl.dev/jay/go-honeybee/types"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -241,7 +243,7 @@ func TestConnect(t *testing.T) {
|
|||||||
|
|
||||||
err = conn.Connect(context.Background())
|
err = conn.Connect(context.Background())
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.ErrorContains(t, err, "already has socket")
|
assert.ErrorIs(t, err, ErrSocketExists)
|
||||||
assert.Equal(t, StateDisconnected, conn.State())
|
assert.Equal(t, StateDisconnected, conn.State())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -253,7 +255,7 @@ func TestConnect(t *testing.T) {
|
|||||||
|
|
||||||
err = conn.Connect(context.Background())
|
err = conn.Connect(context.Background())
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.ErrorContains(t, err, "connection is closed")
|
assert.ErrorIs(t, err, ErrConnectionClosed)
|
||||||
assert.Equal(t, StateClosed, conn.State())
|
assert.Equal(t, StateClosed, conn.State())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -467,17 +469,72 @@ func TestConnectionIncoming(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConnectionErrors(t *testing.T) {
|
func TestConnectionErrors(t *testing.T) {
|
||||||
conn, err := NewConnection("ws://test", nil, nil)
|
t.Run("clean close by peer", func(t *testing.T) {
|
||||||
|
mockSocket := honeybeetest.NewMockSocket()
|
||||||
|
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
|
||||||
|
return 0, nil, &websocket.CloseError{
|
||||||
|
Code: websocket.CloseNormalClosure,
|
||||||
|
Text: "goodbye",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := NewConnectionFromSocket(mockSocket, nil, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
errors := conn.Errors()
|
honeybeetest.Eventually(t, func() bool {
|
||||||
assert.NotNil(t, errors)
|
select {
|
||||||
|
case err := <-conn.Errors():
|
||||||
|
return errors.Is(err, ErrPeerClosedClean)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected clean close error")
|
||||||
|
})
|
||||||
|
|
||||||
// send data through the channel to verify they are the same
|
t.Run("unexpected close", func(t *testing.T) {
|
||||||
testErr := fmt.Errorf("test error")
|
mockSocket := honeybeetest.NewMockSocket()
|
||||||
conn.errors <- testErr
|
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
|
||||||
received := <-errors
|
return 0, nil, &websocket.CloseError{
|
||||||
assert.Equal(t, testErr, received)
|
Code: websocket.CloseProtocolError,
|
||||||
|
Text: "bad protocol",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := NewConnectionFromSocket(mockSocket, nil, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case err := <-conn.Errors():
|
||||||
|
return errors.Is(err, ErrPeerClosedUnexpected)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected unexpected close error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read error", func(t *testing.T) {
|
||||||
|
mockSocket := honeybeetest.NewMockSocket()
|
||||||
|
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
|
||||||
|
return 0, nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := NewConnectionFromSocket(mockSocket, nil, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case err := <-conn.Errors():
|
||||||
|
return errors.Is(err, ErrReadError)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected read error")
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test helpers
|
// Test helpers
|
||||||
|
|||||||
+16
-4
@@ -13,16 +13,28 @@ var (
|
|||||||
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")
|
||||||
InvalidRetryJitterFactor = errors.New("jitter factor must be between 0.0 and 1.0")
|
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
|
||||||
|
ErrNilRetryManager = errors.New("retry manager cannot be nil")
|
||||||
|
ErrNilDialer = errors.New("dialer cannot be nil")
|
||||||
|
ErrEmptyURL = errors.New("URL cannot be empty")
|
||||||
|
|
||||||
// Connection Errors
|
// Connection Errors
|
||||||
ErrConnectionClosed = errors.New("connection closed")
|
ErrConnectionClosed = errors.New("connection closed")
|
||||||
ErrWriteFailed = errors.New("write failed")
|
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(text string) error {
|
func NewConfigError(err error) error {
|
||||||
return fmt.Errorf("configuration error: %s", text)
|
return fmt.Errorf("configuration error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectionError(text string) error {
|
func NewConnectionError(err error) error {
|
||||||
return fmt.Errorf("connection error: %s", text)
|
return fmt.Errorf("connection error: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -54,13 +54,13 @@ func AcquireSocket(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if retryMgr == nil {
|
if retryMgr == nil {
|
||||||
return nil, nil, NewConnectionError("retry manager cannot be nil")
|
return nil, nil, NewConnectionError(ErrNilRetryManager)
|
||||||
}
|
}
|
||||||
if dialer == nil {
|
if dialer == nil {
|
||||||
return nil, nil, NewConnectionError("dialer cannot be nil")
|
return nil, nil, NewConnectionError(ErrNilDialer)
|
||||||
}
|
}
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return nil, nil, NewConnectionError("URL cannot be empty")
|
return nil, nil, NewConnectionError(ErrEmptyURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|||||||
Reference in New Issue
Block a user