mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
The count-based evictOldest used an O(n squared) selection sort with a candidate cap over an unordered map range, so it could evict the wrong entries. It now collects all entries and sorts by expiry, matching the memory-based evictor. A non-positive CACHE_TTL behaved differently per backend: the standard memory cache panicked, Redis stored a permanent key, and the LRU cache expired entries immediately. Each backend now clamps a non-positive TTL to a shared default at its Set boundary. The nil-logger fallback in the Redis wrapper now uses the constructor so it cannot panic.
192 lines
4.6 KiB
Go
192 lines
4.6 KiB
Go
package libpack_cache_memory
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type MemoryTestSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) SetupTest() {
|
|
}
|
|
|
|
func TestCachingTestSuite(t *testing.T) {
|
|
suite.Run(t, new(MemoryTestSuite))
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_New() {
|
|
suite.T().Run("should return a new cache", func(t *testing.T) {
|
|
cache := New(2 * time.Second)
|
|
suite.NotNil(cache)
|
|
})
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_CacheUse() {
|
|
cache := New(30 * time.Second)
|
|
tests := []struct {
|
|
name string
|
|
cache_value string
|
|
}{
|
|
{
|
|
name: "test1",
|
|
cache_value: "test1-123",
|
|
},
|
|
{
|
|
name: "test2",
|
|
cache_value: "test2-123",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
suite.T().Run(tt.name, func(t *testing.T) {
|
|
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
|
c, ok := cache.Get(tt.name)
|
|
suite.Equal(true, ok)
|
|
suite.Equal(tt.name, string(c))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_CacheDelete() {
|
|
cache := New(30 * time.Second)
|
|
tests := []struct {
|
|
name string
|
|
cache_value string
|
|
}{
|
|
{
|
|
name: "test1",
|
|
cache_value: "test1-123",
|
|
},
|
|
{
|
|
name: "test2",
|
|
cache_value: "test2-123",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
suite.T().Run(tt.name, func(t *testing.T) {
|
|
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
|
c, ok := cache.Get(tt.name)
|
|
suite.Equal(true, ok)
|
|
suite.Equal(tt.name, string(c))
|
|
cache.Delete(tt.name)
|
|
c, ok = cache.Get(tt.name)
|
|
suite.Equal(false, ok)
|
|
suite.Equal("", string(c))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_CacheExpire() {
|
|
cache := New(30 * time.Second)
|
|
tests := []struct {
|
|
name string
|
|
cache_value string
|
|
ttl time.Duration
|
|
}{
|
|
{
|
|
name: "test1",
|
|
cache_value: "test1-123",
|
|
ttl: 2 * time.Second,
|
|
},
|
|
{
|
|
name: "test2",
|
|
cache_value: "test2-123",
|
|
ttl: 5 * time.Second,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
suite.T().Run(tt.name, func(t *testing.T) {
|
|
cache.Set(tt.name, []byte(tt.name), tt.ttl)
|
|
c, ok := cache.Get(tt.name)
|
|
suite.Equal(true, ok)
|
|
suite.Equal(tt.name, string(c))
|
|
time.Sleep(tt.ttl)
|
|
c, ok = cache.Get(tt.name)
|
|
suite.Equal(false, ok)
|
|
suite.Equal("", string(c))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_ConcurrentReadWrite() {
|
|
cache := New(5 * time.Second)
|
|
const numGoroutines = 100
|
|
const numOperations = 1000
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(numGoroutines)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("key-%d-%d", id, j)
|
|
value := []byte(fmt.Sprintf("value-%d-%d", id, j))
|
|
|
|
if j%2 == 0 {
|
|
cache.Set(key, value, 5*time.Second)
|
|
} else {
|
|
_, _ = cache.Get(key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_LargeItems() {
|
|
cache := New(5 * time.Second)
|
|
largeValue := make([]byte, 10*1024*1024) // 10MB
|
|
cache.Set("large-key", largeValue, 5*time.Second)
|
|
|
|
retrieved, found := cache.Get("large-key")
|
|
suite.Assert().True(found)
|
|
suite.Assert().Equal(largeValue, retrieved)
|
|
}
|
|
|
|
// Test_ZeroTTL verifies the C11 cross-backend rule: a non-positive TTL is
|
|
// clamped to defaultTTL instead of being stored as already-expired. Before
|
|
// this fix, Set("zero-ttl", ..., 0) stamped ExpiresAt at time.Now(), so the
|
|
// very next Get always missed.
|
|
func (suite *MemoryTestSuite) Test_ZeroTTL() {
|
|
cache := New(5 * time.Second)
|
|
cache.Set("zero-ttl", []byte("value"), 0)
|
|
|
|
retrieved, found := cache.Get("zero-ttl")
|
|
suite.Assert().True(found, "zero TTL should be clamped to defaultTTL, not stored as already-expired")
|
|
suite.Assert().Equal([]byte("value"), retrieved)
|
|
|
|
raw, ok := cache.entries.Load("zero-ttl")
|
|
suite.Require().True(ok)
|
|
entry, ok := raw.(CacheEntry)
|
|
suite.Require().True(ok)
|
|
suite.Assert().WithinDuration(time.Now().Add(defaultTTL), entry.ExpiresAt, 2*time.Second,
|
|
"clamped entry should expire ~defaultTTL from now")
|
|
}
|
|
|
|
// Test_NegativeTTL verifies the same clamp applies to a negative TTL, not
|
|
// just exactly zero.
|
|
func (suite *MemoryTestSuite) Test_NegativeTTL() {
|
|
cache := New(5 * time.Second)
|
|
cache.Set("negative-ttl", []byte("value"), -1*time.Second)
|
|
|
|
retrieved, found := cache.Get("negative-ttl")
|
|
suite.Assert().True(found, "negative TTL should be clamped to defaultTTL, not stored as already-expired")
|
|
suite.Assert().Equal([]byte("value"), retrieved)
|
|
}
|
|
|
|
func (suite *MemoryTestSuite) Test_LongTTL() {
|
|
cache := New(5 * time.Second)
|
|
cache.Set("long-ttl", []byte("value"), 24*365*time.Hour) // 1 year
|
|
|
|
retrieved, found := cache.Get("long-ttl")
|
|
suite.Assert().True(found)
|
|
suite.Assert().Equal([]byte("value"), retrieved)
|
|
}
|