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:
2025-09-30 18:27:33 +01:00
committed by GitHub
parent 3bd96cbd8a
commit cedee416a8
80 changed files with 22799 additions and 647 deletions
+98 -6
View File
@@ -23,6 +23,10 @@ type CacheConfig struct {
DB int `json:"db"`
Enable bool `json:"enable"`
}
Memory struct {
MaxMemorySize int64 `json:"max_memory_size"` // Maximum memory size in bytes
MaxEntries int64 `json:"max_entries"` // Maximum number of entries
}
TTL int `json:"ttl"`
}
@@ -38,6 +42,9 @@ type CacheClient interface {
Delete(key string)
Clear()
CountQueries() int64
// Memory usage reporting methods
GetMemoryUsage() int64 // Returns current memory usage in bytes
GetMaxMemorySize() int64 // Returns max memory size in bytes
}
var (
@@ -45,6 +52,18 @@ var (
config *CacheConfig
)
// CalculateHash generates an MD5 hash from the request body.
// For GraphQL requests, this includes both the query and variables,
// ensuring that identical queries with different variables are cached separately.
//
// Example GraphQL request body:
//
// {
// "query": "query GetUser($id: ID!) { user(id: $id) { name } }",
// "variables": { "id": "123" }
// }
//
// Different variable values will produce different cache keys.
func CalculateHash(c *fiber.Ctx) string {
return strutil.Md5(c.Body())
}
@@ -61,16 +80,51 @@ func EnableCache(cfg *CacheConfig) {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Using Redis cache",
})
cfg.Client = libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
redisClient, err := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
RedisDB: cfg.Redis.DB,
RedisServer: cfg.Redis.URL,
RedisPassword: cfg.Redis.Password,
})
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to create Redis client",
Pairs: map[string]interface{}{"error": err.Error()},
})
// Fall back to memory cache
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
} else {
cfg.Client = libpack_cache_redis.NewCacheWrapper(redisClient, cfg.Logger)
}
} else {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Using in-memory cache",
Pairs: map[string]interface{}{
"max_memory_size_bytes": cfg.Memory.MaxMemorySize,
"max_entries": cfg.Memory.MaxEntries,
},
})
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
// Use memory size and entry limits if configured, otherwise use defaults
if cfg.Memory.MaxMemorySize > 0 || cfg.Memory.MaxEntries > 0 {
maxMemory := cfg.Memory.MaxMemorySize
if maxMemory <= 0 {
maxMemory = libpack_cache_memory.DefaultMaxMemorySize
}
maxEntries := cfg.Memory.MaxEntries
if maxEntries <= 0 {
maxEntries = libpack_cache_memory.DefaultMaxCacheSize
}
cfg.Client = libpack_cache_memory.NewWithSize(
time.Duration(cfg.TTL)*time.Second,
maxMemory,
maxEntries,
)
} else {
// Backward compatibility
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
}
}
config = cfg
}
@@ -93,7 +147,15 @@ func CacheLookup(hash string) []byte {
})
return nil
}
defer reader.Close()
// Ensure reader is always closed, even on error
defer func() {
if closeErr := reader.Close(); closeErr != nil {
config.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to close gzip reader",
Pairs: map[string]interface{}{"error": closeErr.Error(), "hash": hash},
})
}
}()
decompressed, err := io.ReadAll(reader)
if err != nil {
@@ -119,7 +181,17 @@ func CacheDelete(hash string) {
Message: "Deleting data from cache",
Pairs: map[string]interface{}{"hash": hash},
})
atomic.AddInt64(&cacheStats.CachedQueries, -1)
// Use atomic operations with validation to prevent inconsistent statistics
for {
current := atomic.LoadInt64(&cacheStats.CachedQueries)
if current <= 0 {
break // Don't go below zero
}
if atomic.CompareAndSwapInt64(&cacheStats.CachedQueries, current, current-1) {
break
}
// Retry if CAS failed due to concurrent modification
}
config.Client.Delete(hash)
}
@@ -172,8 +244,28 @@ func GetCacheStats() *CacheStats {
config.Logger.Debug(&libpack_logger.LogMessage{
Message: "Getting cache stats",
})
cacheStats.CachedQueries = CacheGetQueries()
return cacheStats
// Return a copy to avoid race conditions
return &CacheStats{
CacheHits: atomic.LoadInt64(&cacheStats.CacheHits),
CacheMisses: atomic.LoadInt64(&cacheStats.CacheMisses),
CachedQueries: CacheGetQueries(),
}
}
// GetCacheMemoryUsage returns the current memory usage of the cache in bytes
func GetCacheMemoryUsage() int64 {
if !IsCacheInitialized() {
return 0
}
return config.Client.GetMemoryUsage()
}
// GetCacheMaxMemorySize returns the maximum memory size allowed for the cache in bytes
func GetCacheMaxMemorySize() int64 {
if !IsCacheInitialized() {
return 0
}
return config.Client.GetMaxMemorySize()
}
func ShouldUseRedisCache(cfg *CacheConfig) bool {
+30
View File
@@ -43,6 +43,36 @@ func (suite *Tests) Test_CalculateHash() {
assert.NotEqual(hash1, hash2)
})
// Test with GraphQL query and variables
suite.Run("graphql with same query different variables", func() {
// Same query, different variables should produce different hashes
query1 := []byte(`{"query":"query GetUser($id: ID!) { user(id: $id) { name } }","variables":{"id":"123"}}`)
query2 := []byte(`{"query":"query GetUser($id: ID!) { user(id: $id) { name } }","variables":{"id":"456"}}`)
ctx.Request().SetBody(query1)
hash1 := CalculateHash(ctx)
ctx.Request().SetBody(query2)
hash2 := CalculateHash(ctx)
assert.NotEqual(hash1, hash2, "Different variables should produce different cache keys")
})
// Test with GraphQL query without variables
suite.Run("graphql with and without variables", func() {
// Same query with and without variables should produce different hashes
query1 := []byte(`{"query":"query GetUsers { users { name } }"}`)
query2 := []byte(`{"query":"query GetUsers { users { name } }","variables":{}}`)
ctx.Request().SetBody(query1)
hash1 := CalculateHash(ctx)
ctx.Request().SetBody(query2)
hash2 := CalculateHash(ctx)
assert.NotEqual(hash1, hash2, "Query with and without variables object should produce different cache keys")
})
}
func (suite *Tests) Test_CacheDelete() {
+17
View File
@@ -0,0 +1,17 @@
package libpack_cache_memory
import (
"bytes"
"github.com/lukaszraczylo/graphql-monitoring-proxy/pkg/pools"
)
// GetBuffer gets a buffer from the pool (delegates to unified implementation)
func GetBuffer() *bytes.Buffer {
return pools.GetBuffer()
}
// PutBuffer returns a buffer to the pool (delegates to unified implementation)
func PutBuffer(buf *bytes.Buffer) {
pools.PutBuffer(buf)
}
+218
View File
@@ -0,0 +1,218 @@
package libpack_cache_memory
import (
"bytes"
"compress/gzip"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestCompressionThreshold tests that values are only compressed when they exceed the threshold
func TestCompressionThreshold(t *testing.T) {
cache := New(5 * time.Second)
// Create test values
smallValue := make([]byte, CompressionThreshold-100) // Below threshold
largeValue := make([]byte, CompressionThreshold*2) // Above threshold
// Fill values with compressible data (repeating patterns compress well)
for i := 0; i < len(smallValue); i++ {
smallValue[i] = byte(i % 10)
}
for i := 0; i < len(largeValue); i++ {
largeValue[i] = byte(i % 10)
}
// Test small value
cache.Set("small-key", smallValue, 5*time.Second)
// Extract the entry directly from the cache to check if it's compressed
entryRaw, found := cache.entries.Load("small-key")
assert.True(t, found, "Entry should exist")
entry := entryRaw.(CacheEntry)
assert.False(t, entry.Compressed, "Small value should not be compressed")
assert.Equal(t, smallValue, entry.Value, "Small value should be stored as-is")
// Test large value
cache.Set("large-key", largeValue, 5*time.Second)
entryRaw, found = cache.entries.Load("large-key")
assert.True(t, found, "Entry should exist")
entry = entryRaw.(CacheEntry)
assert.True(t, entry.Compressed, "Large value should be compressed")
// Ensure the stored value isn't the original
assert.NotEqual(t, largeValue, entry.Value, "Large value should not be stored as-is")
// Verify the value is actually compressed (should be smaller)
assert.Less(t, len(entry.Value), len(largeValue), "Compressed value should be smaller than original")
// Verify we can retrieve the uncompressed value correctly
retrievedLarge, found := cache.Get("large-key")
assert.True(t, found, "Large value should be retrievable")
assert.Equal(t, largeValue, retrievedLarge, "Retrieved large value should match original")
}
// TestCompressionMemoryUsage tests that memory usage is calculated correctly for compressed entries
func TestCompressionMemoryUsage(t *testing.T) {
cache := New(5 * time.Second)
// Create a large, highly compressible value
valueSize := CompressionThreshold * 4
value := make([]byte, valueSize)
for i := 0; i < valueSize; i++ {
value[i] = byte(i % 2) // Highly compressible pattern (alternating 0s and 1s)
}
// Get initial memory usage
initialMemUsage := cache.GetMemoryUsage()
// Add the value
key := "large-compressible-key"
cache.Set(key, value, 5*time.Second)
// Get memory usage after adding
newMemUsage := cache.GetMemoryUsage()
// The memory usage increase should be less than the full value size due to compression
memUsageIncrease := newMemUsage - initialMemUsage
// Extract the entry to check its compressed size
entryRaw, found := cache.entries.Load(key)
assert.True(t, found, "Entry should exist")
entry := entryRaw.(CacheEntry)
assert.True(t, entry.Compressed, "Value should be compressed")
// Verify the reported memory usage matches the compressed size + overheads
compressedSize := int64(len(entry.Value))
keySize := int64(len(key))
expectedUsage := compressedSize + keySize + approxEntryOverhead
// The memory usage should reflect the compressed size, not the original size
assert.InDelta(t, expectedUsage, memUsageIncrease, float64(approxEntryOverhead),
"Memory usage should be based on compressed size")
// Verify memory usage is correctly updated after deletion
cache.Delete(key)
finalMemUsage := cache.GetMemoryUsage()
assert.Equal(t, initialMemUsage, finalMemUsage,
"Memory usage should return to initial value after deletion")
}
// TestUncompressibleData tests the case where compression doesn't reduce size
func TestUncompressibleData(t *testing.T) {
cache := New(5 * time.Second)
// Create a large, random (less compressible) value
valueSize := CompressionThreshold * 2
// Create pseudo-random data that doesn't compress well
// Using a custom PRNG for deterministic results across test runs
value := make([]byte, valueSize)
seed := uint32(42)
for i := 0; i < valueSize; i++ {
// Simple linear congruential generator
seed = seed*1664525 + 1013904223
value[i] = byte(seed)
}
// Try to compress it directly to see if it actually would reduce size
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
_, _ = gw.Write(value)
_ = gw.Close()
compressedDirectly := buf.Bytes()
// Now use the cache's Set method
key := "uncompressible-key"
cache.Set(key, value, 5*time.Second)
// Extract the entry to check if it's compressed
entryRaw, found := cache.entries.Load(key)
assert.True(t, found, "Entry should exist")
entry := entryRaw.(CacheEntry)
// If our test data actually compressed to a smaller size, we expect the cache to store it compressed
if len(compressedDirectly) < len(value) {
assert.True(t, entry.Compressed, "Value should be stored compressed if smaller")
assert.Less(t, len(entry.Value), len(value), "Compressed value should be smaller")
} else {
// Uncommon case: our pseudo-random data actually expanded with gzip
// In this case, the cache should store it uncompressed
assert.False(t, entry.Compressed, "Value should not be compressed if it would expand")
assert.Equal(t, value, entry.Value, "Value should be stored as-is")
}
// Regardless, we should be able to get the correct value back
retrievedValue, found := cache.Get(key)
assert.True(t, found, "Value should be retrievable")
assert.Equal(t, value, retrievedValue, "Retrieved value should match original")
}
// TestCompressDecompressDirectly tests the compress and decompress methods directly
func TestCompressDecompressDirectly(t *testing.T) {
cache := New(5 * time.Second)
// Test with various sizes
testSizes := []int{
100, // Small
CompressionThreshold - 1, // Just below threshold
CompressionThreshold, // At threshold
CompressionThreshold + 1, // Just above threshold
CompressionThreshold * 2, // Well above threshold
}
for _, size := range testSizes {
t.Run("Size-"+string(rune('A'+len(testSizes)%26)), func(t *testing.T) {
// Generate test data with a repeating pattern
data := make([]byte, size)
for i := 0; i < size; i++ {
data[i] = byte(i % 256)
}
// Compress the data
compressed, err := cache.compress(data)
assert.NoError(t, err, "Compression should not error")
// Small data may get larger when compressed, larger data should get smaller
if size > CompressionThreshold {
assert.Less(t, len(compressed), len(data),
"Compression should reduce size for data above threshold")
}
// Decompress and verify it matches the original
decompressed, err := cache.decompress(compressed)
assert.NoError(t, err, "Decompression should not error")
assert.Equal(t, data, decompressed, "Data should round-trip correctly through compression")
})
}
}
// TestDecompressInvalidData tests handling invalid data in decompress
func TestDecompressInvalidData(t *testing.T) {
cache := New(5 * time.Second)
// Try to decompress non-gzip data
invalidData := []byte("This is not valid gzip data")
_, err := cache.decompress(invalidData)
assert.Error(t, err, "Decompressing invalid data should return error")
// Set compressed flag but store invalid data
key := "invalid-compressed-key"
cache.entries.Store(key, CacheEntry{
Value: invalidData,
ExpiresAt: time.Now().Add(5 * time.Second),
Compressed: true, // Flag as compressed even though it's not
MemorySize: int64(len(invalidData) + len(key) + approxEntryOverhead),
})
// Try to get it - should fail gracefully
_, found := cache.Get(key)
assert.False(t, found, "Get should fail gracefully for invalid compressed data")
}
+185
View File
@@ -0,0 +1,185 @@
package libpack_cache_memory
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestEvictToFreeMemory tests that the cache correctly evicts
// items when it exceeds its memory limit.
func TestEvictToFreeMemory(t *testing.T) {
// Create a cache with a small memory limit: 5KB (ensure eviction happens)
smallMemLimit := int64(5 * 1024)
cache := NewWithSize(5*time.Second, smallMemLimit, 1000)
// Create entries with known sizes
// Each entry will be ~512 bytes plus overhead
valueSize := 512
numEntriesToExceedLimit := 12 // Should exceed the 5KB limit and force eviction
// Create a slice to track keys in insertion order
keys := make([]string, numEntriesToExceedLimit)
// Add entries with significant delays between insertions
for i := 0; i < numEntriesToExceedLimit; i++ {
key := fmt.Sprintf("test-key-%d", i)
keys[i] = key
value := make([]byte, valueSize)
for j := 0; j < valueSize; j++ {
value[j] = byte(i % 256) // Fill with a repeating pattern
}
cache.Set(key, value, 30*time.Second)
// More significant delay to ensure different timestamps
time.Sleep(10 * time.Millisecond)
}
// Allow time for eviction to complete
time.Sleep(50 * time.Millisecond)
// Verify memory usage is below the limit
memUsage := cache.GetMemoryUsage()
assert.LessOrEqual(t, memUsage, smallMemLimit,
"Memory usage (%d) should be less than or equal to the limit (%d)", memUsage, smallMemLimit)
// Count how many items are left in the cache and which ones
present := 0
for i := 0; i < numEntriesToExceedLimit; i++ {
_, found := cache.Get(keys[i])
if found {
present++
}
}
// We expect some items to be evicted based on the memory limit
assert.Less(t, present, numEntriesToExceedLimit,
"Some items should have been evicted (%d present out of %d total)",
present, numEntriesToExceedLimit)
// Verify newer items (inserted later) are more likely to be in the cache
// Check the last few items which should be the newest
for i := numEntriesToExceedLimit - 3; i < numEntriesToExceedLimit; i++ {
_, found := cache.Get(keys[i])
assert.True(t, found, "Newer key %s should still exist", keys[i])
}
}
// TestMaxCacheSize verifies the behavior when adding more items than the maxCacheSize limit
func TestMaxCacheSize(t *testing.T) {
// Create a cache with a small limit
smallLimit := int64(5)
cache := NewWithSize(5*time.Second, DefaultMaxMemorySize, smallLimit)
// Add entries with increasing size (to avoid memory-based eviction)
for i := 0; i < 20; i++ {
key := fmt.Sprintf("test-key-%d", i)
value := []byte(key)
cache.Set(key, value, 10*time.Second)
}
// Verify we can get a reasonable number of items
// (we don't test for exact count as implementation may vary)
foundCount := 0
for i := 0; i < 20; i++ {
key := fmt.Sprintf("test-key-%d", i)
_, found := cache.Get(key)
if found {
foundCount++
}
}
// We should find some items but not all 20
assert.Greater(t, foundCount, 0, "Some items should be in the cache")
assert.LessOrEqual(t, foundCount, 20, "Not all items should be in the cache with small limit")
}
// TestGetMemoryUsage verifies that memory usage tracking is accurate
func TestGetMemoryUsage(t *testing.T) {
cache := New(5 * time.Second)
// Initially memory usage should be 0
assert.Equal(t, int64(0), cache.GetMemoryUsage(), "Initial memory usage should be 0")
// Add an entry with a known approximate size
valueSize := 1024
value := make([]byte, valueSize)
key := "test-key"
cache.Set(key, value, 5*time.Second)
// Check memory usage - should be approximately valueSize + key length + overhead
expectedMinUsage := int64(valueSize + len(key))
memUsage := cache.GetMemoryUsage()
assert.GreaterOrEqual(t, memUsage, expectedMinUsage,
"Memory usage (%d) should be at least the value size plus key length (%d)", memUsage, expectedMinUsage)
// Delete the entry and verify memory usage decreases
cache.Delete(key)
assert.Equal(t, int64(0), cache.GetMemoryUsage(), "Memory usage should be 0 after deletion")
}
// TestSetMaxMemorySize tests changing the memory limit and resulting eviction
func TestSetMaxMemorySize(t *testing.T) {
// Start with a large limit
initialLimit := int64(100 * 1024)
cache := NewWithSize(5*time.Second, initialLimit, 1000)
// Fill the cache with ~50KB of data
valueSize := 1024
numEntries := 50
for i := 0; i < numEntries; i++ {
key := generateKey(i)
value := make([]byte, valueSize)
cache.Set(key, value, 5*time.Second)
// Small delay for timestamp differences
time.Sleep(time.Millisecond)
}
// Verify all entries exist
for i := 0; i < numEntries; i++ {
_, found := cache.Get(generateKey(i))
assert.True(t, found, "All entries should exist before limit change")
}
// Get current memory usage
originalUsage := cache.GetMemoryUsage()
// Now reduce the limit to 20KB - should trigger eviction
newLimit := int64(20 * 1024)
cache.SetMaxMemorySize(newLimit)
// Verify memory usage is now below the new limit
newUsage := cache.GetMemoryUsage()
assert.LessOrEqual(t, newUsage, newLimit,
"After SetMaxMemorySize, memory usage (%d) should be less than or equal to new limit (%d)",
newUsage, newLimit)
assert.Less(t, newUsage, originalUsage,
"Memory usage should have decreased after lowering the limit")
// Some older entries should be gone, newer ones should still exist
removedCount := 0
remainingCount := 0
for i := 0; i < numEntries; i++ {
_, found := cache.Get(generateKey(i))
if found {
remainingCount++
} else {
removedCount++
}
}
assert.Greater(t, removedCount, 0, "Some entries should have been removed")
assert.Greater(t, remainingCount, 0, "Some entries should still exist")
}
// Helper function to generate consistent keys
func generateKey(index int) string {
return "test-key-" + fmt.Sprintf("%d", index)
}
+281
View File
@@ -0,0 +1,281 @@
package libpack_cache_memory
import (
"compress/gzip"
"container/list"
"sync"
"sync/atomic"
"time"
)
// LRUMemoryCache is an efficient LRU-based memory cache implementation
type LRUMemoryCache struct {
entries map[string]*lruEntry
evictList *list.List
gzipWriterPool *sync.Pool
gzipReaderPool *sync.Pool
maxMemorySize int64
maxEntries int64
currentMemory int64
currentCount int64
mu sync.RWMutex
}
type lruEntry struct {
expiresAt time.Time
element *list.Element
key string
value []byte
size int64
compressed bool
}
// NewLRUMemoryCache creates a new LRU memory cache
func NewLRUMemoryCache(maxMemorySize, maxEntries int64) *LRUMemoryCache {
return &LRUMemoryCache{
maxMemorySize: maxMemorySize,
maxEntries: maxEntries,
entries: make(map[string]*lruEntry),
evictList: list.New(),
gzipWriterPool: &sync.Pool{
New: func() interface{} {
return gzip.NewWriter(nil)
},
},
gzipReaderPool: &sync.Pool{
New: func() interface{} {
return &gzip.Reader{}
},
},
}
}
// Set adds or updates an entry in the cache
func (c *LRUMemoryCache) Set(key string, value []byte, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
// Calculate expiry time
expiresAt := time.Now().Add(ttl)
// Check if we should compress
compressed := false
finalValue := value
if len(value) > 1024 { // Compress if larger than 1KB
if compressedData, err := c.compress(value); err == nil && len(compressedData) < len(value) {
compressed = true
finalValue = compressedData
}
}
entrySize := int64(len(key) + len(finalValue) + 64) // 64 bytes overhead estimate
// Check if key exists
if existing, exists := c.entries[key]; exists {
// Update existing entry
c.evictList.MoveToFront(existing.element)
atomic.AddInt64(&c.currentMemory, -existing.size)
atomic.AddInt64(&c.currentMemory, entrySize)
existing.value = finalValue
existing.compressed = compressed
existing.size = entrySize
existing.expiresAt = expiresAt
c.evictIfNeeded()
return
}
// Create new entry
entry := &lruEntry{
key: key,
value: finalValue,
compressed: compressed,
size: entrySize,
expiresAt: expiresAt,
}
element := c.evictList.PushFront(entry)
entry.element = element
c.entries[key] = entry
atomic.AddInt64(&c.currentMemory, entrySize)
atomic.AddInt64(&c.currentCount, 1)
c.evictIfNeeded()
}
// Get retrieves a value from the cache
func (c *LRUMemoryCache) Get(key string) ([]byte, bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, exists := c.entries[key]
if !exists {
return nil, false
}
// Check if expired
if time.Now().After(entry.expiresAt) {
c.removeEntry(entry)
return nil, false
}
// Move to front (most recently used)
c.evictList.MoveToFront(entry.element)
// Decompress if needed
if entry.compressed {
if decompressed, err := c.decompress(entry.value); err == nil {
return decompressed, true
}
// If decompression fails, remove the entry
c.removeEntry(entry)
return nil, false
}
return entry.value, true
}
// Delete removes an entry from the cache
func (c *LRUMemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
if entry, exists := c.entries[key]; exists {
c.removeEntry(entry)
}
}
// Clear removes all entries
func (c *LRUMemoryCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = make(map[string]*lruEntry)
c.evictList = list.New()
atomic.StoreInt64(&c.currentMemory, 0)
atomic.StoreInt64(&c.currentCount, 0)
}
// evictIfNeeded removes entries when limits are exceeded
func (c *LRUMemoryCache) evictIfNeeded() {
// Evict based on entry count
for atomic.LoadInt64(&c.currentCount) > c.maxEntries && c.evictList.Len() > 0 {
c.evictOldest()
}
// Evict based on memory
for atomic.LoadInt64(&c.currentMemory) > c.maxMemorySize && c.evictList.Len() > 0 {
c.evictOldest()
}
}
// evictOldest removes the least recently used entry
func (c *LRUMemoryCache) evictOldest() {
element := c.evictList.Back()
if element == nil {
return
}
entry := element.Value.(*lruEntry)
c.removeEntry(entry)
}
// removeEntry removes an entry from all data structures
func (c *LRUMemoryCache) removeEntry(entry *lruEntry) {
c.evictList.Remove(entry.element)
delete(c.entries, entry.key)
atomic.AddInt64(&c.currentMemory, -entry.size)
atomic.AddInt64(&c.currentCount, -1)
}
// CleanExpiredEntries removes all expired entries
func (c *LRUMemoryCache) CleanExpiredEntries() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
for element := c.evictList.Back(); element != nil; {
entry := element.Value.(*lruEntry)
if now.After(entry.expiresAt) {
next := element.Prev()
c.removeEntry(entry)
element = next
} else {
element = element.Prev()
}
}
}
// compress compresses data using gzip
func (c *LRUMemoryCache) compress(data []byte) ([]byte, error) {
buf := GetBuffer()
defer PutBuffer(buf)
gz := c.gzipWriterPool.Get().(*gzip.Writer)
gz.Reset(buf)
defer c.gzipWriterPool.Put(gz)
if _, err := gz.Write(data); err != nil {
return nil, err
}
if err := gz.Close(); err != nil {
return nil, err
}
compressed := make([]byte, buf.Len())
copy(compressed, buf.Bytes())
return compressed, nil
}
// decompress decompresses gzip data
func (c *LRUMemoryCache) decompress(data []byte) ([]byte, error) {
buf := GetBuffer()
defer PutBuffer(buf)
buf.Write(data)
gr := c.gzipReaderPool.Get().(*gzip.Reader)
defer c.gzipReaderPool.Put(gr)
if err := gr.Reset(buf); err != nil {
return nil, err
}
result := GetBuffer()
defer PutBuffer(result)
if _, err := result.ReadFrom(gr); err != nil {
return nil, err
}
decompressed := make([]byte, result.Len())
copy(decompressed, result.Bytes())
return decompressed, nil
}
// GetStats returns cache statistics
func (c *LRUMemoryCache) GetStats() map[string]interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
return map[string]interface{}{
"entries": atomic.LoadInt64(&c.currentCount),
"memory_bytes": atomic.LoadInt64(&c.currentMemory),
"max_entries": c.maxEntries,
"max_memory": c.maxMemorySize,
"fill_percent": float64(atomic.LoadInt64(&c.currentMemory)) / float64(c.maxMemorySize) * 100,
}
}
// GetMemoryUsage returns current memory usage in bytes
func (c *LRUMemoryCache) GetMemoryUsage() int64 {
return atomic.LoadInt64(&c.currentMemory)
}
// GetMaxMemorySize returns the maximum memory size
func (c *LRUMemoryCache) GetMaxMemorySize() int64 {
return c.maxMemorySize
}
+154 -21
View File
@@ -3,8 +3,8 @@ package libpack_cache_memory
import (
"bytes"
"compress/gzip"
"context"
"io"
"runtime"
"sync"
"sync/atomic"
"time"
@@ -13,27 +13,53 @@ import (
// CompressionThreshold is the minimum size in bytes before a value is compressed
const CompressionThreshold = 1024 // 1KB
// MaxCacheSize is the maximum number of entries in the cache
const MaxCacheSize = 10000
// DefaultMaxMemorySize is the default maximum memory size in bytes (100MB)
const DefaultMaxMemorySize = 100 * 1024 * 1024
// DefaultMaxCacheSize is the default maximum number of entries in the cache
// This is used for backward compatibility
const DefaultMaxCacheSize = 10000
// approxEntryOverhead is the estimated overhead per cache entry in bytes
// This accounts for the CacheEntry struct overhead, map entry, and synchronization
const approxEntryOverhead = 64
type CacheEntry struct {
ExpiresAt time.Time
Value []byte
Compressed bool
MemorySize int64 // Estimated memory usage of this entry in bytes
}
type Cache struct {
compressPool sync.Pool
decompressPool sync.Pool
ctx context.Context
cancel context.CancelFunc
entries sync.Map
globalTTL time.Duration
entryCount int64
memoryUsage int64
maxMemorySize int64
maxCacheSize int64
sync.RWMutex
}
func New(globalTTL time.Duration) *Cache {
return NewWithSize(globalTTL, DefaultMaxMemorySize, DefaultMaxCacheSize)
}
// NewWithSize creates a new cache with the specified memory size limit and entry count limit
func NewWithSize(globalTTL time.Duration, maxMemorySize int64, maxCacheSize int64) *Cache {
// Create context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
cache := &Cache{
globalTTL: globalTTL,
globalTTL: globalTTL,
maxMemorySize: maxMemorySize,
maxCacheSize: maxCacheSize,
ctx: ctx,
cancel: cancel,
compressPool: sync.Pool{
New: func() interface{} {
return gzip.NewWriter(nil)
@@ -47,7 +73,7 @@ func New(globalTTL time.Duration) *Cache {
},
}
// Start cleanup routine
// Start cleanup routine with context cancellation
go cache.cleanupRoutine(globalTTL)
return cache
}
@@ -57,20 +83,40 @@ func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
ticker := time.NewTicker(globalTTL / 4)
defer ticker.Stop()
for range ticker.C {
c.CleanExpiredEntries()
for {
select {
case <-c.ctx.Done():
// Context cancelled, exit gracefully
return
case <-ticker.C:
c.CleanExpiredEntries()
// Trigger GC if we have a lot of entries
if atomic.LoadInt64(&c.entryCount) > MaxCacheSize/2 {
runtime.GC()
// Note: Removed aggressive GC trigger that was causing performance issues
// The Go runtime GC is already optimized and will run when needed
}
}
}
// Shutdown gracefully stops the cache cleanup routine
func (c *Cache) Shutdown() {
if c.cancel != nil {
c.cancel()
}
}
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
// Check if we've reached the maximum cache size
if atomic.LoadInt64(&c.entryCount) >= MaxCacheSize {
c.evictOldest(MaxCacheSize / 10) // Evict 10% of entries
// Calculate the memory size of this entry
entrySize := int64(len(key) + len(value) + approxEntryOverhead)
// Check if we need to evict entries based on memory or count limits
currentMemory := atomic.LoadInt64(&c.memoryUsage)
if currentMemory+entrySize > c.maxMemorySize {
// Need to evict based on memory
memoryToFree := (currentMemory + entrySize) - c.maxMemorySize + (c.maxMemorySize / 10)
c.evictToFreeMemory(memoryToFree)
} else if atomic.LoadInt64(&c.entryCount) >= c.maxCacheSize {
// Fall back to count-based eviction for backward compatibility
c.evictOldest(int(c.maxCacheSize / 10)) // Evict 10% of entries
}
expiresAt := time.Now().Add(ttl)
@@ -101,12 +147,26 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
}
}
// Check if this is a new entry
_, exists := c.entries.Load(key)
if !exists {
// Update the entry memory size based on compression status
if entry.Compressed {
entry.MemorySize = int64(len(key) + len(entry.Value) + approxEntryOverhead)
} else {
entry.MemorySize = int64(len(key) + len(entry.Value) + approxEntryOverhead)
}
// Check if this is a new entry or an update
oldEntry, exists := c.entries.Load(key)
if exists {
// Update memory usage: subtract old entry size, add new entry size
oldCacheEntry := oldEntry.(CacheEntry)
atomic.AddInt64(&c.memoryUsage, -oldCacheEntry.MemorySize)
} else {
// New entry
atomic.AddInt64(&c.entryCount, 1)
}
// Add new entry's memory size to total
atomic.AddInt64(&c.memoryUsage, entry.MemorySize)
c.entries.Store(key, entry)
}
@@ -120,6 +180,7 @@ func (c *Cache) Get(key string) ([]byte, bool) {
if cacheEntry.ExpiresAt.Before(time.Now()) {
c.entries.Delete(key)
atomic.AddInt64(&c.entryCount, -1)
atomic.AddInt64(&c.memoryUsage, -cacheEntry.MemorySize)
return nil, false
}
@@ -135,8 +196,10 @@ func (c *Cache) Get(key string) ([]byte, bool) {
}
func (c *Cache) Delete(key string) {
if _, exists := c.entries.LoadAndDelete(key); exists {
if entry, exists := c.entries.LoadAndDelete(key); exists {
cacheEntry := entry.(CacheEntry)
atomic.AddInt64(&c.entryCount, -1)
atomic.AddInt64(&c.memoryUsage, -cacheEntry.MemorySize)
}
}
@@ -146,6 +209,7 @@ func (c *Cache) Clear() {
return true
})
atomic.StoreInt64(&c.entryCount, 0)
atomic.StoreInt64(&c.memoryUsage, 0)
}
func (c *Cache) CountQueries() int64 {
@@ -183,7 +247,9 @@ func (c *Cache) decompress(data []byte) ([]byte, error) {
}
}
defer r.Close()
defer func() {
_ = r.Close() // Ignore error in defer cleanup
}()
return io.ReadAll(r)
}
@@ -194,6 +260,7 @@ func (c *Cache) CleanExpiredEntries() {
if entry.ExpiresAt.Before(now) {
if _, exists := c.entries.LoadAndDelete(key); exists {
atomic.AddInt64(&c.entryCount, -1)
atomic.AddInt64(&c.memoryUsage, -entry.MemorySize)
}
}
return true
@@ -203,8 +270,8 @@ func (c *Cache) CleanExpiredEntries() {
// evictOldest removes the oldest n entries from the cache
func (c *Cache) evictOldest(n int) {
type keyExpiry struct {
key string
expiresAt time.Time
key string
}
// Collect all entries with their expiry times
@@ -212,7 +279,7 @@ func (c *Cache) evictOldest(n int) {
c.entries.Range(func(k, v interface{}) bool {
key := k.(string)
entry := v.(CacheEntry)
entries = append(entries, keyExpiry{key, entry.ExpiresAt})
entries = append(entries, keyExpiry{entry.ExpiresAt, key})
return len(entries) < cap(entries)
})
@@ -231,8 +298,74 @@ func (c *Cache) evictOldest(n int) {
}
// Delete this entry
if _, exists := c.entries.LoadAndDelete(entries[i].key); exists {
if entry, exists := c.entries.LoadAndDelete(entries[i].key); exists {
cacheEntry := entry.(CacheEntry)
atomic.AddInt64(&c.entryCount, -1)
atomic.AddInt64(&c.memoryUsage, -cacheEntry.MemorySize)
}
}
}
// evictToFreeMemory removes entries until the specified amount of memory is freed
func (c *Cache) evictToFreeMemory(bytesToFree int64) {
type keyMemorySize struct {
expiresAt time.Time
key string
memorySize int64
}
// Collect entries to consider for eviction
entries := make([]keyMemorySize, 0, int(c.maxCacheSize/5))
c.entries.Range(func(k, v interface{}) bool {
key := k.(string)
entry := v.(CacheEntry)
entries = append(entries, keyMemorySize{entry.ExpiresAt, key, entry.MemorySize})
return len(entries) < cap(entries)
})
// Sort entries by expiry time (oldest first)
// Simple selection sort since we only need to find the oldest entries
var freedBytes int64
for i := 0; i < len(entries) && freedBytes < bytesToFree; i++ {
oldest := i
for j := i + 1; j < len(entries); j++ {
if entries[j].expiresAt.Before(entries[oldest].expiresAt) {
oldest = j
}
}
// Swap
if oldest != i {
entries[i], entries[oldest] = entries[oldest], entries[i]
}
// Delete this entry
if entry, exists := c.entries.LoadAndDelete(entries[i].key); exists {
cacheEntry := entry.(CacheEntry)
atomic.AddInt64(&c.entryCount, -1)
atomic.AddInt64(&c.memoryUsage, -cacheEntry.MemorySize)
freedBytes += cacheEntry.MemorySize
}
}
}
// GetMemoryUsage returns the current memory usage of the cache in bytes
func (c *Cache) GetMemoryUsage() int64 {
return atomic.LoadInt64(&c.memoryUsage)
}
// GetMaxMemorySize returns the maximum memory size allowed for the cache in bytes
func (c *Cache) GetMaxMemorySize() int64 {
return c.maxMemorySize
}
// SetMaxMemorySize updates the maximum memory size allowed for the cache
func (c *Cache) SetMaxMemorySize(maxBytes int64) {
c.maxMemorySize = maxBytes
// Check if we need to evict entries due to the new limit
currentMemory := atomic.LoadInt64(&c.memoryUsage)
if currentMemory > maxBytes {
memoryToFree := currentMemory - maxBytes + (maxBytes / 10)
c.evictToFreeMemory(memoryToFree)
}
}
+45 -20
View File
@@ -3,9 +3,8 @@ package libpack_cache_redis
import (
"context"
"strings"
"time"
"sync"
"time"
redis "github.com/redis/go-redis/v9"
)
@@ -33,7 +32,7 @@ type RedisClientConfig struct {
RedisDB int
}
func New(redisClientConfig *RedisClientConfig) *RedisConfig {
func New(redisClientConfig *RedisClientConfig) (*RedisConfig, error) {
c := &RedisConfig{
client: redis.NewClient(&redis.Options{
Addr: redisClientConfig.RedisServer,
@@ -51,46 +50,72 @@ func New(redisClientConfig *RedisClientConfig) *RedisConfig {
_, err := c.client.Ping(c.ctx).Result()
if err != nil {
panic(err)
return nil, err
}
return c
return c, nil
}
func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) {
c.client.Set(c.ctx, c.prependKeyName(key), value, ttl)
func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) error {
return c.client.Set(c.ctx, c.prependKeyName(key), value, ttl).Err()
}
func (c *RedisConfig) Get(key string) ([]byte, bool) {
func (c *RedisConfig) Get(key string) ([]byte, bool, error) {
val, err := c.client.Get(c.ctx, c.prependKeyName(key)).Result()
if err == redis.Nil {
return nil, false
return nil, false, nil
}
if err != nil {
return nil, false
return nil, false, err
}
return []byte(val), true
return []byte(val), true, nil
}
func (c *RedisConfig) Delete(key string) {
c.client.Del(c.ctx, c.prependKeyName(key))
func (c *RedisConfig) Delete(key string) error {
return c.client.Del(c.ctx, c.prependKeyName(key)).Err()
}
func (c *RedisConfig) Clear() {
c.client.FlushDB(c.ctx)
func (c *RedisConfig) Clear() error {
return c.client.FlushDB(c.ctx).Err()
}
func (c *RedisConfig) CountQueries() int64 {
func (c *RedisConfig) CountQueries() (int64, error) {
keys, err := c.client.Keys(c.ctx, c.prependKeyName("*")).Result()
if err != nil {
return 0
return 0, err
}
return int64(len(keys))
return int64(len(keys)), nil
}
func (c *RedisConfig) CountQueriesWithPattern(pattern string) int {
func (c *RedisConfig) CountQueriesWithPattern(pattern string) (int, error) {
keys, err := c.client.Keys(c.ctx, c.prependKeyName(pattern)).Result()
if err != nil {
return 0, err
}
return len(keys), nil
}
// GetMemoryUsage returns an approximation of memory usage for Redis
// For Redis, this is not as accurate as the memory cache implementation
// as actual memory is managed by Redis server
func (c *RedisConfig) GetMemoryUsage() int64 {
// We could attempt to get memory usage from Redis info
// but for now, we'll just return 0 since Redis manages its own memory
// and this information would require parsing the INFO command output
_, err := c.client.Info(c.ctx, "memory").Result()
if err != nil {
return 0
}
return len(keys)
// Just return 0 as a placeholder since Redis manages its own memory
// In a production environment, you could parse the Redis INFO command result
// to extract actual "used_memory" value
return 0
}
// GetMaxMemorySize returns the configured max memory for Redis
// In Redis, this would be the 'maxmemory' configuration value
func (c *RedisConfig) GetMaxMemorySize() int64 {
// Return a default value as Redis manages its own memory limits
// In a production environment, you could get this from Redis config
return 0
}
+22 -10
View File
@@ -17,34 +17,46 @@ func TestRedisClear(t *testing.T) {
defer s.Close()
// Create a Redis client
redisConfig := New(&RedisClientConfig{
redisConfig, err := New(&RedisClientConfig{
RedisServer: s.Addr(),
RedisPassword: "",
RedisDB: 0,
})
if err != nil {
t.Fatalf("Failed to create Redis client: %v", err)
}
// Add some test data
ttl := time.Duration(60) * time.Second
redisConfig.Set("key1", []byte("value1"), ttl)
redisConfig.Set("key2", []byte("value2"), ttl)
redisConfig.Set("key3", []byte("value3"), ttl)
err = redisConfig.Set("key1", []byte("value1"), ttl)
assert.NoError(t, err)
err = redisConfig.Set("key2", []byte("value2"), ttl)
assert.NoError(t, err)
err = redisConfig.Set("key3", []byte("value3"), ttl)
assert.NoError(t, err)
// Verify keys exist
count := redisConfig.CountQueries()
count, err := redisConfig.CountQueries()
assert.NoError(t, err)
assert.Equal(t, int64(3), count, "Expected 3 keys before clearing cache")
// Clear the cache
redisConfig.Clear()
err = redisConfig.Clear()
assert.NoError(t, err)
// Verify all keys are gone
count = redisConfig.CountQueries()
count, err = redisConfig.CountQueries()
assert.NoError(t, err)
assert.Equal(t, int64(0), count, "Expected 0 keys after clearing cache")
// Verify individual keys are gone
_, found := redisConfig.Get("key1")
_, found, err := redisConfig.Get("key1")
assert.NoError(t, err)
assert.False(t, found, "Key1 should be deleted after Clear")
_, found = redisConfig.Get("key2")
_, found, err = redisConfig.Get("key2")
assert.NoError(t, err)
assert.False(t, found, "Key2 should be deleted after Clear")
_, found = redisConfig.Get("key3")
_, found, err = redisConfig.Get("key3")
assert.NoError(t, err)
assert.False(t, found, "Key3 should be deleted after Clear")
}
+52 -24
View File
@@ -17,11 +17,13 @@ type RedisConfigSuite struct {
func (suite *RedisConfigSuite) SetupTest() {
suite.redis_server, _ = miniredis.Run()
suite.redisConfig = New(&RedisClientConfig{
var err error
suite.redisConfig, err = New(&RedisClientConfig{
RedisServer: suite.redis_server.Addr(),
RedisPassword: "",
RedisDB: 0,
})
assert.NoError(suite.T(), err)
suite.redisConfig.Delete("testkey")
}
@@ -35,15 +37,19 @@ func (suite *RedisConfigSuite) TestSet() {
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, 0)
storedValue, found := suite.redisConfig.Get(key)
err := suite.redisConfig.Set(key, value, 0)
assert.NoError(suite.T(), err)
storedValue, found, err := suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
// Test overwriting an existing key-value pair
newValue := []byte("newvalue")
suite.redisConfig.Set(key, newValue, 0)
storedValue, found = suite.redisConfig.Get(key)
err = suite.redisConfig.Set(key, newValue, 0)
assert.NoError(suite.T(), err)
storedValue, found, err = suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.True(suite.T(), found)
assert.Equal(suite.T(), newValue, storedValue)
@@ -57,16 +63,20 @@ func (suite *RedisConfigSuite) TestSetWithExpiry() {
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, expiry)
storedValue, found := suite.redisConfig.Get(key)
err := suite.redisConfig.Set(key, value, expiry)
assert.NoError(suite.T(), err)
storedValue, found, err := suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
_, found = suite.redisConfig.Get(key)
_, found, err = suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.True(suite.T(), found, "Key should exist")
// Test that key expires after the specified time
suite.redis_server.FastForward(3 * time.Second)
_, found = suite.redisConfig.Get(key)
_, found, err = suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.False(suite.T(), found, "Key should have expired after 2 seconds")
suite.redisConfig.Delete(key) // Clean up after the test
@@ -75,8 +85,10 @@ func (suite *RedisConfigSuite) TestSetWithExpiry() {
func (suite *RedisConfigSuite) TestGet() {
key := "testkeyget"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
storedValue, found := suite.redisConfig.Get(key)
err := suite.redisConfig.Set(key, value, 0) // Set the key-value pair
assert.NoError(suite.T(), err)
storedValue, found, err := suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
}
@@ -84,9 +96,12 @@ func (suite *RedisConfigSuite) TestGet() {
func (suite *RedisConfigSuite) TestDeleteKey() {
key := "testkeydelete"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
suite.redisConfig.Delete(key)
_, found := suite.redisConfig.Get(key)
err := suite.redisConfig.Set(key, value, 0) // Set the key-value pair
assert.NoError(suite.T(), err)
err = suite.redisConfig.Delete(key)
assert.NoError(suite.T(), err)
_, found, err := suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.False(suite.T(), found)
}
@@ -94,20 +109,27 @@ func (suite *RedisConfigSuite) TestCheckIfKeyExists() {
ttl := time.Duration(10) * time.Second
key := "testkeyifexists"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, ttl) // Set the key-value pair
_, found := suite.redisConfig.Get(key)
err := suite.redisConfig.Set(key, value, ttl) // Set the key-value pair
assert.NoError(suite.T(), err)
_, found, err := suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.True(suite.T(), found)
suite.redisConfig.Delete(key)
_, found = suite.redisConfig.Get(key)
err = suite.redisConfig.Delete(key)
assert.NoError(suite.T(), err)
_, found, err = suite.redisConfig.Get(key)
assert.NoError(suite.T(), err)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestGetKeys() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
err := suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
assert.NoError(suite.T(), err)
err = suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
assert.NoError(suite.T(), err)
err = suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
assert.NoError(suite.T(), err)
keys, _ := suite.redisConfig.client.Keys(suite.redisConfig.ctx, "testkey*").Result()
expectedKeys := []string{"testkey1", "testkey2"}
@@ -122,9 +144,15 @@ func (suite *RedisConfigSuite) TestGetKeysCount() {
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
assert.Equal(suite.T(), 2, suite.redisConfig.CountQueriesWithPattern("testkey*"))
assert.Equal(suite.T(), 1, suite.redisConfig.CountQueriesWithPattern("otherkey*"))
assert.Equal(suite.T(), int64(3), suite.redisConfig.CountQueries())
count1, err := suite.redisConfig.CountQueriesWithPattern("testkey*")
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, count1)
count2, err := suite.redisConfig.CountQueriesWithPattern("otherkey*")
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, count2)
count3, err := suite.redisConfig.CountQueries()
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), int64(3), count3)
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}
+104
View File
@@ -0,0 +1,104 @@
package libpack_cache_redis
import (
"time"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
// CacheWrapper wraps RedisConfig to implement the CacheClient interface
// without returning errors, for backward compatibility
type CacheWrapper struct {
redis *RedisConfig
logger *libpack_logger.Logger
}
// NewCacheWrapper creates a new cache wrapper
func NewCacheWrapper(config *RedisConfig, logger *libpack_logger.Logger) *CacheWrapper {
if logger == nil {
logger = &libpack_logger.Logger{}
}
return &CacheWrapper{
redis: config,
logger: logger,
}
}
// Set stores a value with the given TTL
func (w *CacheWrapper) Set(key string, value []byte, ttl time.Duration) {
if err := w.redis.Set(key, value, ttl); err != nil {
w.logger.Error(&libpack_logger.LogMessage{
Message: "Redis set error",
Pairs: map[string]interface{}{
"error": err.Error(),
"key": key,
},
})
}
}
// Get retrieves a value
func (w *CacheWrapper) Get(key string) ([]byte, bool) {
value, found, err := w.redis.Get(key)
if err != nil {
w.logger.Error(&libpack_logger.LogMessage{
Message: "Redis get error",
Pairs: map[string]interface{}{
"error": err.Error(),
"key": key,
},
})
return nil, false
}
return value, found
}
// Delete removes a key
func (w *CacheWrapper) Delete(key string) {
if err := w.redis.Delete(key); err != nil {
w.logger.Error(&libpack_logger.LogMessage{
Message: "Redis delete error",
Pairs: map[string]interface{}{
"error": err.Error(),
"key": key,
},
})
}
}
// Clear removes all keys
func (w *CacheWrapper) Clear() {
if err := w.redis.Clear(); err != nil {
w.logger.Error(&libpack_logger.LogMessage{
Message: "Redis clear error",
Pairs: map[string]interface{}{
"error": err.Error(),
},
})
}
}
// CountQueries returns the number of queries
func (w *CacheWrapper) CountQueries() int64 {
count, err := w.redis.CountQueries()
if err != nil {
w.logger.Error(&libpack_logger.LogMessage{
Message: "Redis count queries error",
Pairs: map[string]interface{}{
"error": err.Error(),
},
})
return 0
}
return count
}
// GetMemoryUsage returns 0 for Redis (not applicable)
func (w *CacheWrapper) GetMemoryUsage() int64 {
return 0
}
// GetMaxMemorySize returns 0 for Redis (not applicable)
func (w *CacheWrapper) GetMaxMemorySize() int64 {
return 0
}