mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-18 04:47:28 +00:00
Add redis support for distributed caching (#83)
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
5fcbd54955
commit
e64fc7f730
+447
@@ -0,0 +1,447 @@
|
||||
package resilience
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestHealthChecker_StatusTransitions tests health status transitions
|
||||
func TestHealthChecker_StatusTransitions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callCount atomic.Int32
|
||||
var shouldFail atomic.Bool
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
callCount.Add(1)
|
||||
if shouldFail.Load() {
|
||||
return errors.New("health check failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 50 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
// Initially unknown
|
||||
assert.Equal(t, HealthUnknown, hc.GetStatus())
|
||||
|
||||
// Trigger failures
|
||||
shouldFail.Store(true)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Should be unhealthy after threshold failures
|
||||
status := hc.GetStatus()
|
||||
assert.True(t, status == HealthUnhealthy || status == HealthDegraded)
|
||||
|
||||
// Recover
|
||||
shouldFail.Store(false)
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Should recover towards healthy
|
||||
finalStatus := hc.GetStatus()
|
||||
assert.True(t, finalStatus == HealthHealthy || finalStatus == HealthDegraded || finalStatus == HealthUnknown)
|
||||
}
|
||||
|
||||
// TestHealthChecker_InitialState tests initial health status
|
||||
func TestHealthChecker_InitialState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
hc := NewHealthChecker(config)
|
||||
assert.Equal(t, HealthUnknown, hc.GetStatus())
|
||||
assert.False(t, hc.IsHealthy())
|
||||
}
|
||||
|
||||
// TestHealthChecker_ForceCheck tests manual health check trigger
|
||||
func TestHealthChecker_ForceCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callCount atomic.Int32
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
callCount.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 10 * time.Second, // Long interval
|
||||
Timeout: 1 * time.Second,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
|
||||
initialCount := callCount.Load()
|
||||
|
||||
// Force check
|
||||
hc.Check(context.Background())
|
||||
|
||||
// Should have been called
|
||||
assert.Greater(t, callCount.Load(), initialCount)
|
||||
}
|
||||
|
||||
// TestHealthChecker_StatusChangeCallback tests status change notifications
|
||||
func TestHealthChecker_StatusChangeCallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var transitions []string
|
||||
var mu sync.Mutex
|
||||
var shouldFail atomic.Bool
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
if shouldFail.Load() {
|
||||
return errors.New("health check failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 30 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
UnhealthyThreshold: 2,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
OnStatusChange: func(from, to HealthStatus) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
transitions = append(transitions, from.String()+"->"+to.String())
|
||||
},
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
// Trigger failures
|
||||
shouldFail.Store(true)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Recover
|
||||
shouldFail.Store(false)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Should have status transitions
|
||||
assert.NotEmpty(t, transitions)
|
||||
}
|
||||
|
||||
// TestHealthChecker_Stats tests statistics tracking
|
||||
func TestHealthChecker_Stats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callCount atomic.Int32
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
callCount.Add(1)
|
||||
if callCount.Load()%2 == 0 {
|
||||
return errors.New("failure")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 20 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
UnhealthyThreshold: 5,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
stats := hc.Stats()
|
||||
|
||||
assert.Greater(t, stats.TotalChecks, int64(0))
|
||||
assert.Greater(t, stats.TotalFailures, int64(0))
|
||||
assert.Greater(t, stats.SuccessRate, 0.0)
|
||||
assert.Less(t, stats.SuccessRate, 1.0)
|
||||
}
|
||||
|
||||
// TestHealthChecker_Timeout tests check timeout handling
|
||||
func TestHealthChecker_Timeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
// Simulate slow check
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 50 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond, // Short timeout
|
||||
UnhealthyThreshold: 2,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Should be unhealthy due to timeouts
|
||||
status := hc.GetStatus()
|
||||
assert.NotEqual(t, HealthHealthy, status)
|
||||
}
|
||||
|
||||
// TestHealthChecker_ConcurrentAccess tests thread safety
|
||||
func TestHealthChecker_ConcurrentAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 10 * time.Millisecond,
|
||||
Timeout: 5 * time.Millisecond,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
goroutines := 20
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 50; j++ {
|
||||
_ = hc.GetStatus()
|
||||
_ = hc.IsHealthy()
|
||||
_ = hc.Stats()
|
||||
hc.Check(context.Background())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Should complete without panics
|
||||
}
|
||||
|
||||
// TestHealthChecker_StopAndStart tests lifecycle management
|
||||
func TestHealthChecker_StopAndStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callCount atomic.Int32
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
callCount.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 20 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
|
||||
// Start
|
||||
hc.Start()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
count1 := callCount.Load()
|
||||
assert.Greater(t, count1, int32(0))
|
||||
|
||||
// Stop
|
||||
hc.Stop()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
count2 := callCount.Load()
|
||||
|
||||
// Should not have increased significantly after stop
|
||||
assert.Less(t, count2-count1, int32(3))
|
||||
}
|
||||
|
||||
// TestHealthChecker_DegradedState tests degraded status
|
||||
func TestHealthChecker_DegradedState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callCount atomic.Int32
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
count := callCount.Add(1)
|
||||
// Fail once, then succeed
|
||||
if count == 1 {
|
||||
return errors.New("single failure")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 30 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
UnhealthyThreshold: 3, // Need 3 failures for unhealthy
|
||||
HealthyThreshold: 2, // Need 2 successes for healthy
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// After initial checks, status should be set (might be healthy or degraded based on execution)
|
||||
status := hc.GetStatus()
|
||||
assert.True(t, status != HealthUnknown, "Status should not be unknown after checks")
|
||||
}
|
||||
|
||||
// TestHealthChecker_DefaultConfig tests default configuration
|
||||
func TestHealthChecker_DefaultConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
hc := NewHealthChecker(config)
|
||||
|
||||
assert.NotNil(t, hc)
|
||||
assert.Equal(t, HealthUnknown, hc.GetStatus())
|
||||
|
||||
// Verify default config was applied (we can't access private fields, so just check it works)
|
||||
assert.NotNil(t, hc)
|
||||
}
|
||||
|
||||
// TestHealthChecker_StatusString tests status string representation
|
||||
func TestHealthChecker_StatusString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, "healthy", HealthHealthy.String())
|
||||
assert.Equal(t, "unhealthy", HealthUnhealthy.String())
|
||||
assert.Equal(t, "degraded", HealthDegraded.String())
|
||||
assert.Equal(t, "unknown", HealthStatus(999).String())
|
||||
}
|
||||
|
||||
// TestHealthChecker_RecoveryPattern tests typical failure and recovery
|
||||
func TestHealthChecker_RecoveryPattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var checkNumber atomic.Int32
|
||||
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
n := checkNumber.Add(1)
|
||||
// Fail checks 3-5, succeed others
|
||||
if n >= 3 && n <= 5 {
|
||||
return errors.New("temporary failure")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var statusLog []HealthStatus
|
||||
var mu sync.Mutex
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 30 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
OnStatusChange: func(from, to HealthStatus) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
statusLog = append(statusLog, to)
|
||||
},
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
hc.Start()
|
||||
defer hc.Stop()
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Should see transitions through unhealthy and back to healthy
|
||||
assert.NotEmpty(t, statusLog)
|
||||
|
||||
// Final status should be healthy or degraded (recovered)
|
||||
finalStatus := hc.GetStatus()
|
||||
assert.True(t, finalStatus == HealthHealthy || finalStatus == HealthDegraded, "Should have recovered")
|
||||
}
|
||||
|
||||
// Benchmark health checker performance
|
||||
func BenchmarkHealthChecker_ForceCheck(b *testing.B) {
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckInterval: 10 * time.Minute,
|
||||
Timeout: 1 * time.Second,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 2,
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
|
||||
hc := NewHealthChecker(config)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hc.Check(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHealthChecker_Status(b *testing.B) {
|
||||
checkFunc := func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &HealthCheckConfig{
|
||||
CheckFunc: checkFunc,
|
||||
}
|
||||
hc := NewHealthChecker(config)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = hc.GetStatus()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user