mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
General library improvements.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Benchmark tiered buffer pools vs single pool
|
||||
func BenchmarkBufferPools(b *testing.B) {
|
||||
// Test data of different sizes
|
||||
smallData := make([]byte, 512)
|
||||
mediumData := make([]byte, 2048)
|
||||
largeData := make([]byte, 8192)
|
||||
|
||||
for i := range smallData {
|
||||
smallData[i] = byte(i % 256)
|
||||
}
|
||||
for i := range mediumData {
|
||||
mediumData[i] = byte(i % 256)
|
||||
}
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
b.Run("SmallBuffer_GetPut", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := getBuffer(len(smallData))
|
||||
buf.Write(smallData)
|
||||
putBuffer(buf)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MediumBuffer_GetPut", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := getBuffer(len(mediumData))
|
||||
buf.Write(mediumData)
|
||||
putBuffer(buf)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("LargeBuffer_GetPut", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := getBuffer(len(largeData))
|
||||
buf.Write(largeData)
|
||||
putBuffer(buf)
|
||||
}
|
||||
})
|
||||
|
||||
// Compare with old single pool approach
|
||||
b.Run("SinglePool_Small", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
buf.Write(smallData)
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SinglePool_Medium", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
buf.Write(mediumData)
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SinglePool_Large", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
buf.Write(largeData)
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark buffer allocation patterns
|
||||
func BenchmarkBufferAllocation(b *testing.B) {
|
||||
b.Run("TieredPools_Mixed", func(b *testing.B) {
|
||||
sizes := []int{256, 512, 1024, 2048, 4096, 8192, 16384}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
size := sizes[i%len(sizes)]
|
||||
buf := getBuffer(size)
|
||||
data := make([]byte, size)
|
||||
buf.Write(data)
|
||||
putBuffer(buf)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("DirectAllocation_Mixed", func(b *testing.B) {
|
||||
sizes := []int{256, 512, 1024, 2048, 4096, 8192, 16384}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
size := sizes[i%len(sizes)]
|
||||
buf := bytes.NewBuffer(make([]byte, 0, size))
|
||||
data := make([]byte, size)
|
||||
buf.Write(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test buffer pool correctness
|
||||
func TestBufferPoolCorrectness(t *testing.T) {
|
||||
testData := []byte("Hello, World! This is test data for buffer pool testing.")
|
||||
|
||||
// Test small buffer
|
||||
buf := getBuffer(len(testData))
|
||||
buf.Write(testData)
|
||||
if buf.Len() != len(testData) {
|
||||
t.Errorf("Buffer length mismatch: expected %d, got %d", len(testData), buf.Len())
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), testData) {
|
||||
t.Error("Buffer content mismatch")
|
||||
}
|
||||
putBuffer(buf)
|
||||
|
||||
// Test that buffer is properly reset when returned to pool
|
||||
buf2 := getBuffer(10)
|
||||
if buf2.Len() != 0 {
|
||||
t.Error("Buffer not properly reset")
|
||||
}
|
||||
putBuffer(buf2)
|
||||
|
||||
// Test large buffer
|
||||
largeData := make([]byte, 10000)
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
largeBuf := getBuffer(len(largeData))
|
||||
largeBuf.Write(largeData)
|
||||
if largeBuf.Len() != len(largeData) {
|
||||
t.Errorf("Large buffer length mismatch: expected %d, got %d", len(largeData), largeBuf.Len())
|
||||
}
|
||||
putBuffer(largeBuf)
|
||||
}
|
||||
|
||||
// Test buffer pool size selection
|
||||
func TestBufferPoolSizeSelection(t *testing.T) {
|
||||
testCases := []struct {
|
||||
size int
|
||||
expectedPool string
|
||||
}{
|
||||
{100, "small"},
|
||||
{1024, "small"},
|
||||
{1025, "medium"},
|
||||
{4096, "medium"},
|
||||
{4097, "large"},
|
||||
{16384, "large"},
|
||||
{20000, "large"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
buf := getBuffer(tc.size)
|
||||
capacity := buf.Cap()
|
||||
|
||||
var poolType string
|
||||
switch {
|
||||
case capacity <= 1024:
|
||||
poolType = "small"
|
||||
case capacity <= 4096:
|
||||
poolType = "medium"
|
||||
default:
|
||||
poolType = "large"
|
||||
}
|
||||
|
||||
if poolType != tc.expectedPool {
|
||||
t.Errorf("Size %d: expected %s pool, got %s pool (capacity: %d)",
|
||||
tc.size, tc.expectedPool, poolType, capacity)
|
||||
}
|
||||
putBuffer(buf)
|
||||
}
|
||||
}
|
||||
Vendored
+110
-15
@@ -12,6 +12,7 @@ import (
|
||||
type CacheEntry struct {
|
||||
ExpiresAt time.Time
|
||||
Value []byte
|
||||
IsCompressed bool
|
||||
}
|
||||
|
||||
const shardCount = 256 // Must be power of 2
|
||||
@@ -26,6 +27,10 @@ type Cache struct {
|
||||
decompressPool sync.Pool
|
||||
shards [shardCount]*shard
|
||||
globalTTL time.Duration
|
||||
cleanupChan chan struct{}
|
||||
stopChan chan struct{}
|
||||
stopped bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// getShard returns the appropriate shard for a given key
|
||||
@@ -38,6 +43,8 @@ func (c *Cache) getShard(key string) *shard {
|
||||
func New(globalTTL time.Duration) *Cache {
|
||||
cache := &Cache{
|
||||
globalTTL: globalTTL,
|
||||
cleanupChan: make(chan struct{}, 1),
|
||||
stopChan: make(chan struct{}),
|
||||
compressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := gzip.NewWriter(nil)
|
||||
@@ -59,31 +66,116 @@ func New(globalTTL time.Duration) *Cache {
|
||||
}
|
||||
}
|
||||
|
||||
go cache.cleanupRoutine(globalTTL)
|
||||
go cache.lazyCleanupWorker()
|
||||
go cache.periodicCleanupRoutine(globalTTL)
|
||||
return cache
|
||||
}
|
||||
|
||||
func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
|
||||
ticker := time.NewTicker(globalTTL / 2)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
func (c *Cache) lazyCleanupWorker() {
|
||||
for {
|
||||
select {
|
||||
case <-c.cleanupChan:
|
||||
c.mu.RLock()
|
||||
if c.stopped {
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
c.CleanExpiredEntries()
|
||||
case <-c.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) periodicCleanupRoutine(globalTTL time.Duration) {
|
||||
// Use a shorter cleanup interval for better responsiveness, but not less than 1 second
|
||||
cleanupInterval := globalTTL / 4
|
||||
if cleanupInterval < time.Second {
|
||||
cleanupInterval = time.Second
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.mu.RLock()
|
||||
if c.stopped {
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
c.CleanExpiredEntries()
|
||||
case <-c.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) triggerLazyCleanup() {
|
||||
c.mu.RLock()
|
||||
if c.stopped {
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
select {
|
||||
case c.cleanupChan <- struct{}{}:
|
||||
// Cleanup triggered
|
||||
default:
|
||||
// Cleanup already pending, skip
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Stop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.stopped {
|
||||
return // Already stopped
|
||||
}
|
||||
|
||||
c.stopped = true
|
||||
close(c.stopChan)
|
||||
}
|
||||
|
||||
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
shard := c.getShard(key)
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
|
||||
const compressionThreshold = 1024 // 1KB threshold
|
||||
var finalValue []byte
|
||||
var isCompressed bool
|
||||
|
||||
if len(value) >= compressionThreshold {
|
||||
compressedValue, err := c.compress(value)
|
||||
if err != nil {
|
||||
return
|
||||
// If compression fails, store uncompressed
|
||||
finalValue = value
|
||||
isCompressed = false
|
||||
} else {
|
||||
// Only use compression if it actually reduces size
|
||||
if len(compressedValue) < len(value) {
|
||||
finalValue = compressedValue
|
||||
isCompressed = true
|
||||
} else {
|
||||
finalValue = value
|
||||
isCompressed = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalValue = value
|
||||
isCompressed = false
|
||||
}
|
||||
|
||||
shard.entries[key] = CacheEntry{
|
||||
Value: compressedValue,
|
||||
Value: finalValue,
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
IsCompressed: isCompressed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,20 +190,23 @@ func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
|
||||
if entry.ExpiresAt.Before(time.Now()) {
|
||||
shard.RUnlock()
|
||||
// Clean up expired entry in background
|
||||
go func() {
|
||||
shard.Lock()
|
||||
delete(shard.entries, key)
|
||||
shard.Unlock()
|
||||
}()
|
||||
// Trigger lazy cleanup instead of immediate deletion
|
||||
c.triggerLazyCleanup()
|
||||
return nil, false
|
||||
}
|
||||
shard.RUnlock()
|
||||
|
||||
value, err := c.decompress(entry.Value)
|
||||
var value []byte
|
||||
var err error
|
||||
|
||||
if entry.IsCompressed {
|
||||
value, err = c.decompress(entry.Value)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
} else {
|
||||
value = entry.Value
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
Vendored
+11
-7
@@ -8,22 +8,24 @@ import (
|
||||
// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package.
|
||||
|
||||
func BenchmarkCacheSet(b *testing.B) {
|
||||
cache := New(30 * time.Second) // Initializing the cache with a TTL of 30 seconds
|
||||
cache := New(5 * time.Second) // Use shorter TTL for tests
|
||||
defer cache.Stop()
|
||||
key := "benchmark-key"
|
||||
value := []byte("benchmark-value")
|
||||
|
||||
b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Set(key, value, 5*time.Second)
|
||||
cache.Set(key, value, 2*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheGet(b *testing.B) {
|
||||
cache := New(30 * time.Second) // Initializing the cache
|
||||
cache := New(5 * time.Second) // Use shorter TTL for tests
|
||||
defer cache.Stop()
|
||||
key := "benchmark-key"
|
||||
value := []byte("benchmark-value")
|
||||
cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve
|
||||
cache.Set(key, value, 2*time.Second) // Pre-set a value to retrieve
|
||||
|
||||
b.ResetTimer() // Start timing
|
||||
|
||||
@@ -33,14 +35,16 @@ func BenchmarkCacheGet(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkCacheExpire(b *testing.B) {
|
||||
key := "benchmark-expire-key"
|
||||
cache := New(5 * time.Second) // Use shorter TTL for tests and reuse cache
|
||||
defer cache.Stop()
|
||||
value := []byte("benchmark-value")
|
||||
ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache := New(30 * time.Second)
|
||||
key := "benchmark-expire-key-" + string(rune(i)) // Use unique keys
|
||||
cache.Set(key, value, ttl)
|
||||
time.Sleep(ttl) // Wait for the key to expire
|
||||
time.Sleep(ttl + time.Millisecond) // Wait for the key to expire with buffer
|
||||
_, _ = cache.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+15
-8
@@ -21,12 +21,15 @@ func TestCachingTestSuite(t *testing.T) {
|
||||
func (suite *CacheTestSuite) Test_New() {
|
||||
suite.T().Run("should return a new cache", func(t *testing.T) {
|
||||
cache := New(2 * time.Second)
|
||||
defer cache.Stop()
|
||||
suite.NotNil(cache)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheUse() {
|
||||
cache := New(30 * time.Second)
|
||||
cache := New(5 * time.Second)
|
||||
defer cache.Stop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
@@ -42,7 +45,7 @@ func (suite *CacheTestSuite) Test_CacheUse() {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
||||
cache.Set(tt.name, []byte(tt.name), 2*time.Second)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
@@ -51,7 +54,9 @@ func (suite *CacheTestSuite) Test_CacheUse() {
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheDelete() {
|
||||
cache := New(30 * time.Second)
|
||||
cache := New(5 * time.Second)
|
||||
defer cache.Stop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
@@ -67,7 +72,7 @@ func (suite *CacheTestSuite) Test_CacheDelete() {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
||||
cache.Set(tt.name, []byte(tt.name), 2*time.Second)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
@@ -80,7 +85,9 @@ func (suite *CacheTestSuite) Test_CacheDelete() {
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheExpire() {
|
||||
cache := New(30 * time.Second)
|
||||
cache := New(5 * time.Second)
|
||||
defer cache.Stop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
@@ -89,12 +96,12 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
ttl: 2 * time.Second,
|
||||
ttl: 100 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
ttl: 5 * time.Second,
|
||||
ttl: 200 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -103,7 +110,7 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
time.Sleep(tt.ttl)
|
||||
time.Sleep(tt.ttl + 50*time.Millisecond) // Add small buffer
|
||||
c, ok = cache.Get(tt.name)
|
||||
suite.Equal(false, ok)
|
||||
suite.Equal("", string(c))
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cache "github.com/lukaszraczylo/go-simple-graphql/cache"
|
||||
)
|
||||
|
||||
// Benchmark cache compression with different data sizes
|
||||
func BenchmarkCacheCompression(b *testing.B) {
|
||||
c := cache.New(5 * time.Second) // Use shorter TTL for tests
|
||||
defer c.Stop()
|
||||
|
||||
// Small data (under 1KB threshold)
|
||||
smallData := make([]byte, 512)
|
||||
for i := range smallData {
|
||||
smallData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Medium data (over 1KB threshold)
|
||||
mediumData := make([]byte, 2048)
|
||||
for i := range mediumData {
|
||||
mediumData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Large data (much larger than threshold)
|
||||
largeData := make([]byte, 8192)
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Highly compressible data
|
||||
compressibleData := make([]byte, 4096)
|
||||
for i := range compressibleData {
|
||||
compressibleData[i] = 'A' // Repeating character compresses well
|
||||
}
|
||||
|
||||
b.Run("SmallData_Set", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.Set("small", smallData, time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SmallData_Get", func(b *testing.B) {
|
||||
c.Set("small", smallData, time.Minute)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = c.Get("small")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MediumData_Set", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.Set("medium", mediumData, time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MediumData_Get", func(b *testing.B) {
|
||||
c.Set("medium", mediumData, time.Minute)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = c.Get("medium")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("LargeData_Set", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.Set("large", largeData, time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("LargeData_Get", func(b *testing.B) {
|
||||
c.Set("large", largeData, time.Minute)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = c.Get("large")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CompressibleData_Set", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.Set("compressible", compressibleData, time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CompressibleData_Get", func(b *testing.B) {
|
||||
c.Set("compressible", compressibleData, time.Minute)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = c.Get("compressible")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark cache memory usage with compression
|
||||
func BenchmarkCacheMemoryUsage(b *testing.B) {
|
||||
c := cache.New(5 * time.Second) // Use shorter TTL for tests
|
||||
defer c.Stop()
|
||||
|
||||
// Test data that compresses well
|
||||
testData := make([]byte, 4096)
|
||||
for i := range testData {
|
||||
testData[i] = byte(i%10 + '0') // Repeating pattern
|
||||
}
|
||||
|
||||
b.Run("WithCompression", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
key := "key" + string(rune(i%1000))
|
||||
c.Set(key, testData, time.Minute)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test compression effectiveness
|
||||
func TestCompressionEffectiveness(t *testing.T) {
|
||||
c := cache.New(5 * time.Second) // Use shorter TTL for tests
|
||||
defer c.Stop()
|
||||
|
||||
// Highly compressible data
|
||||
compressibleData := make([]byte, 4096)
|
||||
for i := range compressibleData {
|
||||
compressibleData[i] = 'A'
|
||||
}
|
||||
|
||||
// Random data (less compressible)
|
||||
randomData := make([]byte, 4096)
|
||||
for i := range randomData {
|
||||
randomData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Small data (under threshold)
|
||||
smallData := make([]byte, 512)
|
||||
for i := range smallData {
|
||||
smallData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Test compressible data
|
||||
c.Set("compressible", compressibleData, time.Minute)
|
||||
retrieved, ok := c.Get("compressible")
|
||||
if !ok {
|
||||
t.Error("Failed to retrieve compressible data")
|
||||
}
|
||||
if len(retrieved) != len(compressibleData) {
|
||||
t.Errorf("Data length mismatch: expected %d, got %d", len(compressibleData), len(retrieved))
|
||||
}
|
||||
|
||||
// Test random data
|
||||
c.Set("random", randomData, time.Minute)
|
||||
retrieved, ok = c.Get("random")
|
||||
if !ok {
|
||||
t.Error("Failed to retrieve random data")
|
||||
}
|
||||
if len(retrieved) != len(randomData) {
|
||||
t.Errorf("Data length mismatch: expected %d, got %d", len(randomData), len(retrieved))
|
||||
}
|
||||
|
||||
// Test small data (should not be compressed)
|
||||
c.Set("small", smallData, time.Minute)
|
||||
retrieved, ok = c.Get("small")
|
||||
if !ok {
|
||||
t.Error("Failed to retrieve small data")
|
||||
}
|
||||
if len(retrieved) != len(smallData) {
|
||||
t.Errorf("Data length mismatch: expected %d, got %d", len(smallData), len(retrieved))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -130,7 +130,7 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) {
|
||||
retry.Attempts(uint(retriesMax)),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(time.Duration(qe.retries_delay)),
|
||||
retry.MaxDelay(10 * time.Second),
|
||||
retry.MaxDelay(10*time.Second),
|
||||
retry.LastErrorOnly(true),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Benchmark_calculateHash(b *testing.B) {
|
||||
client := NewConnection()
|
||||
query := &Query{
|
||||
Query: "query { viewer { login } }",
|
||||
Variables: map[string]interface{}{"var1": "value1"},
|
||||
}
|
||||
query.JsonQuery = client.convertToJSON(query)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = calculateHash(query)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_calculateHashLargeQuery(b *testing.B) {
|
||||
client := NewConnection()
|
||||
// Create a larger query to test hash performance with bigger payloads
|
||||
largeQuery := `query {
|
||||
viewer {
|
||||
login
|
||||
name
|
||||
email
|
||||
bio
|
||||
company
|
||||
location
|
||||
websiteUrl
|
||||
twitterUsername
|
||||
repositories(first: 100) {
|
||||
nodes {
|
||||
name
|
||||
description
|
||||
url
|
||||
stargazerCount
|
||||
forkCount
|
||||
primaryLanguage {
|
||||
name
|
||||
color
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
variables := make(map[string]interface{})
|
||||
for i := 0; i < 50; i++ {
|
||||
variables[fmt.Sprintf("var%d", i)] = fmt.Sprintf("value%d", i)
|
||||
}
|
||||
|
||||
query := &Query{
|
||||
Query: largeQuery,
|
||||
Variables: variables,
|
||||
}
|
||||
query.JsonQuery = client.convertToJSON(query)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = calculateHash(query)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"hash/fnv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Benchmark comparing MD5 vs FNV hash performance
|
||||
func BenchmarkHashAlgorithms(b *testing.B) {
|
||||
testData := []byte(`{"query":"query GetUser($id: ID!) { user(id: $id) { id name email profile { avatar bio } posts { id title content createdAt } } }","variables":{"id":"12345"}}`)
|
||||
|
||||
b.Run("MD5", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hash := md5.Sum(testData)
|
||||
_ = hex.EncodeToString(hash[:])
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FNV64a", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hash := fnv.New64a()
|
||||
hash.Write(testData)
|
||||
_ = hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark the new calculateHash function
|
||||
func BenchmarkCalculateHash(b *testing.B) {
|
||||
query := &Query{
|
||||
Query: "query GetUser($id: ID!) { user(id: $id) { id name email profile { avatar bio } posts { id title content createdAt } } }",
|
||||
Variables: map[string]interface{}{
|
||||
"id": "12345",
|
||||
},
|
||||
JsonQuery: []byte(`{"query":"query GetUser($id: ID!) { user(id: $id) { id name email profile { avatar bio } posts { id title content createdAt } } }","variables":{"id":"12345"}}`),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = calculateHash(query)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark hash collision rate (quality test)
|
||||
func BenchmarkHashCollisions(b *testing.B) {
|
||||
queries := make([]*Query, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
queries[i] = &Query{
|
||||
Query: "query GetUser($id: ID!) { user(id: $id) { id name email } }",
|
||||
Variables: map[string]interface{}{
|
||||
"id": i,
|
||||
},
|
||||
JsonQuery: []byte(`{"query":"query GetUser($id: ID!) { user(id: $id) { id name email } }","variables":{"id":` + string(rune(i)) + `}}`),
|
||||
}
|
||||
}
|
||||
|
||||
b.Run("FNV_Collision_Test", func(b *testing.B) {
|
||||
hashes := make(map[string]bool)
|
||||
collisions := 0
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, query := range queries {
|
||||
hash := calculateHash(query)
|
||||
if hashes[hash] {
|
||||
collisions++
|
||||
} else {
|
||||
hashes[hash] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(collisions), "collisions")
|
||||
})
|
||||
}
|
||||
+4
-3
@@ -1,9 +1,9 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
@@ -17,8 +17,9 @@ func searchForKeysInMapStringInterface(msi map[string]interface{}, key string) i
|
||||
}
|
||||
|
||||
func calculateHash(query *Query) string {
|
||||
hash := md5.Sum(query.JsonQuery)
|
||||
return hex.EncodeToString(hash[:])
|
||||
hash := fnv.New64a()
|
||||
hash.Write(query.JsonQuery)
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (b *BaseClient) cacheLookup(hash string) []byte {
|
||||
|
||||
@@ -11,19 +11,22 @@ import (
|
||||
)
|
||||
|
||||
func (b *BaseClient) createHttpClient() (http_client *http.Client) {
|
||||
// Optimized connection pool settings for better performance
|
||||
httpTransport := &http.Transport{
|
||||
MaxIdleConns: 512,
|
||||
MaxConnsPerHost: 512,
|
||||
MaxIdleConnsPerHost: 512,
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Second,
|
||||
MaxIdleConns: 100, // Reduced from 512
|
||||
MaxConnsPerHost: 50, // Reduced from 512
|
||||
MaxIdleConnsPerHost: 10, // Reduced from 512
|
||||
IdleConnTimeout: 30 * time.Second, // Increased for better reuse
|
||||
ResponseHeaderTimeout: 10 * time.Second, // Reduced for faster timeouts
|
||||
DisableKeepAlives: false,
|
||||
DisableCompression: false,
|
||||
WriteBufferSize: 4096, // Optimize buffer size
|
||||
ReadBufferSize: 4096, // Optimize buffer size
|
||||
}
|
||||
|
||||
if strings.HasPrefix(b.endpoint, "http://") {
|
||||
http_client = &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
Timeout: 30 * time.Second, // Increased for better reliability
|
||||
Transport: httpTransport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
@@ -41,10 +44,12 @@ func (b *BaseClient) createHttpClient() (http_client *http.Client) {
|
||||
http2Transport := &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
ReadIdleTimeout: 15 * time.Second,
|
||||
PingTimeout: 15 * time.Second,
|
||||
ReadIdleTimeout: 30 * time.Second, // Increased for better reuse
|
||||
PingTimeout: 10 * time.Second, // Reduced for faster detection
|
||||
WriteByteTimeout: 10 * time.Second, // Add write timeout
|
||||
}
|
||||
http_client = &http.Client{
|
||||
Timeout: 30 * time.Second, // Increased for better reliability
|
||||
Transport: http2Transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cache "github.com/lukaszraczylo/go-simple-graphql/cache"
|
||||
)
|
||||
|
||||
// Performance regression tests to ensure optimizations maintain their benefits
|
||||
func TestPerformanceRegression(t *testing.T) {
|
||||
// Test hash performance
|
||||
t.Run("HashPerformance", func(t *testing.T) {
|
||||
query := &Query{
|
||||
Query: "query GetUser($id: ID!) { user(id: $id) { id name email } }",
|
||||
Variables: map[string]interface{}{
|
||||
"id": "12345",
|
||||
},
|
||||
JsonQuery: []byte(`{"query":"query GetUser($id: ID!) { user(id: $id) { id name email } }","variables":{"id":"12345"}}`),
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 10000; i++ {
|
||||
_ = calculateHash(query)
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete 10k hashes in under 25ms (accounts for race detection + coverage overhead)
|
||||
if duration > 25*time.Millisecond {
|
||||
t.Errorf("Hash performance regression: 10k hashes took %v, expected < 25ms", duration)
|
||||
}
|
||||
})
|
||||
|
||||
// Test buffer pool performance
|
||||
t.Run("BufferPoolPerformance", func(t *testing.T) {
|
||||
start := time.Now()
|
||||
for i := 0; i < 10000; i++ {
|
||||
buf := getBuffer(1024)
|
||||
buf.WriteString("test data")
|
||||
putBuffer(buf)
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete 10k buffer operations in under 25ms (accounts for race detection + coverage overhead)
|
||||
if duration > 25*time.Millisecond {
|
||||
t.Errorf("Buffer pool performance regression: 10k operations took %v, expected < 25ms", duration)
|
||||
}
|
||||
})
|
||||
|
||||
// Test cache performance
|
||||
t.Run("CachePerformance", func(t *testing.T) {
|
||||
c := cache.New(5 * time.Second) // Use shorter TTL for tests
|
||||
defer c.Stop()
|
||||
|
||||
testData := make([]byte, 512) // Small data that won't be compressed
|
||||
for i := range testData {
|
||||
testData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 10000; i++ {
|
||||
key := "key" + string(rune(i%100))
|
||||
c.Set(key, testData, time.Minute)
|
||||
_, _ = c.Get(key)
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete 10k cache operations in under 50ms
|
||||
if duration > 50*time.Millisecond {
|
||||
t.Errorf("Cache performance regression: 10k operations took %v, expected < 50ms", duration)
|
||||
}
|
||||
})
|
||||
|
||||
// Test processFlags performance
|
||||
t.Run("ProcessFlagsPerformance", func(t *testing.T) {
|
||||
variables := map[string]interface{}{
|
||||
"id": "123",
|
||||
"gqlcache": true,
|
||||
"gqlretries": true,
|
||||
}
|
||||
headers := map[string]interface{}{}
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 10000; i++ {
|
||||
_, _, _ = processFlags(variables, headers)
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete 10k flag processing operations in under 30ms (accounts for race detection + coverage overhead)
|
||||
if duration > 30*time.Millisecond {
|
||||
t.Errorf("ProcessFlags performance regression: 10k operations took %v, expected < 30ms", duration)
|
||||
}
|
||||
})
|
||||
|
||||
// Test query compilation performance
|
||||
t.Run("QueryCompilationPerformance", func(t *testing.T) {
|
||||
client := CreateTestClient()
|
||||
|
||||
query := "query GetUser($id: ID!) { user(id: $id) { id name email } }"
|
||||
variables := map[string]interface{}{"id": "123"}
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
_ = client.compileQuery(query, variables)
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete 1k query compilations in under 50ms
|
||||
if duration > 50*time.Millisecond {
|
||||
t.Errorf("Query compilation performance regression: 1k operations took %v, expected < 50ms", duration)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark comprehensive workflow to ensure no regression
|
||||
func BenchmarkComprehensiveWorkflow(b *testing.B) {
|
||||
client := CreateTestClient()
|
||||
|
||||
query := "query GetUser($id: ID!) { user(id: $id) { id name email profile { avatar bio } } }"
|
||||
variables := map[string]interface{}{
|
||||
"id": "123",
|
||||
"gqlcache": true,
|
||||
"gqlretries": true,
|
||||
}
|
||||
headers := map[string]interface{}{}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Process flags
|
||||
enableCache, enableRetries, cleanedVariables := processFlags(variables, headers)
|
||||
|
||||
// Compile query
|
||||
compiledQuery := client.compileQuery(query, cleanedVariables)
|
||||
|
||||
// Calculate hash
|
||||
if enableCache {
|
||||
_ = calculateHash(compiledQuery)
|
||||
}
|
||||
|
||||
// Use variables to avoid optimization
|
||||
_ = enableRetries
|
||||
}
|
||||
}
|
||||
|
||||
// Memory allocation benchmark
|
||||
func BenchmarkMemoryAllocations(b *testing.B) {
|
||||
client := CreateTestClient()
|
||||
|
||||
query := "query GetUser($id: ID!) { user(id: $id) { id name } }"
|
||||
variables := map[string]interface{}{"id": "123"}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
compiledQuery := client.compileQuery(query, variables)
|
||||
_ = calculateHash(compiledQuery)
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,34 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Pre-sized buffer pool for better performance
|
||||
bufferPool = sync.Pool{
|
||||
// Tiered buffer pools for different size requirements
|
||||
smallBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Pre-allocate buffer with 4KB capacity
|
||||
// Small buffers for typical GraphQL queries (1KB)
|
||||
b := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
mediumBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Medium buffers for larger queries (4KB)
|
||||
b := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
largeBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Large buffers for very large queries (16KB)
|
||||
b := bytes.NewBuffer(make([]byte, 0, 16384))
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
// Backward compatibility - use medium pool as default
|
||||
bufferPool *sync.Pool
|
||||
|
||||
// Pre-allocate error maps to reduce allocations
|
||||
errPairsPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
@@ -22,3 +41,34 @@ var (
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
bufferPool = &mediumBufferPool
|
||||
}
|
||||
|
||||
// getBuffer returns an appropriately sized buffer based on estimated size
|
||||
func getBuffer(estimatedSize int) *bytes.Buffer {
|
||||
switch {
|
||||
case estimatedSize <= 1024:
|
||||
return smallBufferPool.Get().(*bytes.Buffer)
|
||||
case estimatedSize <= 4096:
|
||||
return mediumBufferPool.Get().(*bytes.Buffer)
|
||||
default:
|
||||
return largeBufferPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
}
|
||||
|
||||
// putBuffer returns a buffer to the appropriate pool based on its capacity
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
capacity := buf.Cap()
|
||||
|
||||
switch {
|
||||
case capacity <= 1024:
|
||||
smallBufferPool.Put(buf)
|
||||
case capacity <= 4096:
|
||||
mediumBufferPool.Put(buf)
|
||||
default:
|
||||
largeBufferPool.Put(buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -11,9 +10,17 @@ import (
|
||||
)
|
||||
|
||||
func (b *BaseClient) convertToJSON(v any) []byte {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer bufferPool.Put(buf)
|
||||
buf.Reset()
|
||||
// Estimate size based on query structure for better buffer selection
|
||||
estimatedSize := 512 // Base size
|
||||
if query, ok := v.(*Query); ok {
|
||||
estimatedSize += len(query.Query)
|
||||
if query.Variables != nil {
|
||||
estimatedSize += len(query.Variables) * 50 // Rough estimate per variable
|
||||
}
|
||||
}
|
||||
|
||||
buf := getBuffer(estimatedSize)
|
||||
defer putBuffer(buf)
|
||||
|
||||
// Use json.NewEncoder for better performance with buffers
|
||||
enc := json.NewEncoder(buf)
|
||||
@@ -42,6 +49,39 @@ func (b *BaseClient) convertToJSON(v any) []byte {
|
||||
return result
|
||||
}
|
||||
|
||||
func processFlags(variables map[string]interface{}, headers map[string]interface{}) (enableCache, enableRetries bool, cleanedVariables map[string]interface{}) {
|
||||
// Start with original variables
|
||||
cleanedVariables = variables
|
||||
|
||||
// Check headers first
|
||||
enableCache, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlcache"))
|
||||
enableRetries, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlretries"))
|
||||
|
||||
// Check variables and clean them if needed
|
||||
if variables != nil {
|
||||
varEnableCache, _ := goutil.ToBool(searchForKeysInMapStringInterface(variables, "gqlcache"))
|
||||
varEnableRetries, _ := goutil.ToBool(searchForKeysInMapStringInterface(variables, "gqlretries"))
|
||||
|
||||
enableCache = enableCache || varEnableCache
|
||||
enableRetries = enableRetries || varEnableRetries
|
||||
|
||||
// Clean variables if flags are present (regardless of their value)
|
||||
_, hasCacheFlag := variables["gqlcache"]
|
||||
_, hasRetriesFlag := variables["gqlretries"]
|
||||
|
||||
if hasCacheFlag || hasRetriesFlag {
|
||||
cleanedVariables = make(map[string]interface{})
|
||||
for k, v := range variables {
|
||||
if k != "gqlcache" && k != "gqlretries" {
|
||||
cleanedVariables[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enableCache, enableRetries, cleanedVariables
|
||||
}
|
||||
|
||||
func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
|
||||
var query string
|
||||
var variables map[string]interface{}
|
||||
@@ -81,7 +121,11 @@ func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
|
||||
}
|
||||
|
||||
func (b *BaseClient) Query(query string, variables map[string]interface{}, headers map[string]interface{}) (any, error) {
|
||||
compiledQuery := b.compileQuery(query, variables)
|
||||
// Process flags before compilation to avoid recompilation
|
||||
enableCache, enableRetries, cleanedVariables := processFlags(variables, headers)
|
||||
|
||||
// Compile query once with cleaned variables
|
||||
compiledQuery := b.compileQuery(query, cleanedVariables)
|
||||
if compiledQuery.JsonQuery == nil {
|
||||
b.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't compile query",
|
||||
@@ -94,11 +138,6 @@ func (b *BaseClient) Query(query string, variables map[string]interface{}, heade
|
||||
Pairs: map[string]interface{}{"query": compiledQuery},
|
||||
})
|
||||
|
||||
enableCache, enableRetries, recompileRequired := compiledQuery.parseHeadersAndVariables(headers)
|
||||
if recompileRequired {
|
||||
compiledQuery = b.compileQuery(query, variables)
|
||||
}
|
||||
|
||||
var queryHash string
|
||||
if (enableCache || b.cache_global) && strutil.HasPrefix(compiledQuery.Query, "query") {
|
||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||
@@ -143,23 +182,3 @@ func (b *BaseClient) Query(query string, variables map[string]interface{}, heade
|
||||
|
||||
return b.decodeResponse(rv)
|
||||
}
|
||||
|
||||
func (q *Query) parseHeadersAndVariables(headers map[string]interface{}) (enableCache, enableRetries, recompileRequired bool) {
|
||||
enableCache, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlcache"))
|
||||
enableRetries, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlretries"))
|
||||
|
||||
if q.Variables != nil {
|
||||
varEnableCache, _ := goutil.ToBool(searchForKeysInMapStringInterface(q.Variables, "gqlcache"))
|
||||
varEnableRetries, _ := goutil.ToBool(searchForKeysInMapStringInterface(q.Variables, "gqlretries"))
|
||||
enableCache = enableCache || varEnableCache
|
||||
enableRetries = enableRetries || varEnableRetries
|
||||
|
||||
if varEnableCache || varEnableRetries {
|
||||
delete(q.Variables, "gqlcache")
|
||||
delete(q.Variables, "gqlretries")
|
||||
recompileRequired = true
|
||||
}
|
||||
}
|
||||
|
||||
return enableCache, enableRetries, recompileRequired
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type QueryOptimizationTestSuite struct {
|
||||
suite.Suite
|
||||
client *BaseClient
|
||||
}
|
||||
|
||||
func (suite *QueryOptimizationTestSuite) SetupTest() {
|
||||
suite.client = CreateTestClient()
|
||||
}
|
||||
|
||||
func (suite *QueryOptimizationTestSuite) TearDownTest() {
|
||||
// No need to stop shared cache - it will be cleaned up globally
|
||||
}
|
||||
|
||||
func (suite *QueryOptimizationTestSuite) TestProcessFlags() {
|
||||
tests := []struct {
|
||||
name string
|
||||
variables map[string]interface{}
|
||||
headers map[string]interface{}
|
||||
expectedCache bool
|
||||
expectedRetries bool
|
||||
expectedVarCount int
|
||||
}{
|
||||
{
|
||||
name: "No flags",
|
||||
variables: map[string]interface{}{"id": "123"},
|
||||
headers: map[string]interface{}{},
|
||||
expectedCache: false,
|
||||
expectedRetries: false,
|
||||
expectedVarCount: 1,
|
||||
},
|
||||
{
|
||||
name: "Cache flag in headers",
|
||||
variables: map[string]interface{}{"id": "123"},
|
||||
headers: map[string]interface{}{"gqlcache": true},
|
||||
expectedCache: true,
|
||||
expectedRetries: false,
|
||||
expectedVarCount: 1,
|
||||
},
|
||||
{
|
||||
name: "Retries flag in headers",
|
||||
variables: map[string]interface{}{"id": "123"},
|
||||
headers: map[string]interface{}{"gqlretries": true},
|
||||
expectedCache: false,
|
||||
expectedRetries: true,
|
||||
expectedVarCount: 1,
|
||||
},
|
||||
{
|
||||
name: "Cache flag in variables",
|
||||
variables: map[string]interface{}{"id": "123", "gqlcache": true},
|
||||
headers: map[string]interface{}{},
|
||||
expectedCache: true,
|
||||
expectedRetries: false,
|
||||
expectedVarCount: 1, // gqlcache should be removed
|
||||
},
|
||||
{
|
||||
name: "Retries flag in variables",
|
||||
variables: map[string]interface{}{"id": "123", "gqlretries": true},
|
||||
headers: map[string]interface{}{},
|
||||
expectedCache: false,
|
||||
expectedRetries: true,
|
||||
expectedVarCount: 1, // gqlretries should be removed
|
||||
},
|
||||
{
|
||||
name: "Both flags in variables",
|
||||
variables: map[string]interface{}{"id": "123", "gqlcache": true, "gqlretries": true},
|
||||
headers: map[string]interface{}{},
|
||||
expectedCache: true,
|
||||
expectedRetries: true,
|
||||
expectedVarCount: 1, // both flags should be removed
|
||||
},
|
||||
{
|
||||
name: "Flags in both headers and variables",
|
||||
variables: map[string]interface{}{"id": "123", "gqlcache": false},
|
||||
headers: map[string]interface{}{"gqlcache": true, "gqlretries": true},
|
||||
expectedCache: true, // header value OR variable value
|
||||
expectedRetries: true,
|
||||
expectedVarCount: 1, // gqlcache should be removed from variables
|
||||
},
|
||||
{
|
||||
name: "Nil variables",
|
||||
variables: nil,
|
||||
headers: map[string]interface{}{"gqlcache": true},
|
||||
expectedCache: true,
|
||||
expectedRetries: false,
|
||||
expectedVarCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.Run(tt.name, func() {
|
||||
enableCache, enableRetries, cleanedVariables := processFlags(tt.variables, tt.headers)
|
||||
|
||||
suite.Equal(tt.expectedCache, enableCache, "Cache flag mismatch")
|
||||
suite.Equal(tt.expectedRetries, enableRetries, "Retries flag mismatch")
|
||||
|
||||
if tt.expectedVarCount == 0 {
|
||||
suite.Nil(cleanedVariables, "Variables should be nil")
|
||||
} else {
|
||||
suite.Len(cleanedVariables, tt.expectedVarCount, "Variable count mismatch")
|
||||
suite.NotContains(cleanedVariables, "gqlcache", "gqlcache should be removed")
|
||||
suite.NotContains(cleanedVariables, "gqlretries", "gqlretries should be removed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *QueryOptimizationTestSuite) TestDoubleCompilationFix() {
|
||||
// Test that query is only compiled once even with flags in variables
|
||||
query := "query GetUser($id: ID!) { user(id: $id) { id name } }"
|
||||
variables := map[string]interface{}{
|
||||
"id": "123",
|
||||
"gqlcache": true,
|
||||
"gqlretries": true,
|
||||
}
|
||||
|
||||
// This should not cause double compilation
|
||||
compiledQuery := suite.client.compileQuery(query, variables)
|
||||
suite.NotNil(compiledQuery)
|
||||
suite.Equal(query, compiledQuery.Query)
|
||||
|
||||
// Variables should still contain the flags at this point (they're cleaned in Query method)
|
||||
suite.Contains(compiledQuery.Variables, "gqlcache")
|
||||
suite.Contains(compiledQuery.Variables, "gqlretries")
|
||||
}
|
||||
|
||||
func (suite *QueryOptimizationTestSuite) TestQueryMethodOptimization() {
|
||||
// Test that processFlags works correctly without making HTTP requests
|
||||
variables := map[string]interface{}{
|
||||
"id": "123",
|
||||
"gqlcache": true,
|
||||
"gqlretries": true,
|
||||
}
|
||||
headers := map[string]interface{}{}
|
||||
|
||||
// Test processFlags directly
|
||||
enableCache, enableRetries, cleanedVariables := processFlags(variables, headers)
|
||||
|
||||
suite.True(enableCache, "Cache should be enabled")
|
||||
suite.True(enableRetries, "Retries should be enabled")
|
||||
suite.Len(cleanedVariables, 1, "Should have only id variable")
|
||||
suite.Equal("123", cleanedVariables["id"], "ID should be preserved")
|
||||
suite.NotContains(cleanedVariables, "gqlcache", "gqlcache should be removed")
|
||||
suite.NotContains(cleanedVariables, "gqlretries", "gqlretries should be removed")
|
||||
}
|
||||
|
||||
// Benchmark the optimization impact
|
||||
func BenchmarkQueryCompilation(b *testing.B) {
|
||||
client := CreateTestClient()
|
||||
|
||||
query := "query GetUser($id: ID!) { user(id: $id) { id name email profile { avatar bio } } }"
|
||||
|
||||
b.Run("WithoutFlags", func(b *testing.B) {
|
||||
variables := map[string]interface{}{"id": "123"}
|
||||
headers := map[string]interface{}{}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
processFlags(variables, headers)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("WithFlags", func(b *testing.B) {
|
||||
variables := map[string]interface{}{
|
||||
"id": "123",
|
||||
"gqlcache": true,
|
||||
"gqlretries": true,
|
||||
}
|
||||
headers := map[string]interface{}{}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
processFlags(variables, headers)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CompileQuery", func(b *testing.B) {
|
||||
variables := map[string]interface{}{"id": "123"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
client.compileQuery(query, variables)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryOptimizationSuite(t *testing.T) {
|
||||
suite.Run(t, new(QueryOptimizationTestSuite))
|
||||
}
|
||||
@@ -27,6 +27,9 @@ func (suite *Tests) TearDownSuite() {
|
||||
if mockServer != nil {
|
||||
mockServer.Close()
|
||||
}
|
||||
|
||||
// Cleanup shared test cache
|
||||
CleanupTestCache()
|
||||
}
|
||||
|
||||
func (suite *Tests) SetupTest() {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cache "github.com/lukaszraczylo/go-simple-graphql/cache"
|
||||
logging "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
testCacheOnce sync.Once
|
||||
testCache *cache.Cache
|
||||
testLogger *logging.Logger
|
||||
)
|
||||
|
||||
// GetTestCache returns a shared cache instance for tests to reduce resource contention
|
||||
func GetTestCache() *cache.Cache {
|
||||
testCacheOnce.Do(func() {
|
||||
testCache = cache.New(5 * time.Second) // Short TTL for tests
|
||||
})
|
||||
return testCache
|
||||
}
|
||||
|
||||
// GetTestLogger returns a shared logger instance for tests
|
||||
func GetTestLogger() *logging.Logger {
|
||||
if testLogger == nil {
|
||||
testLogger = logging.New()
|
||||
testLogger.SetMinLogLevel(logging.LEVEL_ERROR)
|
||||
}
|
||||
return testLogger
|
||||
}
|
||||
|
||||
// CleanupTestCache stops the shared test cache - call this in TestMain if needed
|
||||
func CleanupTestCache() {
|
||||
if testCache != nil {
|
||||
testCache.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTestClient creates a test client with shared resources
|
||||
func CreateTestClient() *BaseClient {
|
||||
return &BaseClient{
|
||||
Logger: GetTestLogger(),
|
||||
cache: GetTestCache(),
|
||||
endpoint: "https://example.com/graphql",
|
||||
responseType: "mapstring",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user