mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
improvements mid may 2025 (#24)
* General improvements and bug fixes. * Improve tests coverage. * fixup! Improve tests coverage. * Update README.md with latest changes. * Fix the uint32 * Resolve issue with race condition for logging. * fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * Fix the test of the rate limiter * Add default ratelimit.json file * Update dependencies. * Significant refactor. * fixup! Significant refactor. * fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LRUCacheTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestLRUCacheTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LRUCacheTestSuite))
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestNewLRUCache() {
|
||||
cache := NewLRUCache(100, 1024*1024) // 100 entries, 1MB
|
||||
|
||||
assert.NotNil(suite.T(), cache)
|
||||
assert.Equal(suite.T(), 0, cache.Len())
|
||||
assert.Equal(suite.T(), int64(0), cache.Size())
|
||||
assert.NotNil(suite.T(), cache.entries)
|
||||
assert.NotNil(suite.T(), cache.evictList)
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestGetSet() {
|
||||
cache := NewLRUCache(10, 1024)
|
||||
|
||||
// Test Set and Get
|
||||
cache.Set("key1", "value1", 10)
|
||||
val, exists := cache.Get("key1")
|
||||
assert.True(suite.T(), exists)
|
||||
assert.Equal(suite.T(), "value1", val)
|
||||
|
||||
// Test Get non-existent key
|
||||
val, exists = cache.Get("nonexistent")
|
||||
assert.False(suite.T(), exists)
|
||||
assert.Nil(suite.T(), val)
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestUpdateExisting() {
|
||||
cache := NewLRUCache(10, 1024)
|
||||
|
||||
// Set initial value
|
||||
cache.Set("key1", "value1", 10)
|
||||
assert.Equal(suite.T(), int64(10), cache.Size())
|
||||
|
||||
// Update with new value and size
|
||||
cache.Set("key1", "value2", 20)
|
||||
val, exists := cache.Get("key1")
|
||||
assert.True(suite.T(), exists)
|
||||
assert.Equal(suite.T(), "value2", val)
|
||||
assert.Equal(suite.T(), int64(20), cache.Size())
|
||||
assert.Equal(suite.T(), 1, cache.Len())
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestEvictionByCount() {
|
||||
cache := NewLRUCache(3, 1024) // Max 3 entries
|
||||
|
||||
// Add 4 entries
|
||||
cache.Set("key1", "value1", 10)
|
||||
cache.Set("key2", "value2", 10)
|
||||
cache.Set("key3", "value3", 10)
|
||||
cache.Set("key4", "value4", 10)
|
||||
|
||||
// Should have evicted key1
|
||||
assert.Equal(suite.T(), 3, cache.Len())
|
||||
_, exists := cache.Get("key1")
|
||||
assert.False(suite.T(), exists)
|
||||
|
||||
// key2, key3, key4 should still exist
|
||||
_, exists = cache.Get("key2")
|
||||
assert.True(suite.T(), exists)
|
||||
_, exists = cache.Get("key3")
|
||||
assert.True(suite.T(), exists)
|
||||
_, exists = cache.Get("key4")
|
||||
assert.True(suite.T(), exists)
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestEvictionBySize() {
|
||||
cache := NewLRUCache(10, 100) // Max 100 bytes
|
||||
|
||||
// Add entries that exceed size limit
|
||||
cache.Set("key1", "value1", 40)
|
||||
cache.Set("key2", "value2", 40)
|
||||
cache.Set("key3", "value3", 40) // Total would be 120, should evict key1
|
||||
|
||||
assert.Equal(suite.T(), 2, cache.Len())
|
||||
assert.LessOrEqual(suite.T(), cache.Size(), int64(100))
|
||||
|
||||
// key1 should be evicted
|
||||
_, exists := cache.Get("key1")
|
||||
assert.False(suite.T(), exists)
|
||||
|
||||
// key2 and key3 should exist
|
||||
_, exists = cache.Get("key2")
|
||||
assert.True(suite.T(), exists)
|
||||
_, exists = cache.Get("key3")
|
||||
assert.True(suite.T(), exists)
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestLRUOrder() {
|
||||
cache := NewLRUCache(3, 1024)
|
||||
|
||||
// Add 3 entries
|
||||
cache.Set("key1", "value1", 10)
|
||||
cache.Set("key2", "value2", 10)
|
||||
cache.Set("key3", "value3", 10)
|
||||
|
||||
// Access key1 to make it most recently used
|
||||
cache.Get("key1")
|
||||
|
||||
// Add a new entry, should evict key2 (least recently used)
|
||||
cache.Set("key4", "value4", 10)
|
||||
|
||||
_, exists := cache.Get("key1")
|
||||
assert.True(suite.T(), exists) // Should exist (recently accessed)
|
||||
_, exists = cache.Get("key2")
|
||||
assert.False(suite.T(), exists) // Should be evicted
|
||||
_, exists = cache.Get("key3")
|
||||
assert.True(suite.T(), exists) // Should exist
|
||||
_, exists = cache.Get("key4")
|
||||
assert.True(suite.T(), exists) // Should exist (newest)
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestDelete() {
|
||||
cache := NewLRUCache(10, 1024)
|
||||
|
||||
cache.Set("key1", "value1", 10)
|
||||
cache.Set("key2", "value2", 20)
|
||||
|
||||
assert.Equal(suite.T(), 2, cache.Len())
|
||||
assert.Equal(suite.T(), int64(30), cache.Size())
|
||||
|
||||
// Delete key1
|
||||
cache.Delete("key1")
|
||||
assert.Equal(suite.T(), 1, cache.Len())
|
||||
assert.Equal(suite.T(), int64(20), cache.Size())
|
||||
|
||||
_, exists := cache.Get("key1")
|
||||
assert.False(suite.T(), exists)
|
||||
|
||||
// Delete non-existent key should be safe
|
||||
cache.Delete("nonexistent")
|
||||
assert.Equal(suite.T(), 1, cache.Len())
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestClear() {
|
||||
cache := NewLRUCache(10, 1024)
|
||||
|
||||
// Add multiple entries
|
||||
for i := 0; i < 5; i++ {
|
||||
cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), 10)
|
||||
}
|
||||
|
||||
assert.Equal(suite.T(), 5, cache.Len())
|
||||
assert.Equal(suite.T(), int64(50), cache.Size())
|
||||
|
||||
// Clear cache
|
||||
cache.Clear()
|
||||
assert.Equal(suite.T(), 0, cache.Len())
|
||||
assert.Equal(suite.T(), int64(0), cache.Size())
|
||||
|
||||
// Should be able to add new entries
|
||||
cache.Set("newkey", "newvalue", 10)
|
||||
assert.Equal(suite.T(), 1, cache.Len())
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestCleanupExpired() {
|
||||
cache := NewLRUCache(10, 1024)
|
||||
|
||||
// Add entries
|
||||
cache.Set("key1", "value1", 10)
|
||||
cache.Set("key2", "value2", 10)
|
||||
|
||||
// Sleep to make entries older
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Add a new entry
|
||||
cache.Set("key3", "value3", 10)
|
||||
|
||||
// Cleanup entries older than 50ms
|
||||
removed := cache.CleanupExpired(50 * time.Millisecond)
|
||||
assert.Equal(suite.T(), 2, removed) // key1 and key2 should be removed
|
||||
|
||||
assert.Equal(suite.T(), 1, cache.Len())
|
||||
_, exists := cache.Get("key3")
|
||||
assert.True(suite.T(), exists) // key3 should still exist
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestGetStats() {
|
||||
cache := NewLRUCache(10, 1000)
|
||||
|
||||
cache.Set("key1", "value1", 100)
|
||||
cache.Set("key2", "value2", 200)
|
||||
|
||||
stats := cache.GetStats()
|
||||
|
||||
assert.Equal(suite.T(), 2, stats["entries"])
|
||||
assert.Equal(suite.T(), int64(300), stats["size_bytes"])
|
||||
assert.Equal(suite.T(), 10, stats["max_entries"])
|
||||
assert.Equal(suite.T(), int64(1000), stats["max_size"])
|
||||
assert.Equal(suite.T(), float64(30), stats["fill_percent"])
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestConcurrentAccess() {
|
||||
cache := NewLRUCache(100, 10240)
|
||||
numGoroutines := 10
|
||||
numOperations := 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Run concurrent operations
|
||||
for g := 0; g < numGoroutines; g++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i := 0; i < numOperations; i++ {
|
||||
key := fmt.Sprintf("key-%d-%d", goroutineID, i)
|
||||
value := fmt.Sprintf("value-%d-%d", goroutineID, i)
|
||||
|
||||
// Mix of operations
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
cache.Set(key, value, 10)
|
||||
case 1:
|
||||
cache.Get(key)
|
||||
case 2:
|
||||
cache.Delete(fmt.Sprintf("key-%d-%d", goroutineID, i-1))
|
||||
case 3:
|
||||
cache.Len()
|
||||
cache.Size()
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Cache should be in a consistent state
|
||||
assert.LessOrEqual(suite.T(), cache.Len(), 100)
|
||||
assert.GreaterOrEqual(suite.T(), cache.Len(), 0)
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestConcurrentEviction() {
|
||||
cache := NewLRUCache(10, 1024) // Small cache to trigger evictions
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 50
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for g := 0; g < numGoroutines; g++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
key := fmt.Sprintf("key-%d-%d", id, i)
|
||||
cache.Set(key, "value", 10)
|
||||
time.Sleep(time.Microsecond) // Small delay to interleave operations
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Should never exceed max entries
|
||||
assert.LessOrEqual(suite.T(), cache.Len(), 10)
|
||||
assert.LessOrEqual(suite.T(), cache.Size(), int64(1024))
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestRaceCondition() {
|
||||
// This test specifically checks for race conditions
|
||||
cache := NewLRUCache(100, 10240)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var setCount, getCount, deleteCount int32
|
||||
|
||||
// Writer goroutines
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
key := fmt.Sprintf("key%d", rand.Intn(50))
|
||||
cache.Set(key, "value", 10)
|
||||
atomic.AddInt32(&setCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Reader goroutines
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
key := fmt.Sprintf("key%d", rand.Intn(50))
|
||||
cache.Get(key)
|
||||
atomic.AddInt32(&getCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Deleter goroutines
|
||||
for i := 0; i < 2; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 50; j++ {
|
||||
key := fmt.Sprintf("key%d", rand.Intn(50))
|
||||
cache.Delete(key)
|
||||
atomic.AddInt32(&deleteCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Stats reader
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
_ = cache.GetStats()
|
||||
time.Sleep(time.Microsecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// Cleanup goroutine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cache.CleanupExpired(5 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify operations completed
|
||||
assert.Equal(suite.T(), int32(500), atomic.LoadInt32(&setCount))
|
||||
assert.Equal(suite.T(), int32(500), atomic.LoadInt32(&getCount))
|
||||
assert.Equal(suite.T(), int32(100), atomic.LoadInt32(&deleteCount))
|
||||
}
|
||||
|
||||
func (suite *LRUCacheTestSuite) TestEdgeCases() {
|
||||
// Zero size cache
|
||||
cache := NewLRUCache(0, 0)
|
||||
cache.Set("key", "value", 10)
|
||||
assert.Equal(suite.T(), 0, cache.Len()) // Should not store anything
|
||||
|
||||
// Negative values should be handled
|
||||
cache = NewLRUCache(-1, -1)
|
||||
cache.Set("key", "value", 10)
|
||||
assert.Equal(suite.T(), 0, cache.Len())
|
||||
|
||||
// Very large size
|
||||
cache = NewLRUCache(1, 1)
|
||||
cache.Set("key", "value", 1000) // Size exceeds limit
|
||||
assert.Equal(suite.T(), 0, cache.Len()) // Should evict immediately
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkLRUCacheSet(b *testing.B) {
|
||||
cache := NewLRUCache(1000, 1024*1024)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
key := fmt.Sprintf("key%d", i)
|
||||
cache.Set(key, "value", 10)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCacheGet(b *testing.B) {
|
||||
cache := NewLRUCache(1000, 1024*1024)
|
||||
|
||||
// Pre-populate cache
|
||||
for i := 0; i < 1000; i++ {
|
||||
key := fmt.Sprintf("key%d", i)
|
||||
cache.Set(key, "value", 10)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
key := fmt.Sprintf("key%d", i%1000)
|
||||
cache.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCacheConcurrent(b *testing.B) {
|
||||
cache := NewLRUCache(1000, 1024*1024)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
key := fmt.Sprintf("key%d", i)
|
||||
if i%2 == 0 {
|
||||
cache.Set(key, "value", 10)
|
||||
} else {
|
||||
cache.Get(key)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user