Files
kubemirror/pkg/circuitbreaker/circuitbreaker_test.go
T
lukaszraczylo 096dca47d1 improvements jan2025 (#6)
* feat(controller): add lazy watcher, improve resource usage and add pattern validation

- [x] Add cache sync health check for readiness probe verification
- [x] Create namespace lister with API reader support for fresh label queries
- [x] Add pattern validation with warning logs for invalid glob patterns
- [x] Implement lazy watcher initialization mode to scan for active resources
- [x] Add requeue delay to namespace reconciler for cache settlement
- [x] Replace custom containsString with slices.Contains from stdlib
- [x] Add structured logging context to reconcilers (kind, group, version)
- [x] Improve error variable naming for clarity in nested conditions
- [x] Add nil-safe label access in namespace reconciler setup
- [x] Add APIReader to namespace and source reconcilers for fresh data
- [x] Improve type assertions with proper error handling in mirror operations
- [x] Reorder struct fields for consistency and readability
- [x] Add comprehensive pattern validation tests and validation API

* feat(controller): add lazy watcher, improve resource usage and add pattern validation

- [x] Add circuit breaker for reconciliation failure tracking and prevention
- [x] Implement granular registration state tracking (not-registered, source-only, fully-registered)
- [x] Add lazy controller initialization for active resource types only
- [x] Consolidate namespace listing into single API call for efficiency
- [x] Add mirror creation verification to catch webhook rejections
- [x] Implement high-cardinality resource detection and warnings
- [x] Add source deletion check in mirror reconciler to prevent races
- [x] Preserve transformation annotations on errors in mirror reconciliation
- [x] Expand constants documentation with labels vs annotations design rationale
- [x] Add comprehensive test coverage for circuit breaker and registration states
- [x] Add mutation-safety tests for hash computation

* fixup! feat(controller): add lazy watcher, improve resource usage and add pattern validation
2026-01-14 13:07:11 +00:00

239 lines
6.5 KiB
Go

package circuitbreaker
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCircuitBreaker_AllowRequest_Closed(t *testing.T) {
cb := NewWithDefaults()
// New resources should be allowed
assert.True(t, cb.AllowRequest("ns", "name", "Secret"))
assert.Equal(t, StateClosed, cb.GetState("ns", "name", "Secret"))
}
func TestCircuitBreaker_OpensAfterThreshold(t *testing.T) {
config := Config{
FailureThreshold: 3,
ResetTimeout: 1 * time.Minute,
HalfOpenSuccessThreshold: 1,
}
cb := New(config)
testErr := errors.New("test error")
// First two failures keep circuit closed
state, justOpened := cb.RecordFailure("ns", "name", "Secret", testErr)
assert.Equal(t, StateClosed, state)
assert.False(t, justOpened)
state, justOpened = cb.RecordFailure("ns", "name", "Secret", testErr)
assert.Equal(t, StateClosed, state)
assert.False(t, justOpened)
// Third failure opens the circuit
state, justOpened = cb.RecordFailure("ns", "name", "Secret", testErr)
assert.Equal(t, StateOpen, state)
assert.True(t, justOpened)
// Request should now be blocked
assert.False(t, cb.AllowRequest("ns", "name", "Secret"))
assert.Equal(t, StateOpen, cb.GetState("ns", "name", "Secret"))
}
func TestCircuitBreaker_ResetOnSuccess(t *testing.T) {
config := Config{
FailureThreshold: 3,
ResetTimeout: 1 * time.Minute,
HalfOpenSuccessThreshold: 1,
}
cb := New(config)
testErr := errors.New("test error")
// Record some failures
cb.RecordFailure("ns", "name", "Secret", testErr)
cb.RecordFailure("ns", "name", "Secret", testErr)
// Success resets failure count
cb.RecordSuccess("ns", "name", "Secret")
assert.Equal(t, 0, cb.GetFailureCount("ns", "name", "Secret"))
// Need 3 more failures to open
cb.RecordFailure("ns", "name", "Secret", testErr)
cb.RecordFailure("ns", "name", "Secret", testErr)
assert.Equal(t, StateClosed, cb.GetState("ns", "name", "Secret"))
}
func TestCircuitBreaker_HalfOpen(t *testing.T) {
config := Config{
FailureThreshold: 2,
ResetTimeout: 100 * time.Millisecond,
HalfOpenSuccessThreshold: 2,
}
cb := New(config)
testErr := errors.New("test error")
// Open the circuit
cb.RecordFailure("ns", "name", "Secret", testErr)
cb.RecordFailure("ns", "name", "Secret", testErr)
assert.Equal(t, StateOpen, cb.GetState("ns", "name", "Secret"))
// Wait for reset timeout
time.Sleep(150 * time.Millisecond)
// Should now be half-open
assert.Equal(t, StateHalfOpen, cb.GetState("ns", "name", "Secret"))
assert.True(t, cb.AllowRequest("ns", "name", "Secret"))
// One success in half-open
cb.RecordSuccess("ns", "name", "Secret")
assert.Equal(t, StateHalfOpen, cb.GetState("ns", "name", "Secret"))
// Second success closes the circuit
cb.RecordSuccess("ns", "name", "Secret")
assert.Equal(t, StateClosed, cb.GetState("ns", "name", "Secret"))
}
func TestCircuitBreaker_HalfOpenFailure(t *testing.T) {
config := Config{
FailureThreshold: 2,
ResetTimeout: 100 * time.Millisecond,
HalfOpenSuccessThreshold: 2,
}
cb := New(config)
testErr := errors.New("test error")
// Open the circuit
cb.RecordFailure("ns", "name", "Secret", testErr)
cb.RecordFailure("ns", "name", "Secret", testErr)
// Wait for reset timeout
time.Sleep(150 * time.Millisecond)
// Call AllowRequest to trigger transition to half-open
assert.True(t, cb.AllowRequest("ns", "name", "Secret"))
assert.Equal(t, StateHalfOpen, cb.GetState("ns", "name", "Secret"))
// Failure in half-open immediately opens
state, justOpened := cb.RecordFailure("ns", "name", "Secret", testErr)
assert.Equal(t, StateOpen, state)
assert.True(t, justOpened)
assert.False(t, cb.AllowRequest("ns", "name", "Secret"))
}
func TestCircuitBreaker_IndependentResources(t *testing.T) {
cb := NewWithDefaults()
testErr := errors.New("test error")
// Failures for resource1
for i := 0; i < 5; i++ {
cb.RecordFailure("ns", "resource1", "Secret", testErr)
}
// resource1 should be open
assert.Equal(t, StateOpen, cb.GetState("ns", "resource1", "Secret"))
// resource2 should still be closed
assert.Equal(t, StateClosed, cb.GetState("ns", "resource2", "Secret"))
assert.True(t, cb.AllowRequest("ns", "resource2", "Secret"))
}
func TestCircuitBreaker_Reset(t *testing.T) {
cb := NewWithDefaults()
testErr := errors.New("test error")
// Open the circuit
for i := 0; i < 5; i++ {
cb.RecordFailure("ns", "name", "Secret", testErr)
}
assert.Equal(t, StateOpen, cb.GetState("ns", "name", "Secret"))
// Reset
cb.Reset("ns", "name", "Secret")
// Should be closed again
assert.Equal(t, StateClosed, cb.GetState("ns", "name", "Secret"))
assert.True(t, cb.AllowRequest("ns", "name", "Secret"))
}
func TestCircuitBreaker_OpenCircuits(t *testing.T) {
cb := NewWithDefaults()
testErr := errors.New("test error")
// Open some circuits
for i := 0; i < 5; i++ {
cb.RecordFailure("ns1", "res1", "Secret", testErr)
cb.RecordFailure("ns2", "res2", "ConfigMap", testErr)
}
open := cb.OpenCircuits()
assert.Len(t, open, 2)
}
func TestCircuitBreaker_Stats(t *testing.T) {
config := Config{
FailureThreshold: 2,
ResetTimeout: 100 * time.Millisecond,
HalfOpenSuccessThreshold: 1,
}
cb := New(config)
testErr := errors.New("test error")
// Create some closed circuits
cb.AllowRequest("ns", "closed1", "Secret")
cb.AllowRequest("ns", "closed2", "Secret")
// Create an open circuit
cb.RecordFailure("ns", "open1", "Secret", testErr)
cb.RecordFailure("ns", "open1", "Secret", testErr)
stats := cb.GetStats()
assert.Equal(t, 3, stats.Total)
assert.Equal(t, 2, stats.Closed)
assert.Equal(t, 1, stats.Open)
assert.Equal(t, 0, stats.HalfOpen)
// Wait for timeout and check half-open
time.Sleep(150 * time.Millisecond)
stats = cb.GetStats()
assert.Equal(t, 3, stats.Total)
assert.Equal(t, 2, stats.Closed)
assert.Equal(t, 0, stats.Open)
assert.Equal(t, 1, stats.HalfOpen)
}
func TestCircuitBreaker_GetLastError(t *testing.T) {
cb := NewWithDefaults()
err1 := errors.New("first error")
err2 := errors.New("second error")
cb.RecordFailure("ns", "name", "Secret", err1)
assert.Equal(t, err1, cb.GetLastError("ns", "name", "Secret"))
cb.RecordFailure("ns", "name", "Secret", err2)
assert.Equal(t, err2, cb.GetLastError("ns", "name", "Secret"))
cb.RecordSuccess("ns", "name", "Secret")
assert.Nil(t, cb.GetLastError("ns", "name", "Secret"))
}
func TestState_String(t *testing.T) {
assert.Equal(t, "closed", StateClosed.String())
assert.Equal(t, "open", StateOpen.String())
assert.Equal(t, "half-open", StateHalfOpen.String())
assert.Equal(t, "unknown", State(99).String())
}