mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-15 12:27:59 +00:00
3b7b0c69e6
# Which Problems Are Solved If a redis cache has connection issues or any other type of permament error, it tanks the responsiveness of ZITADEL. We currently do not support things like Redis cluster or sentinel. So adding a simple redis cache improves performance but introduces a single point of failure. # How the Problems Are Solved Implement a [circuit breaker](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/dn589784(v=pandp.10)?redirectedfrom=MSDN) as [`redis.Limiter`](https://pkg.go.dev/github.com/redis/go-redis/v9#Limiter) by wrapping sony's [gobreaker](https://github.com/sony/gobreaker) package. This package is picked as it seems well maintained and we already use their `sonyflake` package # Additional Changes - The unit tests constructed an unused `redis.Client` and didn't cleanup the connector. This is now fixed. # Additional Context Closes #8864
169 lines
3.6 KiB
Go
169 lines
3.6 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sony/gobreaker/v2"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zitadel/zitadel/internal/cache"
|
|
)
|
|
|
|
func TestCBConfig_readyToTrip(t *testing.T) {
|
|
type fields struct {
|
|
MaxConsecutiveFailures uint32
|
|
MaxFailureRatio float64
|
|
}
|
|
type args struct {
|
|
counts gobreaker.Counts
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "disabled",
|
|
fields: fields{},
|
|
args: args{
|
|
counts: gobreaker.Counts{
|
|
Requests: 100,
|
|
ConsecutiveFailures: 5,
|
|
TotalFailures: 10,
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no failures",
|
|
fields: fields{
|
|
MaxConsecutiveFailures: 5,
|
|
MaxFailureRatio: 0.1,
|
|
},
|
|
args: args{
|
|
counts: gobreaker.Counts{
|
|
Requests: 100,
|
|
ConsecutiveFailures: 0,
|
|
TotalFailures: 0,
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "some failures",
|
|
fields: fields{
|
|
MaxConsecutiveFailures: 5,
|
|
MaxFailureRatio: 0.1,
|
|
},
|
|
args: args{
|
|
counts: gobreaker.Counts{
|
|
Requests: 100,
|
|
ConsecutiveFailures: 5,
|
|
TotalFailures: 10,
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "consecutive exceeded",
|
|
fields: fields{
|
|
MaxConsecutiveFailures: 5,
|
|
MaxFailureRatio: 0.1,
|
|
},
|
|
args: args{
|
|
counts: gobreaker.Counts{
|
|
Requests: 100,
|
|
ConsecutiveFailures: 6,
|
|
TotalFailures: 0,
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "ratio exceeded",
|
|
fields: fields{
|
|
MaxConsecutiveFailures: 5,
|
|
MaxFailureRatio: 0.1,
|
|
},
|
|
args: args{
|
|
counts: gobreaker.Counts{
|
|
Requests: 100,
|
|
ConsecutiveFailures: 1,
|
|
TotalFailures: 11,
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := &CBConfig{
|
|
MaxConsecutiveFailures: tt.fields.MaxConsecutiveFailures,
|
|
MaxFailureRatio: tt.fields.MaxFailureRatio,
|
|
}
|
|
if got := config.readyToTrip(tt.args.counts); got != tt.want {
|
|
t.Errorf("CBConfig.readyToTrip() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_redisCache_limiter(t *testing.T) {
|
|
c, _ := prepareCache(t, cache.Config{}, withCircuitBreakerOption(
|
|
&CBConfig{
|
|
MaxConsecutiveFailures: 2,
|
|
MaxFailureRatio: 0.4,
|
|
Timeout: 100 * time.Millisecond,
|
|
MaxRetryRequests: 1,
|
|
},
|
|
))
|
|
|
|
ctx := context.Background()
|
|
canceledCtx, cancel := context.WithCancel(ctx)
|
|
cancel()
|
|
timedOutCtx, cancel := context.WithTimeout(ctx, -1)
|
|
defer cancel()
|
|
|
|
// CB is and should remain closed
|
|
for i := 0; i < 10; i++ {
|
|
err := c.Truncate(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
for i := 0; i < 10; i++ {
|
|
err := c.Truncate(canceledCtx)
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
// Timeout err should open the CB after more than 2 failures
|
|
for i := 0; i < 3; i++ {
|
|
err := c.Truncate(timedOutCtx)
|
|
if i > 2 {
|
|
require.ErrorIs(t, err, gobreaker.ErrOpenState)
|
|
} else {
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
}
|
|
}
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// CB should be half-open. If the first command fails, the CB will be Open again
|
|
err := c.Truncate(timedOutCtx)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
err = c.Truncate(timedOutCtx)
|
|
require.ErrorIs(t, err, gobreaker.ErrOpenState)
|
|
|
|
// Reset the DB to closed
|
|
time.Sleep(200 * time.Millisecond)
|
|
err = c.Truncate(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Exceed the ratio
|
|
err = c.Truncate(timedOutCtx)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
err = c.Truncate(ctx)
|
|
require.ErrorIs(t, err, gobreaker.ErrOpenState)
|
|
}
|