mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
cedee416a8
* 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
372 lines
9.7 KiB
Go
372 lines
9.7 KiB
Go
package libpack_cache_memory
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// CompressionThreshold is the minimum size in bytes before a value is compressed
|
|
const CompressionThreshold = 1024 // 1KB
|
|
|
|
// 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,
|
|
maxMemorySize: maxMemorySize,
|
|
maxCacheSize: maxCacheSize,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
compressPool: sync.Pool{
|
|
New: func() interface{} {
|
|
return gzip.NewWriter(nil)
|
|
},
|
|
},
|
|
decompressPool: sync.Pool{
|
|
New: func() interface{} {
|
|
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
|
|
return r
|
|
},
|
|
},
|
|
}
|
|
|
|
// Start cleanup routine with context cancellation
|
|
go cache.cleanupRoutine(globalTTL)
|
|
return cache
|
|
}
|
|
|
|
func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
|
|
// Clean up more frequently when the cache is large
|
|
ticker := time.NewTicker(globalTTL / 4)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
// Context cancelled, exit gracefully
|
|
return
|
|
case <-ticker.C:
|
|
c.CleanExpiredEntries()
|
|
|
|
// 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) {
|
|
// 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)
|
|
|
|
// Only compress if the value is larger than the threshold
|
|
var entry CacheEntry
|
|
if len(value) > CompressionThreshold {
|
|
compressedValue, err := c.compress(value)
|
|
if err == nil && len(compressedValue) < len(value) {
|
|
entry = CacheEntry{
|
|
Value: compressedValue,
|
|
ExpiresAt: expiresAt,
|
|
Compressed: true,
|
|
}
|
|
} else {
|
|
// If compression failed or didn't reduce size, store uncompressed
|
|
entry = CacheEntry{
|
|
Value: value,
|
|
ExpiresAt: expiresAt,
|
|
Compressed: false,
|
|
}
|
|
}
|
|
} else {
|
|
entry = CacheEntry{
|
|
Value: value,
|
|
ExpiresAt: expiresAt,
|
|
Compressed: false,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
func (c *Cache) Get(key string) ([]byte, bool) {
|
|
entry, ok := c.entries.Load(key)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
cacheEntry := entry.(CacheEntry)
|
|
if cacheEntry.ExpiresAt.Before(time.Now()) {
|
|
c.entries.Delete(key)
|
|
atomic.AddInt64(&c.entryCount, -1)
|
|
atomic.AddInt64(&c.memoryUsage, -cacheEntry.MemorySize)
|
|
return nil, false
|
|
}
|
|
|
|
if cacheEntry.Compressed {
|
|
value, err := c.decompress(cacheEntry.Value)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
return cacheEntry.Value, true
|
|
}
|
|
|
|
func (c *Cache) Delete(key string) {
|
|
if entry, exists := c.entries.LoadAndDelete(key); exists {
|
|
cacheEntry := entry.(CacheEntry)
|
|
atomic.AddInt64(&c.entryCount, -1)
|
|
atomic.AddInt64(&c.memoryUsage, -cacheEntry.MemorySize)
|
|
}
|
|
}
|
|
|
|
func (c *Cache) Clear() {
|
|
c.entries.Range(func(key, value interface{}) bool {
|
|
c.entries.Delete(key)
|
|
return true
|
|
})
|
|
atomic.StoreInt64(&c.entryCount, 0)
|
|
atomic.StoreInt64(&c.memoryUsage, 0)
|
|
}
|
|
|
|
func (c *Cache) CountQueries() int64 {
|
|
return atomic.LoadInt64(&c.entryCount)
|
|
}
|
|
|
|
func (c *Cache) compress(data []byte) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
w := c.compressPool.Get().(*gzip.Writer)
|
|
defer c.compressPool.Put(w)
|
|
|
|
w.Reset(&buf)
|
|
if _, err := w.Write(data); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (c *Cache) decompress(data []byte) ([]byte, error) {
|
|
r, ok := c.decompressPool.Get().(*gzip.Reader)
|
|
defer c.decompressPool.Put(r)
|
|
|
|
if !ok || r == nil {
|
|
var err error
|
|
r, err = gzip.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := r.Reset(bytes.NewReader(data)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
_ = r.Close() // Ignore error in defer cleanup
|
|
}()
|
|
return io.ReadAll(r)
|
|
}
|
|
|
|
func (c *Cache) CleanExpiredEntries() {
|
|
now := time.Now()
|
|
c.entries.Range(func(key, value interface{}) bool {
|
|
entry := value.(CacheEntry)
|
|
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
|
|
})
|
|
}
|
|
|
|
// evictOldest removes the oldest n entries from the cache
|
|
func (c *Cache) evictOldest(n int) {
|
|
type keyExpiry struct {
|
|
expiresAt time.Time
|
|
key string
|
|
}
|
|
|
|
// Collect all entries with their expiry times
|
|
entries := make([]keyExpiry, 0, n*2)
|
|
c.entries.Range(func(k, v interface{}) bool {
|
|
key := k.(string)
|
|
entry := v.(CacheEntry)
|
|
entries = append(entries, keyExpiry{entry.ExpiresAt, key})
|
|
return len(entries) < cap(entries)
|
|
})
|
|
|
|
// Sort by expiry time (oldest first)
|
|
// Using a simple selection sort since we only need to find the n oldest
|
|
for i := 0; i < n && i < len(entries); 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|