mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
Add LRU cache support.
This commit is contained in:
@@ -155,6 +155,7 @@ You can still use the non-prefixed environment variables in the spirit of the ba
|
|||||||
| `CACHE_TTL` | The cache TTL | `60` |
|
| `CACHE_TTL` | The cache TTL | `60` |
|
||||||
| `CACHE_MAX_MEMORY_SIZE` | Maximum memory size for cache in MB | `100` |
|
| `CACHE_MAX_MEMORY_SIZE` | Maximum memory size for cache in MB | `100` |
|
||||||
| `CACHE_MAX_ENTRIES` | Maximum number of entries in cache | `10000` |
|
| `CACHE_MAX_ENTRIES` | Maximum number of entries in cache | `10000` |
|
||||||
|
| `CACHE_USE_LRU` | Use LRU eviction algorithm (see [Cache Eviction](#cache-eviction-algorithms)) | `false` |
|
||||||
| `CACHE_PER_USER_DISABLED` | **⚠️ SECURITY**: Disable per-user cache isolation | `false` (**DO NOT** set to `true` in multi-user apps) |
|
| `CACHE_PER_USER_DISABLED` | **⚠️ SECURITY**: Disable per-user cache isolation | `false` (**DO NOT** set to `true` in multi-user apps) |
|
||||||
| `ENABLE_REDIS_CACHE` | Enable distributed Redis cache | `false` |
|
| `ENABLE_REDIS_CACHE` | Enable distributed Redis cache | `false` |
|
||||||
| `CACHE_REDIS_URL` | URL to redis server / cluster endpoint | `localhost:6379` |
|
| `CACHE_REDIS_URL` | URL to redis server / cluster endpoint | `localhost:6379` |
|
||||||
@@ -439,6 +440,32 @@ These features ensure the cache runs efficiently even under high load and with l
|
|||||||
Since version `0.5.30` the cache is gzipped in the memory, which should optimise the memory usage quite significantly.
|
Since version `0.5.30` the cache is gzipped in the memory, which should optimise the memory usage quite significantly.
|
||||||
Since version `0.15.48` the you can also use the distributed Redis cache.
|
Since version `0.15.48` the you can also use the distributed Redis cache.
|
||||||
|
|
||||||
|
#### Cache Eviction Algorithms
|
||||||
|
|
||||||
|
The proxy supports two cache eviction strategies:
|
||||||
|
|
||||||
|
**Standard (default):** Uses Go's `sync.Map` with approximate eviction. When memory limits are reached, entries are evicted based on iteration order (pseudo-random). This is memory-efficient and has excellent concurrent read performance.
|
||||||
|
|
||||||
|
**LRU (Least Recently Used):** Uses a proper LRU algorithm with a linked list to track access order. When limits are reached, the least recently accessed entries are evicted first. Enable with `CACHE_USE_LRU=true`.
|
||||||
|
|
||||||
|
| Feature | Standard | LRU |
|
||||||
|
|---------|----------|-----|
|
||||||
|
| Eviction order | Pseudo-random | Least recently used |
|
||||||
|
| Read performance | Excellent | Good |
|
||||||
|
| Memory tracking | Approximate | Precise |
|
||||||
|
| Best for | High read throughput | Cache hit optimization |
|
||||||
|
|
||||||
|
*LRU cache configuration:*
|
||||||
|
```bash
|
||||||
|
GMP_ENABLE_GLOBAL_CACHE=true
|
||||||
|
GMP_CACHE_TTL=300
|
||||||
|
GMP_CACHE_USE_LRU=true
|
||||||
|
GMP_CACHE_MAX_MEMORY_SIZE=200
|
||||||
|
GMP_CACHE_MAX_ENTRIES=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
Use LRU when cache hit rate is critical and you want to ensure frequently accessed data stays cached. Use Standard (default) for maximum read throughput with less memory overhead.
|
||||||
|
|
||||||
#### Read-only endpoint
|
#### Read-only endpoint
|
||||||
|
|
||||||
You can now specify the read-only GraphQL endpoint by setting the `HOST_GRAPHQL_READONLY` environment variable. The default value is empty, preventing the proxy from using the read-only endpoint for the queries and directing all the requests to the main endpoint specified as `HOST_GRAPHQL`. If the `HOST_GRAPHQL_READONLY` is set, the proxy will use the read-only endpoint for the queries with the `query` type and the main endpoint for the `mutation` type queries. Format of the read-only endpoint is the same as `HOST_GRAPHQL` endpoint, for example `http://localhost:8080/`.
|
You can now specify the read-only GraphQL endpoint by setting the `HOST_GRAPHQL_READONLY` environment variable. The default value is empty, preventing the proxy from using the read-only endpoint for the queries and directing all the requests to the main endpoint specified as `HOST_GRAPHQL`. If the `HOST_GRAPHQL_READONLY` is set, the proxy will use the read-only endpoint for the queries with the `query` type and the main endpoint for the `mutation` type queries. Format of the read-only endpoint is the same as `HOST_GRAPHQL` endpoint, for example `http://localhost:8080/`.
|
||||||
|
|||||||
+1
-1
@@ -429,7 +429,7 @@ func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// clearCache clears the cache
|
// clearCache clears the cache
|
||||||
func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
||||||
// TODO: Implement cache clearing
|
libpack_cache.CacheClear()
|
||||||
return c.JSON(map[string]interface{}{
|
return c.JSON(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Cache cleared successfully",
|
"message": "Cache cleared successfully",
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ func TestAdminDashboard_GetCacheStats(t *testing.T) {
|
|||||||
CacheRedisEnable bool
|
CacheRedisEnable bool
|
||||||
CacheMaxMemorySize int
|
CacheMaxMemorySize int
|
||||||
CacheMaxEntries int
|
CacheMaxEntries int
|
||||||
|
CacheUseLRU bool
|
||||||
GraphQLQueryCacheSize int
|
GraphQLQueryCacheSize int
|
||||||
PerUserCacheDisabled bool
|
PerUserCacheDisabled bool
|
||||||
}{
|
}{
|
||||||
@@ -221,6 +222,7 @@ func TestAdminDashboard_GetCacheStats(t *testing.T) {
|
|||||||
CacheTTL: 60,
|
CacheTTL: 60,
|
||||||
CacheMaxMemorySize: 100,
|
CacheMaxMemorySize: 100,
|
||||||
CacheMaxEntries: 10000,
|
CacheMaxEntries: 10000,
|
||||||
|
CacheUseLRU: false,
|
||||||
PerUserCacheDisabled: false,
|
PerUserCacheDisabled: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+28
-27
@@ -27,6 +27,7 @@ type CacheConfig struct {
|
|||||||
Memory struct {
|
Memory struct {
|
||||||
MaxMemorySize int64 `json:"max_memory_size"` // Maximum memory size in bytes
|
MaxMemorySize int64 `json:"max_memory_size"` // Maximum memory size in bytes
|
||||||
MaxEntries int64 `json:"max_entries"` // Maximum number of entries
|
MaxEntries int64 `json:"max_entries"` // Maximum number of entries
|
||||||
|
UseLRU bool `json:"use_lru"` // Use LRU eviction algorithm instead of random eviction
|
||||||
}
|
}
|
||||||
TTL int `json:"ttl"`
|
TTL int `json:"ttl"`
|
||||||
IncludeUserContext bool `json:"include_user_context"` // Include user ID and role in cache key
|
IncludeUserContext bool `json:"include_user_context"` // Include user ID and role in cache key
|
||||||
@@ -96,16 +97,6 @@ func CalculateHash(c *fiber.Ctx, userID string, userRole string) string {
|
|||||||
return strutil.Md5(cacheKeyData)
|
return strutil.Md5(cacheKeyData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalculateHashLegacy generates a cache hash using only the request body (DEPRECATED).
|
|
||||||
// This function exists for backward compatibility only and should NOT be used
|
|
||||||
// in production multi-user applications as it creates a security vulnerability
|
|
||||||
// where users can see each other's cached data.
|
|
||||||
//
|
|
||||||
// Deprecated: Use CalculateHash with user context instead.
|
|
||||||
func CalculateHashLegacy(c *fiber.Ctx) string {
|
|
||||||
return strutil.Md5(c.Body())
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnableCache(cfg *CacheConfig) {
|
func EnableCache(cfg *CacheConfig) {
|
||||||
if cfg.Logger == nil {
|
if cfg.Logger == nil {
|
||||||
cfg.Logger = libpack_logger.New()
|
cfg.Logger = libpack_logger.New()
|
||||||
@@ -134,34 +125,41 @@ func EnableCache(cfg *CacheConfig) {
|
|||||||
cfg.Client = libpack_cache_redis.NewCacheWrapper(redisClient, cfg.Logger)
|
cfg.Client = libpack_cache_redis.NewCacheWrapper(redisClient, cfg.Logger)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Calculate memory and entry limits
|
||||||
|
maxMemory := cfg.Memory.MaxMemorySize
|
||||||
|
if maxMemory <= 0 {
|
||||||
|
maxMemory = libpack_cache_memory.DefaultMaxMemorySize
|
||||||
|
}
|
||||||
|
|
||||||
|
maxEntries := cfg.Memory.MaxEntries
|
||||||
|
if maxEntries <= 0 {
|
||||||
|
maxEntries = libpack_cache_memory.DefaultMaxCacheSize
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheType := "standard"
|
||||||
|
if cfg.Memory.UseLRU {
|
||||||
|
cacheType = "LRU"
|
||||||
|
}
|
||||||
|
|
||||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||||
Message: "Using in-memory cache",
|
Message: "Using in-memory cache",
|
||||||
Pairs: map[string]interface{}{
|
Pairs: map[string]interface{}{
|
||||||
"max_memory_size_bytes": cfg.Memory.MaxMemorySize,
|
"type": cacheType,
|
||||||
"max_entries": cfg.Memory.MaxEntries,
|
"max_memory_size_bytes": maxMemory,
|
||||||
|
"max_entries": maxEntries,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use memory size and entry limits if configured, otherwise use defaults
|
if cfg.Memory.UseLRU {
|
||||||
if cfg.Memory.MaxMemorySize > 0 || cfg.Memory.MaxEntries > 0 {
|
// Use LRU cache with proper eviction algorithm
|
||||||
maxMemory := cfg.Memory.MaxMemorySize
|
cfg.Client = libpack_cache_memory.NewLRUMemoryCache(maxMemory, maxEntries)
|
||||||
if maxMemory <= 0 {
|
} else {
|
||||||
maxMemory = libpack_cache_memory.DefaultMaxMemorySize
|
// Use standard sync.Map-based cache
|
||||||
}
|
|
||||||
|
|
||||||
maxEntries := cfg.Memory.MaxEntries
|
|
||||||
if maxEntries <= 0 {
|
|
||||||
maxEntries = libpack_cache_memory.DefaultMaxCacheSize
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Client = libpack_cache_memory.NewWithSize(
|
cfg.Client = libpack_cache_memory.NewWithSize(
|
||||||
time.Duration(cfg.TTL)*time.Second,
|
time.Duration(cfg.TTL)*time.Second,
|
||||||
maxMemory,
|
maxMemory,
|
||||||
maxEntries,
|
maxEntries,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// Backward compatibility
|
|
||||||
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config = cfg
|
config = cfg
|
||||||
@@ -271,6 +269,9 @@ func CacheGetQueries() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CacheClear() {
|
func CacheClear() {
|
||||||
|
if !IsCacheInitialized() {
|
||||||
|
return
|
||||||
|
}
|
||||||
config.Client.Clear()
|
config.Client.Clear()
|
||||||
cacheStats = &CacheStats{}
|
cacheStats = &CacheStats{}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
@@ -279,3 +279,8 @@ func (c *LRUMemoryCache) GetMemoryUsage() int64 {
|
|||||||
func (c *LRUMemoryCache) GetMaxMemorySize() int64 {
|
func (c *LRUMemoryCache) GetMaxMemorySize() int64 {
|
||||||
return c.maxMemorySize
|
return c.maxMemorySize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountQueries returns the number of entries in the cache
|
||||||
|
func (c *LRUMemoryCache) CountQueries() int64 {
|
||||||
|
return atomic.LoadInt64(&c.currentCount)
|
||||||
|
}
|
||||||
|
|||||||
+343
@@ -0,0 +1,343 @@
|
|||||||
|
package libpack_cache_memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LRUMemoryCacheTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLRUMemoryCacheTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(LRUMemoryCacheTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestNewLRUMemoryCache() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100) // 1MB, 100 entries
|
||||||
|
suite.NotNil(cache)
|
||||||
|
suite.Equal(int64(0), cache.CountQueries())
|
||||||
|
suite.Equal(int64(0), cache.GetMemoryUsage())
|
||||||
|
suite.Equal(int64(1024*1024), cache.GetMaxMemorySize())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestSetAndGet() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
// Set a value
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
|
||||||
|
// Get the value
|
||||||
|
val, found := cache.Get("key1")
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal([]byte("value1"), val)
|
||||||
|
|
||||||
|
// Get non-existent key
|
||||||
|
val, found = cache.Get("nonexistent")
|
||||||
|
suite.False(found)
|
||||||
|
suite.Nil(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestUpdateExisting() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
cache.Set("key1", []byte("value2"), 5*time.Second)
|
||||||
|
|
||||||
|
val, found := cache.Get("key1")
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal([]byte("value2"), val)
|
||||||
|
suite.Equal(int64(1), cache.CountQueries())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestDelete() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
suite.Equal(int64(1), cache.CountQueries())
|
||||||
|
|
||||||
|
cache.Delete("key1")
|
||||||
|
suite.Equal(int64(0), cache.CountQueries())
|
||||||
|
|
||||||
|
val, found := cache.Get("key1")
|
||||||
|
suite.False(found)
|
||||||
|
suite.Nil(val)
|
||||||
|
|
||||||
|
// Delete non-existent key should not panic
|
||||||
|
cache.Delete("nonexistent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestClear() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||||
|
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||||
|
suite.Equal(int64(3), cache.CountQueries())
|
||||||
|
|
||||||
|
cache.Clear()
|
||||||
|
suite.Equal(int64(0), cache.CountQueries())
|
||||||
|
suite.Equal(int64(0), cache.GetMemoryUsage())
|
||||||
|
|
||||||
|
_, found := cache.Get("key1")
|
||||||
|
suite.False(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestExpiration() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Should exist immediately
|
||||||
|
val, found := cache.Get("key1")
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal([]byte("value1"), val)
|
||||||
|
|
||||||
|
// Wait for expiration
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
// Should be expired
|
||||||
|
val, found = cache.Get("key1")
|
||||||
|
suite.False(found)
|
||||||
|
suite.Nil(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestEvictionByCount() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 3) // Max 3 entries
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||||
|
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||||
|
|
||||||
|
// All 3 should exist
|
||||||
|
_, found := cache.Get("key1")
|
||||||
|
suite.True(found)
|
||||||
|
_, found = cache.Get("key2")
|
||||||
|
suite.True(found)
|
||||||
|
_, found = cache.Get("key3")
|
||||||
|
suite.True(found)
|
||||||
|
|
||||||
|
// Add 4th entry - should evict oldest (key1)
|
||||||
|
cache.Set("key4", []byte("value4"), 5*time.Second)
|
||||||
|
|
||||||
|
suite.Equal(int64(3), cache.CountQueries())
|
||||||
|
|
||||||
|
// key1 should be evicted (it was least recently used)
|
||||||
|
_, found = cache.Get("key1")
|
||||||
|
suite.False(found)
|
||||||
|
|
||||||
|
// Others should still exist
|
||||||
|
_, found = cache.Get("key2")
|
||||||
|
suite.True(found)
|
||||||
|
_, found = cache.Get("key3")
|
||||||
|
suite.True(found)
|
||||||
|
_, found = cache.Get("key4")
|
||||||
|
suite.True(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestLRUOrder() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 3) // Max 3 entries
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||||
|
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||||
|
|
||||||
|
// Access key1 to make it recently used
|
||||||
|
cache.Get("key1")
|
||||||
|
|
||||||
|
// Add key4 - should evict key2 (now least recently used)
|
||||||
|
cache.Set("key4", []byte("value4"), 5*time.Second)
|
||||||
|
|
||||||
|
// key2 should be evicted
|
||||||
|
_, found := cache.Get("key2")
|
||||||
|
suite.False(found)
|
||||||
|
|
||||||
|
// key1 should still exist (was accessed recently)
|
||||||
|
_, found = cache.Get("key1")
|
||||||
|
suite.True(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestEvictionByMemory() {
|
||||||
|
// Small memory limit - 500 bytes
|
||||||
|
cache := NewLRUMemoryCache(500, 100)
|
||||||
|
|
||||||
|
// Each entry has ~64 bytes overhead + key + value
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||||
|
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||||
|
|
||||||
|
// Add large entry that should trigger eviction
|
||||||
|
largeValue := make([]byte, 200)
|
||||||
|
cache.Set("large", largeValue, 5*time.Second)
|
||||||
|
|
||||||
|
// Memory should be under limit
|
||||||
|
suite.LessOrEqual(cache.GetMemoryUsage(), int64(500))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestCompression() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
// Create a compressible value (> 1KB to trigger compression)
|
||||||
|
largeValue := make([]byte, 2048)
|
||||||
|
for i := range largeValue {
|
||||||
|
largeValue[i] = 'A' // Highly compressible
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Set("compressed", largeValue, 5*time.Second)
|
||||||
|
|
||||||
|
// Should be able to retrieve it correctly
|
||||||
|
val, found := cache.Get("compressed")
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(largeValue, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestGetStats() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||||
|
|
||||||
|
stats := cache.GetStats()
|
||||||
|
suite.Equal(int64(2), stats["entries"])
|
||||||
|
suite.Equal(int64(1024*1024), stats["max_memory"])
|
||||||
|
suite.Equal(int64(100), stats["max_entries"])
|
||||||
|
suite.NotNil(stats["memory_bytes"])
|
||||||
|
suite.NotNil(stats["fill_percent"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestConcurrentAccess() {
|
||||||
|
cache := NewLRUMemoryCache(10*1024*1024, 1000)
|
||||||
|
const numGoroutines = 50
|
||||||
|
const numOperations = 500
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(numGoroutines * 3) // readers, writers, deleters
|
||||||
|
|
||||||
|
// Writers
|
||||||
|
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))
|
||||||
|
cache.Set(key, value, 5*time.Second)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readers
|
||||||
|
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)
|
||||||
|
cache.Get(key)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleters
|
||||||
|
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%100)
|
||||||
|
cache.Delete(key)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestCleanExpiredEntries() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
cache.Set("expire1", []byte("value1"), 50*time.Millisecond)
|
||||||
|
cache.Set("expire2", []byte("value2"), 50*time.Millisecond)
|
||||||
|
cache.Set("keep", []byte("value3"), 5*time.Second)
|
||||||
|
|
||||||
|
suite.Equal(int64(3), cache.CountQueries())
|
||||||
|
|
||||||
|
// Wait for some to expire
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Clean expired entries
|
||||||
|
cache.CleanExpiredEntries()
|
||||||
|
|
||||||
|
// Only "keep" should remain
|
||||||
|
suite.Equal(int64(1), cache.CountQueries())
|
||||||
|
|
||||||
|
_, found := cache.Get("keep")
|
||||||
|
suite.True(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LRUMemoryCacheTestSuite) TestCountQueries() {
|
||||||
|
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||||
|
|
||||||
|
suite.Equal(int64(0), cache.CountQueries())
|
||||||
|
|
||||||
|
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||||
|
suite.Equal(int64(1), cache.CountQueries())
|
||||||
|
|
||||||
|
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||||
|
suite.Equal(int64(2), cache.CountQueries())
|
||||||
|
|
||||||
|
cache.Delete("key1")
|
||||||
|
suite.Equal(int64(1), cache.CountQueries())
|
||||||
|
|
||||||
|
cache.Clear()
|
||||||
|
suite.Equal(int64(0), cache.CountQueries())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmarks
|
||||||
|
|
||||||
|
func BenchmarkLRUMemoryCacheSet(b *testing.B) {
|
||||||
|
cache := NewLRUMemoryCache(100*1024*1024, 100000)
|
||||||
|
value := []byte("benchmark-value")
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
key := fmt.Sprintf("key-%d", i)
|
||||||
|
cache.Set(key, value, 5*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLRUMemoryCacheGet(b *testing.B) {
|
||||||
|
cache := NewLRUMemoryCache(100*1024*1024, 100000)
|
||||||
|
value := []byte("benchmark-value")
|
||||||
|
|
||||||
|
// Pre-populate
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
key := fmt.Sprintf("key-%d", i)
|
||||||
|
cache.Set(key, value, 5*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
key := fmt.Sprintf("key-%d", i%10000)
|
||||||
|
cache.Get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLRUMemoryCacheConcurrent(b *testing.B) {
|
||||||
|
cache := NewLRUMemoryCache(100*1024*1024, 100000)
|
||||||
|
value := []byte("benchmark-value")
|
||||||
|
|
||||||
|
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, 5*time.Second)
|
||||||
|
} else {
|
||||||
|
cache.Get(key)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -106,117 +106,8 @@ func (e *ProxyError) WithMetadata(key string, value interface{}) *ProxyError {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common error constructors
|
|
||||||
|
|
||||||
// NewConnectionError creates a connection-related error
|
|
||||||
func NewConnectionError(err error) *ProxyError {
|
|
||||||
code := ErrCodeConnectionRefused
|
|
||||||
if err != nil {
|
|
||||||
errStr := err.Error()
|
|
||||||
if contains(errStr, "reset") {
|
|
||||||
code = ErrCodeConnectionReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewProxyError(code, "Failed to connect to backend", 502, true).
|
|
||||||
WithCause(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTimeoutError creates a timeout error
|
|
||||||
func NewTimeoutError(err error) *ProxyError {
|
|
||||||
return NewProxyError(ErrCodeTimeout, "Request timed out", 504, false).
|
|
||||||
WithCause(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCircuitOpenError creates a circuit breaker open error
|
|
||||||
func NewCircuitOpenError() *ProxyError {
|
|
||||||
return NewProxyError(ErrCodeCircuitOpen, "Service temporarily unavailable due to circuit breaker", 503, false).
|
|
||||||
WithDetails("The backend service is currently experiencing issues. Please try again later.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRateLimitError creates a rate limit error
|
|
||||||
func NewRateLimitError(userID, role string) *ProxyError {
|
|
||||||
return NewProxyError(ErrCodeRateLimited, "Rate limit exceeded", 429, false).
|
|
||||||
WithDetails("You have exceeded the rate limit for your role").
|
|
||||||
WithMetadata("user_id", userID).
|
|
||||||
WithMetadata("role", role)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBackendError creates a backend error from status code
|
|
||||||
func NewBackendError(statusCode int, body string) *ProxyError {
|
|
||||||
code := ErrCodeBackendError
|
|
||||||
message := "Backend returned an error"
|
|
||||||
retryable := false
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case statusCode == 429:
|
|
||||||
code = ErrCodeRateLimited
|
|
||||||
message = "Backend rate limit exceeded"
|
|
||||||
retryable = true
|
|
||||||
case statusCode == 503:
|
|
||||||
code = ErrCodeServiceUnavailable
|
|
||||||
message = "Backend service unavailable"
|
|
||||||
retryable = true
|
|
||||||
case statusCode == 502 || statusCode == 504:
|
|
||||||
code = ErrCodeBadGateway
|
|
||||||
message = "Bad gateway"
|
|
||||||
retryable = true
|
|
||||||
case statusCode >= 500:
|
|
||||||
code = ErrCodeBackendError
|
|
||||||
message = "Backend server error"
|
|
||||||
retryable = true
|
|
||||||
case statusCode == 404:
|
|
||||||
code = ErrCodeNotFound
|
|
||||||
message = "Resource not found"
|
|
||||||
case statusCode == 403:
|
|
||||||
code = ErrCodeForbidden
|
|
||||||
message = "Access forbidden"
|
|
||||||
case statusCode == 401:
|
|
||||||
code = ErrCodeUnauthorized
|
|
||||||
message = "Unauthorized"
|
|
||||||
case statusCode >= 400:
|
|
||||||
code = ErrCodeInvalidRequest
|
|
||||||
message = "Invalid request"
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewProxyError(code, message, statusCode, retryable).
|
|
||||||
WithMetadata("backend_status", statusCode).
|
|
||||||
WithMetadata("backend_body", truncateString(body, 500))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInvalidResponseError creates an invalid response error
|
|
||||||
func NewInvalidResponseError(details string) *ProxyError {
|
|
||||||
return NewProxyError(ErrCodeInvalidResponse, "Backend returned invalid response", 502, false).
|
|
||||||
WithDetails(details)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInternalError creates an internal error
|
|
||||||
func NewInternalError(err error) *ProxyError {
|
|
||||||
return NewProxyError(ErrCodeInternalError, "Internal proxy error", 500, false).
|
|
||||||
WithCause(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewContextCanceledError creates a context canceled error
|
|
||||||
func NewContextCanceledError() *ProxyError {
|
|
||||||
return NewProxyError(ErrCodeContextCanceled, "Request canceled", 499, false).
|
|
||||||
WithDetails("The request was canceled by the client")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
return len(s) > 0 && len(substr) > 0 && len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsMiddle(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func truncateString(s string, maxLen int) string {
|
func truncateString(s string, maxLen int) string {
|
||||||
if len(s) <= maxLen {
|
if len(s) <= maxLen {
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ func parseConfig() {
|
|||||||
c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60)
|
c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60)
|
||||||
c.Cache.CacheMaxMemorySize = getDetailsFromEnv("CACHE_MAX_MEMORY_SIZE", 100) // Default 100MB
|
c.Cache.CacheMaxMemorySize = getDetailsFromEnv("CACHE_MAX_MEMORY_SIZE", 100) // Default 100MB
|
||||||
c.Cache.CacheMaxEntries = getDetailsFromEnv("CACHE_MAX_ENTRIES", 10000) // Default 10000 entries
|
c.Cache.CacheMaxEntries = getDetailsFromEnv("CACHE_MAX_ENTRIES", 10000) // Default 10000 entries
|
||||||
|
c.Cache.CacheUseLRU = getDetailsFromEnv("CACHE_USE_LRU", false) // Use LRU eviction algorithm
|
||||||
// GraphQL query parsing cache - auto-calculate based on CPU cores if not set
|
// GraphQL query parsing cache - auto-calculate based on CPU cores if not set
|
||||||
c.Cache.GraphQLQueryCacheSize = getDetailsFromEnv("GRAPHQL_QUERY_CACHE_SIZE", runtime.GOMAXPROCS(0)*250)
|
c.Cache.GraphQLQueryCacheSize = getDetailsFromEnv("GRAPHQL_QUERY_CACHE_SIZE", runtime.GOMAXPROCS(0)*250)
|
||||||
|
|
||||||
@@ -390,9 +391,16 @@ func parseConfig() {
|
|||||||
// Memory cache configurations
|
// Memory cache configurations
|
||||||
cacheConfig.Memory.MaxMemorySize = int64(cfg.Cache.CacheMaxMemorySize) * 1024 * 1024 // Convert MB to bytes
|
cacheConfig.Memory.MaxMemorySize = int64(cfg.Cache.CacheMaxMemorySize) * 1024 * 1024 // Convert MB to bytes
|
||||||
cacheConfig.Memory.MaxEntries = int64(cfg.Cache.CacheMaxEntries)
|
cacheConfig.Memory.MaxEntries = int64(cfg.Cache.CacheMaxEntries)
|
||||||
|
cacheConfig.Memory.UseLRU = cfg.Cache.CacheUseLRU
|
||||||
|
|
||||||
|
cacheType := "standard"
|
||||||
|
if cfg.Cache.CacheUseLRU {
|
||||||
|
cacheType = "LRU"
|
||||||
|
}
|
||||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||||
Message: "Configuring memory cache with limits",
|
Message: "Configuring memory cache with limits",
|
||||||
Pairs: map[string]interface{}{
|
Pairs: map[string]interface{}{
|
||||||
|
"type": cacheType,
|
||||||
"max_memory_mb": cfg.Cache.CacheMaxMemorySize,
|
"max_memory_mb": cfg.Cache.CacheMaxMemorySize,
|
||||||
"max_entries": cfg.Cache.CacheMaxEntries,
|
"max_entries": cfg.Cache.CacheMaxEntries,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,3 @@ func (ms *MetricsSetup) RegisterDefaultMetrics() {
|
|||||||
ms.RegisterMetricsCounter(MetricsCacheMiss, nil)
|
ms.RegisterMetricsCounter(MetricsCacheMiss, nil)
|
||||||
ms.RegisterMetricsCounter(MetricsQueriesCached, nil)
|
ms.RegisterMetricsCounter(MetricsQueriesCached, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MetricsSetup) RegisterGoMetrics() {
|
|
||||||
// TODO: metrics.WriteProcessMetrics(ms.metrics_set)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type config struct {
|
|||||||
CacheRedisEnable bool
|
CacheRedisEnable bool
|
||||||
CacheMaxMemorySize int
|
CacheMaxMemorySize int
|
||||||
CacheMaxEntries int
|
CacheMaxEntries int
|
||||||
|
CacheUseLRU bool // Use LRU eviction algorithm instead of random eviction
|
||||||
GraphQLQueryCacheSize int // Max number of parsed GraphQL queries to cache
|
GraphQLQueryCacheSize int // Max number of parsed GraphQL queries to cache
|
||||||
PerUserCacheDisabled bool // Disable per-user cache isolation (SECURITY RISK - not recommended)
|
PerUserCacheDisabled bool // Disable per-user cache isolation (SECURITY RISK - not recommended)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user