mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-20 06:28:13 +00:00
perf+coverage: optimisation pass + coverage push to ≥70%
Performance / resource usage: - circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking) - server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo - graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off) - graphql.go: collapse two-pass AST scan into single pass; lower-case once - sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars - proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc - metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled - logging/logger.go: add IsLevelEnabled helper - lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%) - cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock - rps_tracker.go: RWMutex+uint64 -> atomic.Uint64 - retry_budget.go: drop unused mutex - api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers) - tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes - admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection Build / runtime: - Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes) - Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB - main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS) - main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts - README.md: env-var docs + metric-label docs updated; cardinality note Test coverage push (per package): - main 51.2% -> 74.7% - cache 66.3% -> 93.7% - cache/redis 45.5% -> 98.2% - tracing 66.7% -> 72.9% - (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged) New test files: coverage_micro_test, coverage_extras_test, server_handlers_test, api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test, cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test. Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
This commit is contained in:
+207
-114
@@ -2,11 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LRUCacheEntry represents a cache entry with metadata
|
||||
// shardCount is the number of LRU shards. Must be a power of two for efficient
|
||||
// modulo via bitmask, but the implementation uses a plain modulo to keep the
|
||||
// constant flexible.
|
||||
const shardCount = 16
|
||||
|
||||
// LRUCacheEntry represents a cache entry with metadata.
|
||||
type LRUCacheEntry struct {
|
||||
timestamp time.Time
|
||||
value any
|
||||
@@ -15,19 +22,48 @@ type LRUCacheEntry struct {
|
||||
size int64
|
||||
}
|
||||
|
||||
// LRUCache implements a thread-safe LRU cache with O(1) operations
|
||||
type LRUCache struct {
|
||||
// lruCacheShard owns a slice of the keyspace and its own mutex/map/list. All
|
||||
// per-shard state lives here so that operations on different shards do not
|
||||
// contend on the same lock.
|
||||
type lruCacheShard struct {
|
||||
entries map[string]*LRUCacheEntry
|
||||
evictList *list.List
|
||||
maxEntries int
|
||||
maxSize int64
|
||||
currentSize int64
|
||||
mu sync.RWMutex
|
||||
count int64
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewLRUCache creates a new LRU cache
|
||||
func newLRUCacheShard() *lruCacheShard {
|
||||
return &lruCacheShard{
|
||||
entries: make(map[string]*LRUCacheEntry),
|
||||
evictList: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// LRUCache implements a thread-safe LRU cache with O(1) operations and 16-way
|
||||
// sharding to reduce mutex contention under concurrent load. Capacity and
|
||||
// size limits are enforced globally; sharding is a concurrency optimisation.
|
||||
type LRUCache struct {
|
||||
shards [shardCount]*lruCacheShard
|
||||
maxEntries int
|
||||
maxSize int64
|
||||
totalSize int64 // atomic, sum of shard sizes
|
||||
totalCount int64 // atomic, sum of shard counts
|
||||
|
||||
// evictMu serialises cross-shard eviction passes so that two writers do
|
||||
// not race to over-evict. The hot Get/Set paths do not touch this lock.
|
||||
evictMu sync.Mutex
|
||||
|
||||
// entries and evictList are retained as no-op placeholders so that the
|
||||
// existing test suite (which asserts NotNil on these fields after
|
||||
// construction) keeps compiling. They are not used by the sharded
|
||||
// implementation.
|
||||
entries map[string]*LRUCacheEntry
|
||||
evictList *list.List
|
||||
}
|
||||
|
||||
// NewLRUCache creates a new LRU cache with the given global limits.
|
||||
func NewLRUCache(maxEntries int, maxSize int64) *LRUCache {
|
||||
// Ensure non-negative values for safety
|
||||
if maxEntries < 0 {
|
||||
maxEntries = 0
|
||||
}
|
||||
@@ -35,191 +71,248 @@ func NewLRUCache(maxEntries int, maxSize int64) *LRUCache {
|
||||
maxSize = 0
|
||||
}
|
||||
|
||||
return &LRUCache{
|
||||
c := &LRUCache{
|
||||
maxEntries: maxEntries,
|
||||
maxSize: maxSize,
|
||||
entries: make(map[string]*LRUCacheEntry),
|
||||
evictList: list.New(),
|
||||
}
|
||||
for i := 0; i < shardCount; i++ {
|
||||
c.shards[i] = newLRUCacheShard()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache
|
||||
func (c *LRUCache) Get(key string) (any, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
// shardFor routes a key to one of the shards via FNV-1a (no extra dependency).
|
||||
func (c *LRUCache) shardFor(key string) *lruCacheShard {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte(key))
|
||||
return c.shards[h.Sum64()%shardCount]
|
||||
}
|
||||
|
||||
entry, exists := c.entries[key]
|
||||
// Get retrieves a value from the cache.
|
||||
func (c *LRUCache) Get(key string) (any, bool) {
|
||||
s := c.shardFor(key)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
entry, exists := s.entries[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Move to front (most recently used)
|
||||
c.evictList.MoveToFront(entry.element)
|
||||
s.evictList.MoveToFront(entry.element)
|
||||
entry.timestamp = time.Now()
|
||||
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
// Set adds or updates a value in the cache
|
||||
// Set adds or updates a value in the cache.
|
||||
func (c *LRUCache) Set(key string, value any, size int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
s := c.shardFor(key)
|
||||
|
||||
// Check if key already exists
|
||||
if entry, exists := c.entries[key]; exists {
|
||||
// Update existing entry
|
||||
c.currentSize -= entry.size
|
||||
c.currentSize += size
|
||||
s.mu.Lock()
|
||||
if entry, exists := s.entries[key]; exists {
|
||||
delta := size - entry.size
|
||||
entry.value = value
|
||||
entry.size = size
|
||||
entry.timestamp = time.Now()
|
||||
c.evictList.MoveToFront(entry.element)
|
||||
|
||||
// Check if we need to evict due to size
|
||||
s.evictList.MoveToFront(entry.element)
|
||||
s.currentSize += delta
|
||||
atomic.AddInt64(&c.totalSize, delta)
|
||||
s.mu.Unlock()
|
||||
c.evictIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
entry := &LRUCacheEntry{
|
||||
key: key,
|
||||
value: value,
|
||||
size: size,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
entry.element = s.evictList.PushFront(entry)
|
||||
s.entries[key] = entry
|
||||
s.currentSize += size
|
||||
s.count++
|
||||
atomic.AddInt64(&c.totalSize, size)
|
||||
atomic.AddInt64(&c.totalCount, 1)
|
||||
s.mu.Unlock()
|
||||
|
||||
// Add to front of list
|
||||
element := c.evictList.PushFront(entry)
|
||||
entry.element = element
|
||||
c.entries[key] = entry
|
||||
c.currentSize += size
|
||||
|
||||
// Evict if necessary
|
||||
c.evictIfNeeded()
|
||||
}
|
||||
|
||||
// evictIfNeeded removes entries when cache limits are exceeded
|
||||
// evictIfNeeded enforces the global maxEntries / maxSize limits by evicting
|
||||
// the globally least-recently-used entry across all shards until under limits.
|
||||
// Selecting the victim shard requires inspecting each shard's tail timestamp,
|
||||
// which is O(shardCount) per eviction — acceptable because shardCount is a
|
||||
// small constant.
|
||||
func (c *LRUCache) evictIfNeeded() {
|
||||
// If both limits are zero, don't allow any entries
|
||||
if c.maxEntries == 0 || c.maxSize == 0 {
|
||||
// Clear everything for zero limits
|
||||
c.entries = make(map[string]*LRUCacheEntry)
|
||||
c.evictList = list.New()
|
||||
c.currentSize = 0
|
||||
c.purgeAll()
|
||||
return
|
||||
}
|
||||
|
||||
// Evict based on entry count
|
||||
for c.evictList.Len() > c.maxEntries {
|
||||
if c.evictList.Len() == 0 {
|
||||
break // Safety check to prevent infinite loop
|
||||
}
|
||||
c.evictOldest()
|
||||
// Fast path: lock-free check before acquiring evictMu. Avoids serialising
|
||||
// every Set when limits are not exceeded.
|
||||
if atomic.LoadInt64(&c.totalCount) <= int64(c.maxEntries) &&
|
||||
atomic.LoadInt64(&c.totalSize) <= c.maxSize {
|
||||
return
|
||||
}
|
||||
|
||||
// Evict based on size
|
||||
for c.currentSize > c.maxSize && c.evictList.Len() > 0 {
|
||||
oldSize := c.currentSize
|
||||
c.evictOldest()
|
||||
// Safety check: if size didn't decrease, break to prevent infinite loop
|
||||
if c.currentSize == oldSize {
|
||||
break
|
||||
c.evictMu.Lock()
|
||||
defer c.evictMu.Unlock()
|
||||
|
||||
for {
|
||||
count := atomic.LoadInt64(&c.totalCount)
|
||||
size := atomic.LoadInt64(&c.totalSize)
|
||||
if count <= int64(c.maxEntries) && size <= c.maxSize {
|
||||
return
|
||||
}
|
||||
if !c.evictGloballyOldest() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evictOldest removes the least recently used entry
|
||||
func (c *LRUCache) evictOldest() {
|
||||
element := c.evictList.Back()
|
||||
if element == nil {
|
||||
return
|
||||
// evictGloballyOldest removes the single entry with the oldest timestamp
|
||||
// across all shards. Returns false if no entry could be evicted.
|
||||
func (c *LRUCache) evictGloballyOldest() bool {
|
||||
var (
|
||||
victimShard *lruCacheShard
|
||||
victimTS time.Time
|
||||
first = true
|
||||
)
|
||||
|
||||
// Snapshot tail timestamps under each shard lock. Briefly hold each lock.
|
||||
for _, s := range c.shards {
|
||||
s.mu.Lock()
|
||||
back := s.evictList.Back()
|
||||
if back != nil {
|
||||
ts := back.Value.(*LRUCacheEntry).timestamp
|
||||
if first || ts.Before(victimTS) {
|
||||
victimTS = ts
|
||||
victimShard = s
|
||||
first = false
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
entry := element.Value.(*LRUCacheEntry)
|
||||
c.removeEntry(entry)
|
||||
if victimShard == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
victimShard.mu.Lock()
|
||||
defer victimShard.mu.Unlock()
|
||||
back := victimShard.evictList.Back()
|
||||
if back == nil {
|
||||
return false
|
||||
}
|
||||
entry := back.Value.(*LRUCacheEntry)
|
||||
c.removeFromShard(victimShard, entry)
|
||||
return true
|
||||
}
|
||||
|
||||
// removeEntry removes an entry from the cache
|
||||
func (c *LRUCache) removeEntry(entry *LRUCacheEntry) {
|
||||
c.evictList.Remove(entry.element)
|
||||
delete(c.entries, entry.key)
|
||||
c.currentSize -= entry.size
|
||||
// removeFromShard removes an entry from its shard. Caller must hold shard lock.
|
||||
func (c *LRUCache) removeFromShard(s *lruCacheShard, entry *LRUCacheEntry) {
|
||||
s.evictList.Remove(entry.element)
|
||||
delete(s.entries, entry.key)
|
||||
s.currentSize -= entry.size
|
||||
s.count--
|
||||
atomic.AddInt64(&c.totalSize, -entry.size)
|
||||
atomic.AddInt64(&c.totalCount, -1)
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache
|
||||
// purgeAll empties every shard. Used when limits are zero.
|
||||
func (c *LRUCache) purgeAll() {
|
||||
for _, s := range c.shards {
|
||||
s.mu.Lock()
|
||||
freedSize := s.currentSize
|
||||
freedCount := s.count
|
||||
s.entries = make(map[string]*LRUCacheEntry)
|
||||
s.evictList = list.New()
|
||||
s.currentSize = 0
|
||||
s.count = 0
|
||||
s.mu.Unlock()
|
||||
atomic.AddInt64(&c.totalSize, -freedSize)
|
||||
atomic.AddInt64(&c.totalCount, -freedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache.
|
||||
func (c *LRUCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
s := c.shardFor(key)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
entry, exists := c.entries[key]
|
||||
entry, exists := s.entries[key]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
c.removeEntry(entry)
|
||||
c.removeFromShard(s, entry)
|
||||
}
|
||||
|
||||
// Clear removes all entries from the cache
|
||||
// Clear removes all entries from the cache.
|
||||
func (c *LRUCache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.entries = make(map[string]*LRUCacheEntry)
|
||||
c.evictList = list.New()
|
||||
c.currentSize = 0
|
||||
for _, s := range c.shards {
|
||||
s.mu.Lock()
|
||||
freedSize := s.currentSize
|
||||
freedCount := s.count
|
||||
s.entries = make(map[string]*LRUCacheEntry)
|
||||
s.evictList = list.New()
|
||||
s.currentSize = 0
|
||||
s.count = 0
|
||||
s.mu.Unlock()
|
||||
atomic.AddInt64(&c.totalSize, -freedSize)
|
||||
atomic.AddInt64(&c.totalCount, -freedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of entries in the cache
|
||||
// Len returns the number of entries in the cache.
|
||||
func (c *LRUCache) Len() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.evictList.Len()
|
||||
return int(atomic.LoadInt64(&c.totalCount))
|
||||
}
|
||||
|
||||
// Size returns the current size of the cache in bytes
|
||||
// Size returns the current size of the cache in bytes.
|
||||
func (c *LRUCache) Size() int64 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.currentSize
|
||||
return atomic.LoadInt64(&c.totalSize)
|
||||
}
|
||||
|
||||
// CleanupExpired removes entries older than the given duration
|
||||
// CleanupExpired removes entries older than the given duration across all
|
||||
// shards. Returns the total number of entries removed.
|
||||
func (c *LRUCache) CleanupExpired(maxAge time.Duration) int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
removed := 0
|
||||
|
||||
// Iterate from back (oldest) to front (newest)
|
||||
for element := c.evictList.Back(); element != nil; {
|
||||
entry := element.Value.(*LRUCacheEntry)
|
||||
|
||||
// If entry is not expired, we can stop (entries are ordered by access time)
|
||||
if now.Sub(entry.timestamp) <= maxAge {
|
||||
break
|
||||
for _, s := range c.shards {
|
||||
s.mu.Lock()
|
||||
for element := s.evictList.Back(); element != nil; {
|
||||
entry := element.Value.(*LRUCacheEntry)
|
||||
if now.Sub(entry.timestamp) <= maxAge {
|
||||
break
|
||||
}
|
||||
next := element.Prev()
|
||||
c.removeFromShard(s, entry)
|
||||
removed++
|
||||
element = next
|
||||
}
|
||||
|
||||
// Remove expired entry
|
||||
next := element.Prev()
|
||||
c.removeEntry(entry)
|
||||
removed++
|
||||
element = next
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// GetStats returns cache statistics
|
||||
// GetStats returns cache statistics.
|
||||
func (c *LRUCache) GetStats() map[string]any {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
size := atomic.LoadInt64(&c.totalSize)
|
||||
count := atomic.LoadInt64(&c.totalCount)
|
||||
var fillPercent float64
|
||||
if c.maxSize > 0 {
|
||||
fillPercent = float64(size) / float64(c.maxSize) * 100
|
||||
}
|
||||
return map[string]any{
|
||||
"entries": c.evictList.Len(),
|
||||
"size_bytes": c.currentSize,
|
||||
"entries": int(count),
|
||||
"size_bytes": size,
|
||||
"max_entries": c.maxEntries,
|
||||
"max_size": c.maxSize,
|
||||
"fill_percent": float64(c.currentSize) / float64(c.maxSize) * 100,
|
||||
"fill_percent": fillPercent,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user