mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +00:00
413e4a1b7d
* LRU + cache conflicts prevention. * Bugfix universalCache flooding ( issue #105 ) 1. Traefik cancels the context for old plugin instances 2. Each plugin's Close() method is called 3. The CacheInterfaceWrapper.Close() was calling cache.Close() on the shared singleton caches 4. Each Close() triggered Clear() which logged "Cleared all items" at INFO level
462 lines
11 KiB
Go
462 lines
11 KiB
Go
package backends
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// setupTestRedis creates a miniredis instance for testing
|
|
func setupTestRedis(t *testing.T) (*miniredis.Miniredis, *RedisBackend) {
|
|
t.Helper()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
mr.Close()
|
|
})
|
|
|
|
backend, err := NewRedisBackend(&Config{
|
|
RedisAddr: mr.Addr(),
|
|
RedisPrefix: "test:",
|
|
PoolSize: 5,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
backend.Close()
|
|
})
|
|
|
|
return mr, backend
|
|
}
|
|
|
|
// TestPipeline_Basic tests basic pipeline functionality
|
|
func TestPipeline_Basic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.Addr(),
|
|
MaxConnections: 5,
|
|
ConnectTimeout: 5 * time.Second,
|
|
ReadTimeout: 1 * time.Second,
|
|
WriteTimeout: 1 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
conn, err := pool.Get(ctx)
|
|
require.NoError(t, err)
|
|
defer pool.Put(conn)
|
|
|
|
t.Run("SingleCommand", func(t *testing.T) {
|
|
pipeline := conn.NewPipeline()
|
|
pipeline.Queue("SET", "single-key", "single-value")
|
|
|
|
responses, err := pipeline.Execute()
|
|
require.NoError(t, err)
|
|
require.Len(t, responses, 1)
|
|
assert.Equal(t, "OK", responses[0])
|
|
})
|
|
|
|
t.Run("MultipleCommands", func(t *testing.T) {
|
|
pipeline := conn.NewPipeline()
|
|
pipeline.Queue("SET", "key1", "value1")
|
|
pipeline.Queue("SET", "key2", "value2")
|
|
pipeline.Queue("SET", "key3", "value3")
|
|
pipeline.Queue("GET", "key1")
|
|
pipeline.Queue("GET", "key2")
|
|
pipeline.Queue("GET", "key3")
|
|
|
|
responses, err := pipeline.Execute()
|
|
require.NoError(t, err)
|
|
require.Len(t, responses, 6)
|
|
|
|
// First 3 are SET responses
|
|
assert.Equal(t, "OK", responses[0])
|
|
assert.Equal(t, "OK", responses[1])
|
|
assert.Equal(t, "OK", responses[2])
|
|
|
|
// Last 3 are GET responses
|
|
assert.Equal(t, "value1", responses[3])
|
|
assert.Equal(t, "value2", responses[4])
|
|
assert.Equal(t, "value3", responses[5])
|
|
})
|
|
|
|
t.Run("EmptyPipeline", func(t *testing.T) {
|
|
pipeline := conn.NewPipeline()
|
|
|
|
responses, err := pipeline.Execute()
|
|
require.NoError(t, err)
|
|
assert.Nil(t, responses)
|
|
})
|
|
|
|
t.Run("NilResponses", func(t *testing.T) {
|
|
pipeline := conn.NewPipeline()
|
|
pipeline.Queue("GET", "nonexistent-key")
|
|
|
|
responses, err := pipeline.Execute()
|
|
require.NoError(t, err)
|
|
require.Len(t, responses, 1)
|
|
assert.Nil(t, responses[0])
|
|
})
|
|
}
|
|
|
|
// TestPipeline_SetMany tests pipelined SetMany
|
|
func TestPipeline_SetMany(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, backend := setupTestRedis(t)
|
|
ctx := context.Background()
|
|
|
|
t.Run("SetManyItems", func(t *testing.T) {
|
|
items := make(map[string][]byte)
|
|
for i := 0; i < 10; i++ {
|
|
items[fmt.Sprintf("setmany-key-%d", i)] = []byte(fmt.Sprintf("value-%d", i))
|
|
}
|
|
|
|
err := backend.SetMany(ctx, items, time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all items were set
|
|
for key, expectedValue := range items {
|
|
value, _, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "Key %s should exist", key)
|
|
assert.Equal(t, expectedValue, value)
|
|
}
|
|
})
|
|
|
|
t.Run("SetManyEmpty", func(t *testing.T) {
|
|
err := backend.SetMany(ctx, map[string][]byte{}, time.Minute)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("SetManySingleItem", func(t *testing.T) {
|
|
items := map[string][]byte{
|
|
"single-setmany": []byte("single-value"),
|
|
}
|
|
|
|
err := backend.SetMany(ctx, items, time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
value, _, exists, err := backend.Get(ctx, "single-setmany")
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, []byte("single-value"), value)
|
|
})
|
|
|
|
t.Run("SetManyNoTTL", func(t *testing.T) {
|
|
items := map[string][]byte{
|
|
"nottl-key1": []byte("value1"),
|
|
"nottl-key2": []byte("value2"),
|
|
}
|
|
|
|
err := backend.SetMany(ctx, items, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Keys should exist
|
|
for key := range items {
|
|
exists, err := backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPipeline_GetMany tests pipelined GetMany
|
|
func TestPipeline_GetMany(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, backend := setupTestRedis(t)
|
|
ctx := context.Background()
|
|
|
|
// Pre-populate cache
|
|
for i := 0; i < 10; i++ {
|
|
key := fmt.Sprintf("getmany-key-%d", i)
|
|
value := []byte(fmt.Sprintf("value-%d", i))
|
|
err := backend.Set(ctx, key, value, time.Minute)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("GetManyExisting", func(t *testing.T) {
|
|
keys := make([]string, 10)
|
|
for i := 0; i < 10; i++ {
|
|
keys[i] = fmt.Sprintf("getmany-key-%d", i)
|
|
}
|
|
|
|
results, err := backend.GetMany(ctx, keys)
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 10)
|
|
|
|
for i, key := range keys {
|
|
assert.Equal(t, []byte(fmt.Sprintf("value-%d", i)), results[key])
|
|
}
|
|
})
|
|
|
|
t.Run("GetManyMixed", func(t *testing.T) {
|
|
keys := []string{
|
|
"getmany-key-0", // exists
|
|
"nonexistent-key-1", // doesn't exist
|
|
"getmany-key-2", // exists
|
|
"nonexistent-key-2", // doesn't exist
|
|
}
|
|
|
|
results, err := backend.GetMany(ctx, keys)
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 2) // Only existing keys
|
|
|
|
assert.Equal(t, []byte("value-0"), results["getmany-key-0"])
|
|
assert.Equal(t, []byte("value-2"), results["getmany-key-2"])
|
|
assert.NotContains(t, results, "nonexistent-key-1")
|
|
assert.NotContains(t, results, "nonexistent-key-2")
|
|
})
|
|
|
|
t.Run("GetManyEmpty", func(t *testing.T) {
|
|
results, err := backend.GetMany(ctx, []string{})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, results)
|
|
assert.Len(t, results, 0)
|
|
})
|
|
|
|
t.Run("GetManySingleKey", func(t *testing.T) {
|
|
results, err := backend.GetMany(ctx, []string{"getmany-key-5"})
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 1)
|
|
assert.Equal(t, []byte("value-5"), results["getmany-key-5"])
|
|
})
|
|
|
|
t.Run("GetManyAllNonexistent", func(t *testing.T) {
|
|
keys := []string{
|
|
"nonexistent-1",
|
|
"nonexistent-2",
|
|
"nonexistent-3",
|
|
}
|
|
|
|
results, err := backend.GetMany(ctx, keys)
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 0)
|
|
})
|
|
}
|
|
|
|
// TestPipeline_LargeBatch tests pipelining with large batches
|
|
func TestPipeline_LargeBatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, backend := setupTestRedis(t)
|
|
ctx := context.Background()
|
|
|
|
t.Run("SetMany100Items", func(t *testing.T) {
|
|
items := make(map[string][]byte)
|
|
for i := 0; i < 100; i++ {
|
|
items[fmt.Sprintf("large-batch-%d", i)] = []byte(fmt.Sprintf("value-%d", i))
|
|
}
|
|
|
|
err := backend.SetMany(ctx, items, time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Verify random samples
|
|
for _, i := range []int{0, 25, 50, 75, 99} {
|
|
key := fmt.Sprintf("large-batch-%d", i)
|
|
value, _, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, []byte(fmt.Sprintf("value-%d", i)), value)
|
|
}
|
|
})
|
|
|
|
t.Run("GetMany100Items", func(t *testing.T) {
|
|
keys := make([]string, 100)
|
|
for i := 0; i < 100; i++ {
|
|
keys[i] = fmt.Sprintf("large-batch-%d", i)
|
|
}
|
|
|
|
results, err := backend.GetMany(ctx, keys)
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 100)
|
|
})
|
|
}
|
|
|
|
// TestPipeline_Stats tests that stats are tracked correctly with pipelining
|
|
func TestPipeline_Stats(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, backend := setupTestRedis(t)
|
|
ctx := context.Background()
|
|
|
|
// Set some items
|
|
items := map[string][]byte{
|
|
"stats-key-1": []byte("value1"),
|
|
"stats-key-2": []byte("value2"),
|
|
}
|
|
err := backend.SetMany(ctx, items, time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Get items (some exist, some don't)
|
|
keys := []string{
|
|
"stats-key-1",
|
|
"stats-key-2",
|
|
"stats-key-nonexistent",
|
|
}
|
|
results, err := backend.GetMany(ctx, keys)
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 2)
|
|
|
|
// Check stats
|
|
stats := backend.GetStats()
|
|
hits := stats["hits"].(int64)
|
|
misses := stats["misses"].(int64)
|
|
|
|
assert.Equal(t, int64(2), hits, "Should have 2 hits")
|
|
assert.Equal(t, int64(1), misses, "Should have 1 miss")
|
|
}
|
|
|
|
// BenchmarkPipeline_SetMany benchmarks SetMany with pipelining
|
|
func BenchmarkPipeline_SetMany(b *testing.B) {
|
|
mr, err := miniredis.Run()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer mr.Close()
|
|
|
|
backend, err := NewRedisBackend(&Config{
|
|
RedisAddr: mr.Addr(),
|
|
RedisPrefix: "bench:",
|
|
PoolSize: 10,
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer backend.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Prepare items
|
|
items := make(map[string][]byte)
|
|
for i := 0; i < 100; i++ {
|
|
items[fmt.Sprintf("bench-key-%d", i)] = []byte(fmt.Sprintf("bench-value-%d", i))
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = backend.SetMany(ctx, items, time.Minute)
|
|
}
|
|
}
|
|
|
|
// BenchmarkPipeline_GetMany benchmarks GetMany with pipelining
|
|
func BenchmarkPipeline_GetMany(b *testing.B) {
|
|
mr, err := miniredis.Run()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer mr.Close()
|
|
|
|
backend, err := NewRedisBackend(&Config{
|
|
RedisAddr: mr.Addr(),
|
|
RedisPrefix: "bench:",
|
|
PoolSize: 10,
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer backend.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Pre-populate cache
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("bench-key-%d", i)
|
|
value := []byte(fmt.Sprintf("bench-value-%d", i))
|
|
backend.Set(ctx, key, value, time.Hour)
|
|
}
|
|
|
|
// Prepare keys
|
|
keys := make([]string, 100)
|
|
for i := 0; i < 100; i++ {
|
|
keys[i] = fmt.Sprintf("bench-key-%d", i)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = backend.GetMany(ctx, keys)
|
|
}
|
|
}
|
|
|
|
// BenchmarkPipeline_VsSequential benchmarks pipeline vs sequential operations
|
|
func BenchmarkPipeline_VsSequential(b *testing.B) {
|
|
mr, err := miniredis.Run()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer mr.Close()
|
|
|
|
backend, err := NewRedisBackend(&Config{
|
|
RedisAddr: mr.Addr(),
|
|
RedisPrefix: "bench:",
|
|
PoolSize: 10,
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer backend.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Prepare items
|
|
items := make(map[string][]byte)
|
|
keys := make([]string, 50)
|
|
for i := 0; i < 50; i++ {
|
|
key := fmt.Sprintf("compare-key-%d", i)
|
|
keys[i] = key
|
|
items[key] = []byte(fmt.Sprintf("compare-value-%d", i))
|
|
}
|
|
|
|
b.Run("Pipelined-Set", func(b *testing.B) {
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = backend.SetMany(ctx, items, time.Minute)
|
|
}
|
|
})
|
|
|
|
b.Run("Sequential-Set", func(b *testing.B) {
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for key, value := range items {
|
|
_ = backend.Set(ctx, key, value, time.Minute)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Pre-populate for get benchmarks
|
|
_ = backend.SetMany(ctx, items, time.Hour)
|
|
|
|
b.Run("Pipelined-Get", func(b *testing.B) {
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = backend.GetMany(ctx, keys)
|
|
}
|
|
})
|
|
|
|
b.Run("Sequential-Get", func(b *testing.B) {
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for _, key := range keys {
|
|
_, _, _, _ = backend.Get(ctx, key)
|
|
}
|
|
}
|
|
})
|
|
}
|