mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
c2c75d69c0
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.
219 lines
5.9 KiB
Go
219 lines
5.9 KiB
Go
package libpack_cache
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
|
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
|
ta "github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// helper resets package-level globals and returns a cleanup func.
|
|
func withFreshMemoryCache(t *testing.T, ttl time.Duration) func() {
|
|
t.Helper()
|
|
prev := config
|
|
prevStats := cacheStats
|
|
config = &CacheConfig{
|
|
Logger: libpack_logger.New(),
|
|
Client: libpack_cache_memory.New(ttl),
|
|
TTL: int(ttl.Seconds()),
|
|
}
|
|
cacheStats = &CacheStats{}
|
|
return func() {
|
|
config = prev
|
|
cacheStats = prevStats
|
|
}
|
|
}
|
|
|
|
// TestGetCacheMemoryUsage_Initialized covers the initialized branch (was 0%).
|
|
func TestGetCacheMemoryUsage_Initialized_ReturnsNonNegative(t *testing.T) {
|
|
defer withFreshMemoryCache(t, 5*time.Minute)()
|
|
|
|
usage := GetCacheMemoryUsage()
|
|
ta.GreaterOrEqual(t, usage, int64(0))
|
|
}
|
|
|
|
// TestGetCacheMemoryUsage_Uninitialized covers the early-return branch.
|
|
func TestGetCacheMemoryUsage_Uninitialized_ReturnsZero(t *testing.T) {
|
|
prev := config
|
|
config = nil
|
|
defer func() { config = prev }()
|
|
|
|
ta.Equal(t, int64(0), GetCacheMemoryUsage())
|
|
}
|
|
|
|
// TestGetCacheMaxMemorySize_Initialized covers the initialized branch (was 0%).
|
|
func TestGetCacheMaxMemorySize_Initialized_ReturnsPositive(t *testing.T) {
|
|
defer withFreshMemoryCache(t, 5*time.Minute)()
|
|
|
|
maxSize := GetCacheMaxMemorySize()
|
|
ta.Greater(t, maxSize, int64(0))
|
|
}
|
|
|
|
// TestGetCacheMaxMemorySize_Uninitialized covers the early-return branch.
|
|
func TestGetCacheMaxMemorySize_Uninitialized_ReturnsZero(t *testing.T) {
|
|
prev := config
|
|
config = nil
|
|
defer func() { config = prev }()
|
|
|
|
ta.Equal(t, int64(0), GetCacheMaxMemorySize())
|
|
}
|
|
|
|
// TestEnableCache_LRUBranch covers cfg.Memory.UseLRU == true branch in EnableCache.
|
|
func TestEnableCache_LRUBranch_InitializesLRUClient(t *testing.T) {
|
|
prev := config
|
|
prevStats := cacheStats
|
|
defer func() {
|
|
config = prev
|
|
cacheStats = prevStats
|
|
}()
|
|
|
|
cfg := &CacheConfig{
|
|
Logger: libpack_logger.New(),
|
|
TTL: 5,
|
|
}
|
|
cfg.Memory.UseLRU = true
|
|
cfg.Memory.MaxMemorySize = 1024 * 1024
|
|
cfg.Memory.MaxEntries = 100
|
|
|
|
EnableCache(cfg)
|
|
require.NotNil(t, config.Client, "LRU client must be set")
|
|
ta.True(t, IsCacheInitialized())
|
|
|
|
// Verify basic ops work with LRU client.
|
|
CacheStore("lru-key", []byte("lru-val"))
|
|
got := CacheLookup("lru-key")
|
|
ta.Equal(t, []byte("lru-val"), got)
|
|
}
|
|
|
|
// TestEnableCache_NilLogger covers the auto-logger creation branch.
|
|
func TestEnableCache_NilLogger_AutoCreatesLogger(t *testing.T) {
|
|
prev := config
|
|
prevStats := cacheStats
|
|
defer func() {
|
|
config = prev
|
|
cacheStats = prevStats
|
|
}()
|
|
|
|
cfg := &CacheConfig{
|
|
Logger: nil, // deliberately nil
|
|
TTL: 5,
|
|
}
|
|
// Should not panic; logger is created internally.
|
|
ta.NotPanics(t, func() { EnableCache(cfg) })
|
|
ta.NotNil(t, cfg.Logger)
|
|
}
|
|
|
|
// TestEnableCache_MemoryDefaults covers the default memory sizing branch (maxMemory<=0).
|
|
func TestEnableCache_MemoryDefaults_UsesDefaultSizes(t *testing.T) {
|
|
prev := config
|
|
prevStats := cacheStats
|
|
defer func() {
|
|
config = prev
|
|
cacheStats = prevStats
|
|
}()
|
|
|
|
cfg := &CacheConfig{
|
|
Logger: libpack_logger.New(),
|
|
TTL: 5,
|
|
}
|
|
// MaxMemorySize and MaxEntries left at zero → defaults kick in.
|
|
EnableCache(cfg)
|
|
require.NotNil(t, config.Client)
|
|
ta.Greater(t, GetCacheMaxMemorySize(), int64(0))
|
|
}
|
|
|
|
// TestEnableCache_RedisFallback covers the Redis error → memory fallback branch.
|
|
func TestEnableCache_RedisFallback_FallsBackToMemory(t *testing.T) {
|
|
prev := config
|
|
prevStats := cacheStats
|
|
defer func() {
|
|
config = prev
|
|
cacheStats = prevStats
|
|
}()
|
|
|
|
cfg := &CacheConfig{
|
|
Logger: libpack_logger.New(),
|
|
TTL: 5,
|
|
}
|
|
cfg.Redis.Enable = true
|
|
cfg.Redis.URL = "127.0.0.1:1" // unreachable port → connection error
|
|
cfg.Redis.DB = 0
|
|
|
|
// Must not panic; should fall back to memory.
|
|
ta.NotPanics(t, func() { EnableCache(cfg) })
|
|
require.NotNil(t, config.Client, "fallback memory client must be set")
|
|
|
|
// Verify it actually works as a memory cache.
|
|
CacheStore("fallback-key", []byte("fallback-val"))
|
|
got := CacheLookup("fallback-key")
|
|
ta.Equal(t, []byte("fallback-val"), got)
|
|
}
|
|
|
|
// TestCacheStore_Uninitialized covers the early-return + log branch in CacheStore (line 238-242).
|
|
func TestCacheStore_Uninitialized_DoesNotPanic(t *testing.T) {
|
|
prev := config
|
|
config = &CacheConfig{
|
|
Logger: libpack_logger.New(),
|
|
Client: nil, // IsCacheInitialized() returns false
|
|
}
|
|
defer func() { config = prev }()
|
|
|
|
ta.NotPanics(t, func() {
|
|
CacheStore("any-key", []byte("any-val"))
|
|
})
|
|
}
|
|
|
|
// TestCacheClear_Uninitialized covers the early-return in CacheClear.
|
|
func TestCacheClear_Uninitialized_DoesNotPanic(t *testing.T) {
|
|
prev := config
|
|
config = nil
|
|
defer func() { config = prev }()
|
|
|
|
ta.NotPanics(t, func() { CacheClear() })
|
|
}
|
|
|
|
// TestCacheDelete_ZeroStats covers the CAS loop branch where CachedQueries is already 0.
|
|
func TestCacheDelete_ZeroStats_DoesNotDecrementBelowZero(t *testing.T) {
|
|
defer withFreshMemoryCache(t, 5*time.Minute)()
|
|
cacheStats.CachedQueries = 0 // already at zero
|
|
|
|
// Should not panic and stats should stay at 0.
|
|
CacheDelete("nonexistent-key")
|
|
ta.Equal(t, int64(0), cacheStats.CachedQueries)
|
|
}
|
|
|
|
// TestEnableCache_Redis_HappyPath covers successful Redis init via miniredis.
|
|
func TestEnableCache_Redis_HappyPath_StoresAndRetrieves(t *testing.T) {
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
prev := config
|
|
prevStats := cacheStats
|
|
defer func() {
|
|
config = prev
|
|
cacheStats = prevStats
|
|
}()
|
|
|
|
cfg := &CacheConfig{
|
|
Logger: libpack_logger.New(),
|
|
TTL: 5,
|
|
}
|
|
cfg.Redis.Enable = true
|
|
cfg.Redis.URL = mr.Addr()
|
|
cfg.Redis.DB = 0
|
|
EnableCache(cfg)
|
|
|
|
require.True(t, IsCacheInitialized())
|
|
CacheStore("r-key", []byte("r-val"))
|
|
ta.Equal(t, []byte("r-val"), CacheLookup("r-key"))
|
|
|
|
// GetCacheMemoryUsage and GetCacheMaxMemorySize via Redis wrapper.
|
|
ta.GreaterOrEqual(t, GetCacheMemoryUsage(), int64(0))
|
|
ta.GreaterOrEqual(t, GetCacheMaxMemorySize(), int64(0))
|
|
}
|