mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
6efb78b7a8
* Smarter approach to the cookies - Single maxCookieSize = 1400 constant with clear documentation - Combined cookie storage for ~40-45% size reduction - Backward compatible migration from legacy cookies * Tuneup the code.
1491 lines
34 KiB
Go
1491 lines
34 KiB
Go
//go:build !yaegi
|
|
|
|
package backends
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// mockBackend is a simple mock implementation of CacheBackend for testing
|
|
type mockBackend struct {
|
|
pingError error
|
|
data map[string]mockEntry
|
|
stats map[string]interface{}
|
|
mu sync.RWMutex
|
|
getCalls atomic.Int32
|
|
setCalls atomic.Int32
|
|
deleteCalls atomic.Int32
|
|
failSet bool
|
|
failGet bool
|
|
failDelete bool
|
|
failClear bool
|
|
failPing bool
|
|
}
|
|
|
|
type mockEntry struct {
|
|
expiresAt time.Time
|
|
value []byte
|
|
}
|
|
|
|
// mockBatchBackend extends mockBackend with batch operations
|
|
type mockBatchBackend struct {
|
|
*mockBackend
|
|
getManyError error
|
|
setManyError error
|
|
}
|
|
|
|
func newMockBackend() *mockBackend {
|
|
return &mockBackend{
|
|
data: make(map[string]mockEntry),
|
|
stats: map[string]interface{}{
|
|
"hits": int64(0),
|
|
"misses": int64(0),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newMockBatchBackend() *mockBatchBackend {
|
|
return &mockBatchBackend{
|
|
mockBackend: newMockBackend(),
|
|
}
|
|
}
|
|
|
|
func (m *mockBatchBackend) GetMany(ctx context.Context, keys []string) (map[string][]byte, error) {
|
|
if m.getManyError != nil {
|
|
return nil, m.getManyError
|
|
}
|
|
|
|
results := make(map[string][]byte)
|
|
for _, key := range keys {
|
|
value, _, exists, err := m.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
results[key] = value
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (m *mockBatchBackend) SetMany(ctx context.Context, items map[string][]byte, ttl time.Duration) error {
|
|
if m.setManyError != nil {
|
|
return m.setManyError
|
|
}
|
|
|
|
for key, value := range items {
|
|
if err := m.Set(ctx, key, value, ttl); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackend) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
|
m.setCalls.Add(1)
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.failSet {
|
|
return errors.New("mock set error")
|
|
}
|
|
|
|
expiresAt := time.Now().Add(ttl)
|
|
if ttl == 0 {
|
|
expiresAt = time.Now().Add(24 * time.Hour)
|
|
}
|
|
|
|
m.data[key] = mockEntry{
|
|
value: value,
|
|
expiresAt: expiresAt,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackend) Get(ctx context.Context, key string) ([]byte, time.Duration, bool, error) {
|
|
m.getCalls.Add(1)
|
|
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
if m.failGet {
|
|
return nil, 0, false, errors.New("mock get error")
|
|
}
|
|
|
|
entry, exists := m.data[key]
|
|
if !exists {
|
|
return nil, 0, false, nil
|
|
}
|
|
|
|
// Check expiration
|
|
if time.Now().After(entry.expiresAt) {
|
|
return nil, 0, false, nil
|
|
}
|
|
|
|
ttl := time.Until(entry.expiresAt)
|
|
return entry.value, ttl, true, nil
|
|
}
|
|
|
|
func (m *mockBackend) Delete(ctx context.Context, key string) (bool, error) {
|
|
m.deleteCalls.Add(1)
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.failDelete {
|
|
return false, errors.New("mock delete error")
|
|
}
|
|
|
|
_, existed := m.data[key]
|
|
delete(m.data, key)
|
|
return existed, nil
|
|
}
|
|
|
|
func (m *mockBackend) Exists(ctx context.Context, key string) (bool, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
entry, exists := m.data[key]
|
|
if !exists {
|
|
return false, nil
|
|
}
|
|
|
|
// Check expiration
|
|
if time.Now().After(entry.expiresAt) {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (m *mockBackend) Clear(ctx context.Context) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.failClear {
|
|
return errors.New("mock clear error")
|
|
}
|
|
|
|
m.data = make(map[string]mockEntry)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackend) GetStats() map[string]interface{} {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.stats
|
|
}
|
|
|
|
func (m *mockBackend) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackend) Ping(ctx context.Context) error {
|
|
if m.failPing {
|
|
if m.pingError != nil {
|
|
return m.pingError
|
|
}
|
|
return errors.New("mock ping error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Constructor Tests
|
|
|
|
func TestNewHybridBackend_Success(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, hybrid)
|
|
|
|
// Verify default values
|
|
assert.NotNil(t, hybrid.logger)
|
|
assert.NotNil(t, hybrid.asyncWriteBuffer)
|
|
assert.NotNil(t, hybrid.syncWriteCacheTypes)
|
|
|
|
hybrid.Close()
|
|
}
|
|
|
|
func TestNewHybridBackend_NilConfig(t *testing.T) {
|
|
hybrid, err := NewHybridBackend(nil)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, hybrid)
|
|
assert.Contains(t, err.Error(), "config is required")
|
|
}
|
|
|
|
func TestNewHybridBackend_NilPrimary(t *testing.T) {
|
|
config := &HybridConfig{
|
|
Primary: nil,
|
|
Secondary: newMockBackend(),
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, hybrid)
|
|
assert.Contains(t, err.Error(), "primary")
|
|
}
|
|
|
|
func TestNewHybridBackend_NilSecondary(t *testing.T) {
|
|
config := &HybridConfig{
|
|
Primary: newMockBackend(),
|
|
Secondary: nil,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, hybrid)
|
|
assert.Contains(t, err.Error(), "secondary")
|
|
}
|
|
|
|
func TestNewHybridBackend_CustomLogger(t *testing.T) {
|
|
logger := &TestLogger{t: t}
|
|
config := &HybridConfig{
|
|
Primary: newMockBackend(),
|
|
Secondary: newMockBackend(),
|
|
Logger: logger,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, logger, hybrid.logger)
|
|
|
|
hybrid.Close()
|
|
}
|
|
|
|
func TestNewHybridBackend_CustomAsyncBufferSize(t *testing.T) {
|
|
config := &HybridConfig{
|
|
Primary: newMockBackend(),
|
|
Secondary: newMockBackend(),
|
|
AsyncBufferSize: 50,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 50, cap(hybrid.asyncWriteBuffer))
|
|
|
|
hybrid.Close()
|
|
}
|
|
|
|
func TestNewHybridBackend_DefaultAsyncBufferSize(t *testing.T) {
|
|
config := &HybridConfig{
|
|
Primary: newMockBackend(),
|
|
Secondary: newMockBackend(),
|
|
// AsyncBufferSize not set or <= 0
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1000, cap(hybrid.asyncWriteBuffer))
|
|
|
|
hybrid.Close()
|
|
}
|
|
|
|
func TestNewHybridBackend_CustomSyncWriteCacheTypes(t *testing.T) {
|
|
customTypes := map[string]bool{
|
|
"custom1": true,
|
|
"custom2": true,
|
|
}
|
|
|
|
config := &HybridConfig{
|
|
Primary: newMockBackend(),
|
|
Secondary: newMockBackend(),
|
|
SyncWriteCacheTypes: customTypes,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
assert.True(t, hybrid.syncWriteCacheTypes["custom1"])
|
|
assert.True(t, hybrid.syncWriteCacheTypes["custom2"])
|
|
|
|
hybrid.Close()
|
|
}
|
|
|
|
func TestNewHybridBackend_DefaultSyncWriteCacheTypes(t *testing.T) {
|
|
config := &HybridConfig{
|
|
Primary: newMockBackend(),
|
|
Secondary: newMockBackend(),
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
|
|
// Should have default critical types
|
|
assert.True(t, hybrid.syncWriteCacheTypes["blacklist"])
|
|
assert.True(t, hybrid.syncWriteCacheTypes["token"])
|
|
|
|
hybrid.Close()
|
|
}
|
|
|
|
// Basic Operations Tests
|
|
|
|
func TestHybridBackend_Set_BothSuccess(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
SyncWriteCacheTypes: map[string]bool{
|
|
"test": true, // Make writes synchronous for testing
|
|
},
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
value := []byte("test-value")
|
|
ttl := 1 * time.Minute
|
|
|
|
err = hybrid.Set(ctx, key, value, ttl)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify L1 write
|
|
assert.Equal(t, int32(1), primary.setCalls.Load())
|
|
assert.Equal(t, int64(1), hybrid.l1Writes.Load())
|
|
|
|
// Give time for sync write to complete
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Verify L2 write (sync)
|
|
assert.Equal(t, int32(1), secondary.setCalls.Load())
|
|
assert.Equal(t, int64(1), hybrid.l2Writes.Load())
|
|
}
|
|
|
|
func TestHybridBackend_Set_L1Failure(t *testing.T) {
|
|
primary := newMockBackend()
|
|
primary.failSet = true
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
err = hybrid.Set(ctx, "key1", []byte("value"), 1*time.Minute)
|
|
|
|
// Should not return error even if L1 fails (continues to L2)
|
|
assert.NoError(t, err)
|
|
assert.Greater(t, hybrid.errors.Load(), int64(0))
|
|
}
|
|
|
|
func TestHybridBackend_Set_AsyncWrite(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
SyncWriteCacheTypes: map[string]bool{
|
|
// "general" is not in sync list, so async
|
|
},
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "general:key1" // Will be async
|
|
value := []byte("test-value")
|
|
|
|
err = hybrid.Set(ctx, key, value, 1*time.Minute)
|
|
assert.NoError(t, err)
|
|
|
|
// L1 should be written immediately
|
|
assert.Equal(t, int32(1), primary.setCalls.Load())
|
|
|
|
// Wait for async worker to process
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// L2 should eventually be written
|
|
assert.Equal(t, int32(1), secondary.setCalls.Load())
|
|
}
|
|
|
|
func TestHybridBackend_Get_L1Hit(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
value := []byte("test-value")
|
|
|
|
// Populate L1 directly
|
|
primary.Set(ctx, key, value, 1*time.Minute)
|
|
|
|
// Get should hit L1
|
|
retrieved, _, exists, err := hybrid.Get(ctx, key)
|
|
assert.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, value, retrieved)
|
|
|
|
// L1 hit counter should increment
|
|
assert.Equal(t, int64(1), hybrid.l1Hits.Load())
|
|
|
|
// L2 should not be queried
|
|
assert.Equal(t, int32(0), secondary.getCalls.Load())
|
|
}
|
|
|
|
func TestHybridBackend_Get_L2Hit(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
value := []byte("test-value")
|
|
|
|
// Populate L2 only
|
|
secondary.Set(ctx, key, value, 1*time.Minute)
|
|
|
|
// Get should miss L1, hit L2
|
|
retrieved, _, exists, err := hybrid.Get(ctx, key)
|
|
assert.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, value, retrieved)
|
|
|
|
// L2 hit counter should increment
|
|
assert.Equal(t, int64(1), hybrid.l2Hits.Load())
|
|
|
|
// L1 should be populated in background
|
|
time.Sleep(150 * time.Millisecond)
|
|
_, _, existsInL1, _ := primary.Get(ctx, key)
|
|
assert.True(t, existsInL1, "L1 should be populated from L2")
|
|
}
|
|
|
|
func TestHybridBackend_Get_Miss(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get non-existent key
|
|
_, _, exists, err := hybrid.Get(ctx, "non-existent")
|
|
assert.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
// Miss counter should increment
|
|
assert.Equal(t, int64(1), hybrid.misses.Load())
|
|
}
|
|
|
|
func TestHybridBackend_Delete_BothCaches(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
|
|
// Populate both caches
|
|
primary.Set(ctx, key, []byte("value"), 1*time.Minute)
|
|
secondary.Set(ctx, key, []byte("value"), 1*time.Minute)
|
|
|
|
// Delete
|
|
deleted, err := hybrid.Delete(ctx, key)
|
|
assert.NoError(t, err)
|
|
assert.True(t, deleted)
|
|
|
|
// Both should be deleted
|
|
assert.Equal(t, int32(1), primary.deleteCalls.Load())
|
|
assert.Equal(t, int32(1), secondary.deleteCalls.Load())
|
|
}
|
|
|
|
func TestHybridBackend_Exists_L1(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
|
|
// Populate L1
|
|
primary.Set(ctx, key, []byte("value"), 1*time.Minute)
|
|
|
|
exists, err := hybrid.Exists(ctx, key)
|
|
assert.NoError(t, err)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestHybridBackend_Exists_L2(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
|
|
// Populate L2 only
|
|
secondary.Set(ctx, key, []byte("value"), 1*time.Minute)
|
|
|
|
exists, err := hybrid.Exists(ctx, key)
|
|
assert.NoError(t, err)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestHybridBackend_Clear_BothCaches(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Populate both
|
|
primary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
secondary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
|
|
err = hybrid.Clear(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Both should be cleared
|
|
exists1, _ := primary.Exists(ctx, "key1")
|
|
exists2, _ := secondary.Exists(ctx, "key2")
|
|
assert.False(t, exists1)
|
|
assert.False(t, exists2)
|
|
}
|
|
|
|
// Fallback Mode Tests
|
|
|
|
func TestHybridBackend_FallbackMode_OnL2Errors(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
secondary.failSet = true
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
SyncWriteCacheTypes: map[string]bool{"test": true},
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Multiple failures should trigger fallback mode
|
|
for i := 0; i < 3; i++ {
|
|
hybrid.Set(ctx, fmt.Sprintf("test:key%d", i), []byte("value"), 1*time.Minute)
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
// Should eventually enter fallback mode
|
|
time.Sleep(50 * time.Millisecond)
|
|
assert.True(t, hybrid.fallbackMode.Load(), "Should enter fallback mode after L2 errors")
|
|
}
|
|
|
|
func TestHybridBackend_FallbackMode_SkipsL2(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
// Manually enable fallback mode
|
|
hybrid.fallbackMode.Store(true)
|
|
|
|
ctx := context.Background()
|
|
key := "test:key1"
|
|
value := []byte("test-value")
|
|
|
|
err = hybrid.Set(ctx, key, value, 1*time.Minute)
|
|
assert.NoError(t, err)
|
|
|
|
// L1 should be written
|
|
assert.Equal(t, int32(1), primary.setCalls.Load())
|
|
|
|
// L2 should be skipped
|
|
time.Sleep(50 * time.Millisecond)
|
|
assert.Equal(t, int32(0), secondary.setCalls.Load())
|
|
}
|
|
|
|
func TestHybridBackend_FallbackMode_Get(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
// Enable fallback mode
|
|
hybrid.fallbackMode.Store(true)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Populate L2
|
|
secondary.Set(ctx, "key1", []byte("value"), 1*time.Minute)
|
|
|
|
// Get should only check L1 in fallback mode
|
|
_, _, exists, err := hybrid.Get(ctx, "key1")
|
|
assert.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
// Miss should be recorded
|
|
assert.Equal(t, int64(1), hybrid.misses.Load())
|
|
|
|
// L2 should not be queried
|
|
assert.Equal(t, int32(0), secondary.getCalls.Load())
|
|
}
|
|
|
|
// Health Monitoring Tests
|
|
|
|
func TestHybridBackend_Ping_BothHealthy(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
err = hybrid.Ping(ctx)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestHybridBackend_Ping_L1Failure(t *testing.T) {
|
|
primary := newMockBackend()
|
|
primary.failPing = true
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
err = hybrid.Ping(ctx)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "L1")
|
|
}
|
|
|
|
func TestHybridBackend_Ping_L2Failure(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
secondary.failPing = true
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
err = hybrid.Ping(ctx)
|
|
|
|
// Should not return error (L2 failure is tolerated)
|
|
assert.NoError(t, err)
|
|
|
|
// But should record error
|
|
lastErr := hybrid.lastL2Error.Load()
|
|
assert.NotNil(t, lastErr)
|
|
}
|
|
|
|
func TestHybridBackend_Ping_RecoverFromFallback(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
secondary.failPing = true
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// First ping fails L2, enters fallback
|
|
hybrid.Ping(ctx)
|
|
assert.True(t, hybrid.fallbackMode.Load())
|
|
|
|
// Fix L2
|
|
secondary.failPing = false
|
|
|
|
// Second ping succeeds, exits fallback
|
|
hybrid.Ping(ctx)
|
|
time.Sleep(10 * time.Millisecond)
|
|
assert.False(t, hybrid.fallbackMode.Load())
|
|
}
|
|
|
|
// GetStats Tests
|
|
|
|
func TestHybridBackend_GetStats(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Generate some activity
|
|
hybrid.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
hybrid.Get(ctx, "key1") // L1 hit
|
|
hybrid.Get(ctx, "key-miss") // miss
|
|
|
|
stats := hybrid.GetStats()
|
|
assert.NotNil(t, stats)
|
|
|
|
// Check required fields
|
|
assert.Equal(t, TypeHybrid, stats["type"])
|
|
assert.Contains(t, stats, "l1_hits")
|
|
assert.Contains(t, stats, "l2_hits")
|
|
assert.Contains(t, stats, "misses")
|
|
assert.Contains(t, stats, "total")
|
|
assert.Contains(t, stats, "l1_writes")
|
|
assert.Contains(t, stats, "l2_writes")
|
|
assert.Contains(t, stats, "errors")
|
|
assert.Contains(t, stats, "fallback_mode")
|
|
assert.Contains(t, stats, "l1_stats")
|
|
assert.Contains(t, stats, "l2_stats")
|
|
}
|
|
|
|
func TestHybridBackend_GetStats_HitRates(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
// Record some hits
|
|
hybrid.l1Hits.Store(10)
|
|
hybrid.l2Hits.Store(5)
|
|
hybrid.misses.Store(5)
|
|
|
|
stats := hybrid.GetStats()
|
|
|
|
// Should calculate hit rates
|
|
assert.Contains(t, stats, "l1_hit_rate")
|
|
assert.Contains(t, stats, "l2_hit_rate")
|
|
assert.Contains(t, stats, "overall_hit_rate")
|
|
|
|
// Check values
|
|
assert.InDelta(t, 0.5, stats["l1_hit_rate"], 0.01)
|
|
assert.InDelta(t, 0.25, stats["l2_hit_rate"], 0.01)
|
|
assert.InDelta(t, 0.75, stats["overall_hit_rate"], 0.01)
|
|
}
|
|
|
|
func TestHybridBackend_GetStats_LastL2Error(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
// Record an L2 error
|
|
errorTime := time.Now()
|
|
hybrid.lastL2Error.Store(errorTime)
|
|
|
|
stats := hybrid.GetStats()
|
|
|
|
assert.Contains(t, stats, "last_l2_error")
|
|
assert.Contains(t, stats, "seconds_since_l2_error")
|
|
}
|
|
|
|
// GetMany/SetMany Tests
|
|
|
|
func TestHybridBackend_GetMany_L1Hits(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Populate L1
|
|
primary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
primary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2"})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 2)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
assert.Equal(t, []byte("value2"), results["key2"])
|
|
|
|
// Should be L1 hits
|
|
assert.Equal(t, int64(2), hybrid.l1Hits.Load())
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_EmptyKeys(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
results, err := hybrid.GetMany(ctx, []string{})
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, results)
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_L2Fallback(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Populate L2 only
|
|
secondary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
secondary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2"})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 2)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
assert.Equal(t, []byte("value2"), results["key2"])
|
|
|
|
// Should be L2 misses (L1 was empty)
|
|
assert.Equal(t, int64(0), hybrid.l1Hits.Load())
|
|
|
|
// Give async L1 population time to complete
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify L1 was populated from L2 hits
|
|
val1, _, exists1, _ := primary.Get(ctx, "key1")
|
|
assert.True(t, exists1)
|
|
assert.Equal(t, []byte("value1"), val1)
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_MixedL1L2(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// key1 in L1 only
|
|
primary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
|
|
// key2 in L2 only
|
|
secondary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
|
|
// key3 in both (L1 should win)
|
|
primary.Set(ctx, "key3", []byte("value3-l1"), 1*time.Minute)
|
|
secondary.Set(ctx, "key3", []byte("value3-l2"), 1*time.Minute)
|
|
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2", "key3"})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 3)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
assert.Equal(t, []byte("value2"), results["key2"])
|
|
assert.Equal(t, []byte("value3-l1"), results["key3"]) // L1 wins
|
|
|
|
// Should have 2 L1 hits (key1, key3)
|
|
assert.Equal(t, int64(2), hybrid.l1Hits.Load())
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_FallbackMode(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Enable fallback mode
|
|
hybrid.fallbackMode.Store(true)
|
|
|
|
// Populate L1 and L2
|
|
primary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
secondary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
|
|
// In fallback mode, should only check L1
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2"})
|
|
assert.NoError(t, err)
|
|
|
|
// Should only find key1 (from L1)
|
|
assert.Len(t, results, 1)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
assert.NotContains(t, results, "key2") // L2 not checked
|
|
|
|
assert.Equal(t, int64(1), hybrid.l1Hits.Load())
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_L2Error(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
secondary.failGet = true // Force L2 errors
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// key1 in L1, key2 needs L2 (but will error)
|
|
primary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2"})
|
|
|
|
// Should still succeed with L1 hits even when L2 errors
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 1)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
|
|
// Note: Individual Get errors may not immediately trigger fallback mode
|
|
// The circuit breaker needs multiple consecutive errors
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_PartialL2Results(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Only key2 exists in L2
|
|
secondary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
|
|
// Request 3 keys, only one exists
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2", "key3"})
|
|
assert.NoError(t, err)
|
|
|
|
// Should only have key2
|
|
assert.Len(t, results, 1)
|
|
assert.Equal(t, []byte("value2"), results["key2"])
|
|
assert.NotContains(t, results, "key1")
|
|
assert.NotContains(t, results, "key3")
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_WithBatchBackend(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBatchBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Populate L2 with batch backend
|
|
secondary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
secondary.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
|
secondary.Set(ctx, "key3", []byte("value3"), 1*time.Minute)
|
|
|
|
// GetMany should use batch operation
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2", "key3"})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 3)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
assert.Equal(t, []byte("value2"), results["key2"])
|
|
assert.Equal(t, []byte("value3"), results["key3"])
|
|
|
|
// Verify L1 populated asynchronously
|
|
time.Sleep(50 * time.Millisecond)
|
|
val1, _, exists1, _ := primary.Get(ctx, "key1")
|
|
assert.True(t, exists1)
|
|
assert.Equal(t, []byte("value1"), val1)
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_BatchBackendError(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBatchBackend()
|
|
secondary.getManyError = errors.New("batch operation failed")
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// key1 in L1
|
|
primary.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
|
|
|
// GetMany should handle batch error gracefully
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2"})
|
|
|
|
// Should return L1 results even though L2 batch failed
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 1)
|
|
assert.Equal(t, []byte("value1"), results["key1"])
|
|
|
|
// Batch error should trigger fallback mode
|
|
time.Sleep(50 * time.Millisecond)
|
|
assert.True(t, hybrid.fallbackMode.Load())
|
|
}
|
|
|
|
func TestHybridBackend_GetMany_MixedBatchResults(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBatchBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// key1 and key2 in L1
|
|
primary.Set(ctx, "key1", []byte("value1-l1"), 1*time.Minute)
|
|
primary.Set(ctx, "key2", []byte("value2-l1"), 1*time.Minute)
|
|
|
|
// key3 and key4 in L2 (batch backend)
|
|
secondary.Set(ctx, "key3", []byte("value3-l2"), 1*time.Minute)
|
|
secondary.Set(ctx, "key4", []byte("value4-l2"), 1*time.Minute)
|
|
|
|
// GetMany with mixed L1/L2 hits via batch
|
|
results, err := hybrid.GetMany(ctx, []string{"key1", "key2", "key3", "key4"})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, results, 4)
|
|
|
|
// L1 results
|
|
assert.Equal(t, []byte("value1-l1"), results["key1"])
|
|
assert.Equal(t, []byte("value2-l1"), results["key2"])
|
|
|
|
// L2 batch results
|
|
assert.Equal(t, []byte("value3-l2"), results["key3"])
|
|
assert.Equal(t, []byte("value4-l2"), results["key4"])
|
|
|
|
// Should have 2 L1 hits
|
|
assert.Equal(t, int64(2), hybrid.l1Hits.Load())
|
|
}
|
|
|
|
func TestHybridBackend_SetMany_Success(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
SyncWriteCacheTypes: map[string]bool{"test": true},
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
items := map[string][]byte{
|
|
"test:key1": []byte("value1"),
|
|
"test:key2": []byte("value2"),
|
|
}
|
|
|
|
err = hybrid.SetMany(ctx, items, 1*time.Minute)
|
|
assert.NoError(t, err)
|
|
|
|
// L1 should have both
|
|
assert.Equal(t, int32(2), primary.setCalls.Load())
|
|
|
|
// L2 should have both (sync writes)
|
|
time.Sleep(50 * time.Millisecond)
|
|
assert.Equal(t, int32(2), secondary.setCalls.Load())
|
|
}
|
|
|
|
func TestHybridBackend_SetMany_EmptyItems(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
err = hybrid.SetMany(ctx, map[string][]byte{}, 1*time.Minute)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Close Tests
|
|
|
|
func TestHybridBackend_Close(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
|
|
err = hybrid.Close()
|
|
assert.NoError(t, err)
|
|
|
|
// Context should be canceled
|
|
select {
|
|
case <-hybrid.ctx.Done():
|
|
// Good
|
|
default:
|
|
t.Error("Context should be canceled after Close")
|
|
}
|
|
}
|
|
|
|
// Helper Function Tests
|
|
|
|
func TestExtractCacheType(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
tests := []struct {
|
|
key string
|
|
expected string
|
|
}{
|
|
{"blacklist:token123", "blacklist"},
|
|
{"token:access123", "token"},
|
|
{"metadata:provider", "metadata"},
|
|
{"jwk:key1234567", "jwk"}, // Needs to be > 10 chars
|
|
{"session:sess1234", "session"},
|
|
{"introspect:tok123", "introspection"},
|
|
{"other:key", "general"},
|
|
{"short", "general"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.key, func(t *testing.T) {
|
|
result := hybrid.extractCacheType(tt.key)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContains(t *testing.T) {
|
|
tests := []struct {
|
|
s string
|
|
substr string
|
|
expected bool
|
|
}{
|
|
{"blacklist", "black", true},
|
|
{"blacklist", "list", true},
|
|
{"blacklist", "xyz", false},
|
|
{"TOKEN", "token", true}, // case insensitive
|
|
{"short", "verylongstring", false},
|
|
{"", "any", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%s-%s", tt.s, tt.substr), func(t *testing.T) {
|
|
result := contains(tt.s, tt.substr)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToLower(t *testing.T) {
|
|
tests := []struct {
|
|
input byte
|
|
expected byte
|
|
}{
|
|
{'A', 'a'},
|
|
{'Z', 'z'},
|
|
{'a', 'a'},
|
|
{'z', 'z'},
|
|
{'0', '0'},
|
|
{'!', '!'},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.input), func(t *testing.T) {
|
|
result := toLower(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Logger Tests
|
|
|
|
func TestDefaultLogger_Methods(t *testing.T) {
|
|
// Create a default logger using the test logger
|
|
testLogger := &TestLogger{t: t}
|
|
|
|
// These should not panic
|
|
testLogger.Debugf("debug %s", "message")
|
|
testLogger.Infof("info %s", "message")
|
|
testLogger.Warnf("warn %s", "message")
|
|
testLogger.Errorf("error %s", "message")
|
|
}
|
|
|
|
// Async Write Worker Tests
|
|
|
|
func TestHybridBackend_AsyncWriteWorker_ProcessesWrites(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
// No sync types - all writes are async
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Queue multiple async writes
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("async:key%d", i)
|
|
value := []byte(fmt.Sprintf("value%d", i))
|
|
err := hybrid.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Wait for async worker to process
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// All should be written to L2
|
|
assert.Equal(t, int32(5), secondary.setCalls.Load())
|
|
}
|
|
|
|
func TestHybridBackend_AsyncWriteWorker_BufferFull(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
AsyncBufferSize: 2, // Very small buffer
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Try to overflow buffer
|
|
for i := 0; i < 10; i++ {
|
|
key := fmt.Sprintf("async:key%d", i)
|
|
value := []byte(fmt.Sprintf("value%d", i))
|
|
hybrid.Set(ctx, key, value, 1*time.Minute)
|
|
}
|
|
|
|
// Some writes should be dropped (errors incremented)
|
|
time.Sleep(50 * time.Millisecond)
|
|
errors := hybrid.errors.Load()
|
|
// May have errors from buffer overflow
|
|
_ = errors
|
|
}
|
|
|
|
// RecordL2Error Tests
|
|
|
|
func TestHybridBackend_RecordL2Error_EntersFallbackMode(t *testing.T) {
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
defer hybrid.Close()
|
|
|
|
// Record error
|
|
hybrid.recordL2Error()
|
|
|
|
// Should have timestamp
|
|
lastErr := hybrid.lastL2Error.Load()
|
|
assert.NotNil(t, lastErr)
|
|
|
|
// Record another error immediately (within 1 second)
|
|
hybrid.recordL2Error()
|
|
|
|
// Should enter fallback mode
|
|
assert.True(t, hybrid.fallbackMode.Load())
|
|
}
|