- Add checksums for WAL entries and WAL entry batches - Implement robust retry and circuit breaker patterns for reliability - Add comprehensive tests for message processing and reliability features - Enhance error handling and timeout management
256 lines
6.5 KiB
Go
256 lines
6.5 KiB
Go
package transport
|
|
|
|
import (
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/KevoDB/kevo/pkg/common/log"
|
|
"github.com/KevoDB/kevo/pkg/transport"
|
|
)
|
|
|
|
func TestCalculateBackoff(t *testing.T) {
|
|
policy := transport.RetryPolicy{
|
|
InitialBackoff: 100 * time.Millisecond,
|
|
MaxBackoff: 5 * time.Second,
|
|
BackoffFactor: 2.0,
|
|
Jitter: 0.0, // Disable jitter for deterministic tests
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
attempt int
|
|
expectedRange [2]time.Duration // Min and max expected duration
|
|
}{
|
|
{
|
|
name: "First attempt",
|
|
attempt: 1,
|
|
expectedRange: [2]time.Duration{100 * time.Millisecond, 200 * time.Millisecond},
|
|
},
|
|
{
|
|
name: "Second attempt",
|
|
attempt: 2,
|
|
expectedRange: [2]time.Duration{200 * time.Millisecond, 400 * time.Millisecond},
|
|
},
|
|
{
|
|
name: "Third attempt",
|
|
attempt: 3,
|
|
expectedRange: [2]time.Duration{400 * time.Millisecond, 800 * time.Millisecond},
|
|
},
|
|
{
|
|
name: "Tenth attempt",
|
|
attempt: 10,
|
|
expectedRange: [2]time.Duration{5 * time.Second, 5 * time.Second}, // Capped at max
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
backoff := calculateBackoff(tt.attempt, policy)
|
|
if backoff < tt.expectedRange[0] || backoff > tt.expectedRange[1] {
|
|
t.Errorf("Expected backoff between %v and %v, got %v",
|
|
tt.expectedRange[0], tt.expectedRange[1], backoff)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test with jitter
|
|
t.Run("With jitter", func(t *testing.T) {
|
|
jitterPolicy := transport.RetryPolicy{
|
|
InitialBackoff: 100 * time.Millisecond,
|
|
MaxBackoff: 5 * time.Second,
|
|
BackoffFactor: 2.0,
|
|
Jitter: 0.5, // 50% jitter
|
|
}
|
|
|
|
// Run multiple times to check for variation
|
|
var values []time.Duration
|
|
for i := 0; i < 10; i++ {
|
|
values = append(values, calculateBackoff(2, jitterPolicy))
|
|
}
|
|
|
|
// Check if we have at least some variation (jitter is working)
|
|
allSame := true
|
|
for i := 1; i < len(values); i++ {
|
|
if values[i] != values[0] {
|
|
allSame = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if allSame {
|
|
t.Error("Expected variation with jitter enabled, but all values are the same")
|
|
}
|
|
})
|
|
}
|
|
|
|
// mockClient is a standalone struct for testing that doesn't rely on the reconnectLoop method
|
|
type mockClient struct {
|
|
logger log.Logger
|
|
circuitBreaker *transport.CircuitBreaker
|
|
status transport.TransportStatus
|
|
options transport.TransportOptions
|
|
shuttingDown bool
|
|
reconnectCalled atomic.Bool
|
|
}
|
|
|
|
// handleConnectionError is a copy of the real implementation but uses reconnectCalled instead
|
|
func (c *mockClient) handleConnectionError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Update status
|
|
c.status.LastError = err
|
|
wasConnected := c.status.Connected
|
|
c.status.Connected = false
|
|
|
|
// Log the error
|
|
c.logger.Error("Connection error: %v", err)
|
|
|
|
// Check if we should attempt to reconnect
|
|
if wasConnected && !c.shuttingDown {
|
|
c.logger.Info("Connection lost, attempting to reconnect")
|
|
// Instead of calling reconnectLoop, set the flag
|
|
c.reconnectCalled.Store(true)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// maybeReconnect is a copy of the real implementation but uses reconnectCalled instead
|
|
func (c *mockClient) maybeReconnect() {
|
|
// Check if we're connected
|
|
if c.status.Connected {
|
|
return
|
|
}
|
|
|
|
// Check if the circuit breaker is open
|
|
if c.circuitBreaker.IsOpen() {
|
|
c.logger.Warn("Circuit breaker is open, not attempting to reconnect")
|
|
return
|
|
}
|
|
|
|
// Mark that reconnect was called
|
|
c.reconnectCalled.Store(true)
|
|
}
|
|
|
|
// IsConnected returns whether the client is connected
|
|
func (c *mockClient) IsConnected() bool {
|
|
return c.status.Connected
|
|
}
|
|
|
|
func TestHandleConnectionError(t *testing.T) {
|
|
// Create a mock client
|
|
client := &mockClient{
|
|
logger: log.NewStandardLogger(),
|
|
circuitBreaker: transport.NewCircuitBreaker(3, 100*time.Millisecond),
|
|
status: transport.TransportStatus{
|
|
Connected: true,
|
|
},
|
|
options: transport.TransportOptions{
|
|
RetryPolicy: transport.RetryPolicy{
|
|
InitialBackoff: 1 * time.Millisecond,
|
|
MaxBackoff: 10 * time.Millisecond,
|
|
MaxRetries: 2,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test with a connection error
|
|
testErr := errors.New("test connection error")
|
|
result := client.handleConnectionError(testErr)
|
|
|
|
// Check results
|
|
if result != testErr {
|
|
t.Errorf("Expected error to be returned, got: %v", result)
|
|
}
|
|
|
|
if client.status.Connected {
|
|
t.Error("Expected connected status to be false")
|
|
}
|
|
|
|
if client.status.LastError != testErr {
|
|
t.Errorf("Expected LastError to be set, got: %v", client.status.LastError)
|
|
}
|
|
|
|
// Check if reconnect was attempted
|
|
if !client.reconnectCalled.Load() {
|
|
t.Error("Expected reconnect to be attempted")
|
|
}
|
|
|
|
// Test with nil error
|
|
client.reconnectCalled.Store(false)
|
|
result = client.handleConnectionError(nil)
|
|
|
|
if result != nil {
|
|
t.Errorf("Expected nil error to be returned, got: %v", result)
|
|
}
|
|
|
|
if client.reconnectCalled.Load() {
|
|
t.Error("Expected no reconnect attempt for nil error")
|
|
}
|
|
}
|
|
|
|
func TestMaybeReconnect(t *testing.T) {
|
|
// Test case 1: Already connected
|
|
t.Run("Already connected", func(t *testing.T) {
|
|
client := &mockClient{
|
|
logger: log.NewStandardLogger(),
|
|
circuitBreaker: transport.NewCircuitBreaker(3, 100*time.Millisecond),
|
|
status: transport.TransportStatus{
|
|
Connected: true,
|
|
},
|
|
}
|
|
|
|
client.maybeReconnect()
|
|
|
|
if client.reconnectCalled.Load() {
|
|
t.Error("Expected no reconnect attempt when already connected")
|
|
}
|
|
})
|
|
|
|
// Test case 2: Not connected but circuit breaker open
|
|
t.Run("Circuit breaker open", func(t *testing.T) {
|
|
client := &mockClient{
|
|
logger: log.NewStandardLogger(),
|
|
circuitBreaker: transport.NewCircuitBreaker(3, 100*time.Millisecond),
|
|
status: transport.TransportStatus{
|
|
Connected: false,
|
|
},
|
|
}
|
|
|
|
// Trip the circuit breaker
|
|
client.circuitBreaker.Trip()
|
|
|
|
client.maybeReconnect()
|
|
|
|
if client.reconnectCalled.Load() {
|
|
t.Error("Expected no reconnect attempt when circuit breaker is open")
|
|
}
|
|
})
|
|
|
|
// Test case 3: Not connected and circuit breaker closed
|
|
t.Run("Not connected, circuit closed", func(t *testing.T) {
|
|
client := &mockClient{
|
|
logger: log.NewStandardLogger(),
|
|
circuitBreaker: transport.NewCircuitBreaker(3, 100*time.Millisecond),
|
|
status: transport.TransportStatus{
|
|
Connected: false,
|
|
},
|
|
options: transport.TransportOptions{
|
|
RetryPolicy: transport.RetryPolicy{
|
|
InitialBackoff: 1 * time.Millisecond,
|
|
},
|
|
},
|
|
}
|
|
|
|
client.maybeReconnect()
|
|
|
|
if !client.reconnectCalled.Load() {
|
|
t.Error("Expected reconnect attempt when not connected and circuit breaker closed")
|
|
}
|
|
})
|
|
}
|