mirror of
https://github.com/redis/go-redis.git
synced 2025-10-18 22:08:50 +03:00
- Adds support for handling push notifications with RESP3. - Using this support adds handlers for hitless upgrades. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Hristo Temelski <hristo.temelski@redis.com>
357 lines
9.4 KiB
Go
357 lines
9.4 KiB
Go
package hitless
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9/logging"
|
|
)
|
|
|
|
func TestCircuitBreaker(t *testing.T) {
|
|
config := &Config{
|
|
LogLevel: logging.LogLevelError, // Reduce noise in tests
|
|
CircuitBreakerFailureThreshold: 5,
|
|
CircuitBreakerResetTimeout: 60 * time.Second,
|
|
CircuitBreakerMaxRequests: 3,
|
|
}
|
|
|
|
t.Run("InitialState", func(t *testing.T) {
|
|
cb := newCircuitBreaker("test-endpoint:6379", config)
|
|
|
|
if cb.IsOpen() {
|
|
t.Error("Circuit breaker should start in closed state")
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerClosed, cb.GetState())
|
|
}
|
|
})
|
|
|
|
t.Run("SuccessfulExecution", func(t *testing.T) {
|
|
cb := newCircuitBreaker("test-endpoint:6379", config)
|
|
|
|
err := cb.Execute(func() error {
|
|
return nil // Success
|
|
})
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerClosed, cb.GetState())
|
|
}
|
|
})
|
|
|
|
t.Run("FailureThreshold", func(t *testing.T) {
|
|
cb := newCircuitBreaker("test-endpoint:6379", config)
|
|
testError := errors.New("test error")
|
|
|
|
// Fail 4 times (below threshold of 5)
|
|
for i := 0; i < 4; i++ {
|
|
err := cb.Execute(func() error {
|
|
return testError
|
|
})
|
|
if err != testError {
|
|
t.Errorf("Expected test error, got %v", err)
|
|
}
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Errorf("Circuit should still be closed after %d failures", i+1)
|
|
}
|
|
}
|
|
|
|
// 5th failure should open the circuit
|
|
err := cb.Execute(func() error {
|
|
return testError
|
|
})
|
|
if err != testError {
|
|
t.Errorf("Expected test error, got %v", err)
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerOpen {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerOpen, cb.GetState())
|
|
}
|
|
})
|
|
|
|
t.Run("OpenCircuitFailsFast", func(t *testing.T) {
|
|
cb := newCircuitBreaker("test-endpoint:6379", config)
|
|
testError := errors.New("test error")
|
|
|
|
// Force circuit to open
|
|
for i := 0; i < 5; i++ {
|
|
cb.Execute(func() error { return testError })
|
|
}
|
|
|
|
// Now it should fail fast
|
|
err := cb.Execute(func() error {
|
|
t.Error("Function should not be called when circuit is open")
|
|
return nil
|
|
})
|
|
|
|
if err != ErrCircuitBreakerOpen {
|
|
t.Errorf("Expected ErrCircuitBreakerOpen, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("HalfOpenTransition", func(t *testing.T) {
|
|
testConfig := &Config{
|
|
LogLevel: logging.LogLevelError,
|
|
CircuitBreakerFailureThreshold: 5,
|
|
CircuitBreakerResetTimeout: 100 * time.Millisecond, // Short timeout for testing
|
|
CircuitBreakerMaxRequests: 3,
|
|
}
|
|
cb := newCircuitBreaker("test-endpoint:6379", testConfig)
|
|
testError := errors.New("test error")
|
|
|
|
// Force circuit to open
|
|
for i := 0; i < 5; i++ {
|
|
cb.Execute(func() error { return testError })
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerOpen {
|
|
t.Error("Circuit should be open")
|
|
}
|
|
|
|
// Wait for reset timeout
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Next call should transition to half-open
|
|
executed := false
|
|
err := cb.Execute(func() error {
|
|
executed = true
|
|
return nil // Success
|
|
})
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
|
|
if !executed {
|
|
t.Error("Function should have been executed in half-open state")
|
|
}
|
|
})
|
|
|
|
t.Run("HalfOpenToClosedTransition", func(t *testing.T) {
|
|
testConfig := &Config{
|
|
LogLevel: logging.LogLevelError,
|
|
CircuitBreakerFailureThreshold: 5,
|
|
CircuitBreakerResetTimeout: 50 * time.Millisecond,
|
|
CircuitBreakerMaxRequests: 3,
|
|
}
|
|
cb := newCircuitBreaker("test-endpoint:6379", testConfig)
|
|
testError := errors.New("test error")
|
|
|
|
// Force circuit to open
|
|
for i := 0; i < 5; i++ {
|
|
cb.Execute(func() error { return testError })
|
|
}
|
|
|
|
// Wait for reset timeout
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Execute successful requests in half-open state
|
|
for i := 0; i < 3; i++ {
|
|
err := cb.Execute(func() error {
|
|
return nil // Success
|
|
})
|
|
if err != nil {
|
|
t.Errorf("Expected no error on attempt %d, got %v", i+1, err)
|
|
}
|
|
}
|
|
|
|
// Circuit should now be closed
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerClosed, cb.GetState())
|
|
}
|
|
})
|
|
|
|
t.Run("HalfOpenToOpenOnFailure", func(t *testing.T) {
|
|
testConfig := &Config{
|
|
LogLevel: logging.LogLevelError,
|
|
CircuitBreakerFailureThreshold: 5,
|
|
CircuitBreakerResetTimeout: 50 * time.Millisecond,
|
|
CircuitBreakerMaxRequests: 3,
|
|
}
|
|
cb := newCircuitBreaker("test-endpoint:6379", testConfig)
|
|
testError := errors.New("test error")
|
|
|
|
// Force circuit to open
|
|
for i := 0; i < 5; i++ {
|
|
cb.Execute(func() error { return testError })
|
|
}
|
|
|
|
// Wait for reset timeout
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// First request in half-open state fails
|
|
err := cb.Execute(func() error {
|
|
return testError
|
|
})
|
|
|
|
if err != testError {
|
|
t.Errorf("Expected test error, got %v", err)
|
|
}
|
|
|
|
// Circuit should be open again
|
|
if cb.GetState() != CircuitBreakerOpen {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerOpen, cb.GetState())
|
|
}
|
|
})
|
|
|
|
t.Run("Stats", func(t *testing.T) {
|
|
cb := newCircuitBreaker("test-endpoint:6379", config)
|
|
testError := errors.New("test error")
|
|
|
|
// Execute some operations
|
|
cb.Execute(func() error { return testError }) // Failure
|
|
cb.Execute(func() error { return testError }) // Failure
|
|
|
|
stats := cb.GetStats()
|
|
|
|
if stats.Endpoint != "test-endpoint:6379" {
|
|
t.Errorf("Expected endpoint 'test-endpoint:6379', got %s", stats.Endpoint)
|
|
}
|
|
|
|
if stats.Failures != 2 {
|
|
t.Errorf("Expected 2 failures, got %d", stats.Failures)
|
|
}
|
|
|
|
if stats.State != CircuitBreakerClosed {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerClosed, stats.State)
|
|
}
|
|
|
|
// Test that success resets failure count
|
|
cb.Execute(func() error { return nil }) // Success
|
|
stats = cb.GetStats()
|
|
|
|
if stats.Failures != 0 {
|
|
t.Errorf("Expected 0 failures after success, got %d", stats.Failures)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCircuitBreakerManager(t *testing.T) {
|
|
config := &Config{
|
|
LogLevel: logging.LogLevelError,
|
|
CircuitBreakerFailureThreshold: 5,
|
|
CircuitBreakerResetTimeout: 60 * time.Second,
|
|
CircuitBreakerMaxRequests: 3,
|
|
}
|
|
|
|
t.Run("GetCircuitBreaker", func(t *testing.T) {
|
|
manager := newCircuitBreakerManager(config)
|
|
|
|
cb1 := manager.GetCircuitBreaker("endpoint1:6379")
|
|
cb2 := manager.GetCircuitBreaker("endpoint2:6379")
|
|
cb3 := manager.GetCircuitBreaker("endpoint1:6379") // Same as cb1
|
|
|
|
if cb1 == cb2 {
|
|
t.Error("Different endpoints should have different circuit breakers")
|
|
}
|
|
|
|
if cb1 != cb3 {
|
|
t.Error("Same endpoint should return the same circuit breaker")
|
|
}
|
|
})
|
|
|
|
t.Run("GetAllStats", func(t *testing.T) {
|
|
manager := newCircuitBreakerManager(config)
|
|
|
|
// Create circuit breakers for different endpoints
|
|
cb1 := manager.GetCircuitBreaker("endpoint1:6379")
|
|
cb2 := manager.GetCircuitBreaker("endpoint2:6379")
|
|
|
|
// Execute some operations
|
|
cb1.Execute(func() error { return nil })
|
|
cb2.Execute(func() error { return errors.New("test error") })
|
|
|
|
stats := manager.GetAllStats()
|
|
|
|
if len(stats) != 2 {
|
|
t.Errorf("Expected 2 circuit breaker stats, got %d", len(stats))
|
|
}
|
|
|
|
// Check that we have stats for both endpoints
|
|
endpoints := make(map[string]bool)
|
|
for _, stat := range stats {
|
|
endpoints[stat.Endpoint] = true
|
|
}
|
|
|
|
if !endpoints["endpoint1:6379"] || !endpoints["endpoint2:6379"] {
|
|
t.Error("Missing stats for expected endpoints")
|
|
}
|
|
})
|
|
|
|
t.Run("Reset", func(t *testing.T) {
|
|
manager := newCircuitBreakerManager(config)
|
|
testError := errors.New("test error")
|
|
|
|
cb := manager.GetCircuitBreaker("test-endpoint:6379")
|
|
|
|
// Force circuit to open
|
|
for i := 0; i < 5; i++ {
|
|
cb.Execute(func() error { return testError })
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerOpen {
|
|
t.Error("Circuit should be open")
|
|
}
|
|
|
|
// Reset all circuit breakers
|
|
manager.Reset()
|
|
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Error("Circuit should be closed after reset")
|
|
}
|
|
|
|
if cb.failures.Load() != 0 {
|
|
t.Error("Failure count should be reset to 0")
|
|
}
|
|
})
|
|
|
|
t.Run("ConfigurableParameters", func(t *testing.T) {
|
|
config := &Config{
|
|
LogLevel: logging.LogLevelError,
|
|
CircuitBreakerFailureThreshold: 10,
|
|
CircuitBreakerResetTimeout: 30 * time.Second,
|
|
CircuitBreakerMaxRequests: 5,
|
|
}
|
|
|
|
cb := newCircuitBreaker("test-endpoint:6379", config)
|
|
|
|
// Test that configuration values are used
|
|
if cb.failureThreshold != 10 {
|
|
t.Errorf("Expected failureThreshold=10, got %d", cb.failureThreshold)
|
|
}
|
|
if cb.resetTimeout != 30*time.Second {
|
|
t.Errorf("Expected resetTimeout=30s, got %v", cb.resetTimeout)
|
|
}
|
|
if cb.maxRequests != 5 {
|
|
t.Errorf("Expected maxRequests=5, got %d", cb.maxRequests)
|
|
}
|
|
|
|
// Test that circuit opens after configured threshold
|
|
testError := errors.New("test error")
|
|
for i := 0; i < 9; i++ {
|
|
err := cb.Execute(func() error { return testError })
|
|
if err != testError {
|
|
t.Errorf("Expected test error, got %v", err)
|
|
}
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Errorf("Circuit should still be closed after %d failures", i+1)
|
|
}
|
|
}
|
|
|
|
// 10th failure should open the circuit
|
|
err := cb.Execute(func() error { return testError })
|
|
if err != testError {
|
|
t.Errorf("Expected test error, got %v", err)
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerOpen {
|
|
t.Errorf("Expected state %v, got %v", CircuitBreakerOpen, cb.GetState())
|
|
}
|
|
})
|
|
}
|