mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-18 01:27:25 +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
+122
-27
@@ -10,22 +10,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CacheEntry struct {
|
type CacheEntry struct {
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
Value []byte
|
Value []byte
|
||||||
|
IsCompressed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const shardCount = 256 // Must be power of 2
|
const shardCount = 256 // Must be power of 2
|
||||||
|
|
||||||
type shard struct {
|
type shard struct {
|
||||||
entries map[string]CacheEntry
|
entries map[string]CacheEntry
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
compressPool sync.Pool
|
compressPool sync.Pool
|
||||||
decompressPool sync.Pool
|
decompressPool sync.Pool
|
||||||
shards [shardCount]*shard
|
shards [shardCount]*shard
|
||||||
globalTTL time.Duration
|
globalTTL time.Duration
|
||||||
|
cleanupChan chan struct{}
|
||||||
|
stopChan chan struct{}
|
||||||
|
stopped bool
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// getShard returns the appropriate shard for a given key
|
// getShard returns the appropriate shard for a given key
|
||||||
@@ -37,7 +42,9 @@ func (c *Cache) getShard(key string) *shard {
|
|||||||
|
|
||||||
func New(globalTTL time.Duration) *Cache {
|
func New(globalTTL time.Duration) *Cache {
|
||||||
cache := &Cache{
|
cache := &Cache{
|
||||||
globalTTL: globalTTL,
|
globalTTL: globalTTL,
|
||||||
|
cleanupChan: make(chan struct{}, 1),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
compressPool: sync.Pool{
|
compressPool: sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
w := gzip.NewWriter(nil)
|
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
|
return cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
|
func (c *Cache) lazyCleanupWorker() {
|
||||||
ticker := time.NewTicker(globalTTL / 2)
|
for {
|
||||||
defer ticker.Stop()
|
select {
|
||||||
|
case <-c.cleanupChan:
|
||||||
for range ticker.C {
|
c.mu.RLock()
|
||||||
c.CleanExpiredEntries()
|
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) {
|
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||||
shard := c.getShard(key)
|
shard := c.getShard(key)
|
||||||
shard.Lock()
|
shard.Lock()
|
||||||
defer shard.Unlock()
|
defer shard.Unlock()
|
||||||
|
|
||||||
compressedValue, err := c.compress(value)
|
const compressionThreshold = 1024 // 1KB threshold
|
||||||
if err != nil {
|
var finalValue []byte
|
||||||
return
|
var isCompressed bool
|
||||||
|
|
||||||
|
if len(value) >= compressionThreshold {
|
||||||
|
compressedValue, err := c.compress(value)
|
||||||
|
if err != nil {
|
||||||
|
// 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{
|
shard.entries[key] = CacheEntry{
|
||||||
Value: compressedValue,
|
Value: finalValue,
|
||||||
ExpiresAt: time.Now().Add(ttl),
|
ExpiresAt: time.Now().Add(ttl),
|
||||||
|
IsCompressed: isCompressed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +190,22 @@ func (c *Cache) Get(key string) ([]byte, bool) {
|
|||||||
|
|
||||||
if entry.ExpiresAt.Before(time.Now()) {
|
if entry.ExpiresAt.Before(time.Now()) {
|
||||||
shard.RUnlock()
|
shard.RUnlock()
|
||||||
// Clean up expired entry in background
|
// Trigger lazy cleanup instead of immediate deletion
|
||||||
go func() {
|
c.triggerLazyCleanup()
|
||||||
shard.Lock()
|
|
||||||
delete(shard.entries, key)
|
|
||||||
shard.Unlock()
|
|
||||||
}()
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
shard.RUnlock()
|
shard.RUnlock()
|
||||||
|
|
||||||
value, err := c.decompress(entry.Value)
|
var value []byte
|
||||||
if err != nil {
|
var err error
|
||||||
return nil, false
|
|
||||||
|
if entry.IsCompressed {
|
||||||
|
value, err = c.decompress(entry.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = entry.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
return value, true
|
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.
|
// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package.
|
||||||
|
|
||||||
func BenchmarkCacheSet(b *testing.B) {
|
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"
|
key := "benchmark-key"
|
||||||
value := []byte("benchmark-value")
|
value := []byte("benchmark-value")
|
||||||
|
|
||||||
b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark
|
b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
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) {
|
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"
|
key := "benchmark-key"
|
||||||
value := []byte("benchmark-value")
|
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
|
b.ResetTimer() // Start timing
|
||||||
|
|
||||||
@@ -33,14 +35,16 @@ func BenchmarkCacheGet(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkCacheExpire(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")
|
value := []byte("benchmark-value")
|
||||||
ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration
|
ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
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)
|
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)
|
_, _ = cache.Get(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+15
-8
@@ -21,12 +21,15 @@ func TestCachingTestSuite(t *testing.T) {
|
|||||||
func (suite *CacheTestSuite) Test_New() {
|
func (suite *CacheTestSuite) Test_New() {
|
||||||
suite.T().Run("should return a new cache", func(t *testing.T) {
|
suite.T().Run("should return a new cache", func(t *testing.T) {
|
||||||
cache := New(2 * time.Second)
|
cache := New(2 * time.Second)
|
||||||
|
defer cache.Stop()
|
||||||
suite.NotNil(cache)
|
suite.NotNil(cache)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CacheTestSuite) Test_CacheUse() {
|
func (suite *CacheTestSuite) Test_CacheUse() {
|
||||||
cache := New(30 * time.Second)
|
cache := New(5 * time.Second)
|
||||||
|
defer cache.Stop()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cache_value string
|
cache_value string
|
||||||
@@ -42,7 +45,7 @@ func (suite *CacheTestSuite) Test_CacheUse() {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
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)
|
c, ok := cache.Get(tt.name)
|
||||||
suite.Equal(true, ok)
|
suite.Equal(true, ok)
|
||||||
suite.Equal(tt.name, string(c))
|
suite.Equal(tt.name, string(c))
|
||||||
@@ -51,7 +54,9 @@ func (suite *CacheTestSuite) Test_CacheUse() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CacheTestSuite) Test_CacheDelete() {
|
func (suite *CacheTestSuite) Test_CacheDelete() {
|
||||||
cache := New(30 * time.Second)
|
cache := New(5 * time.Second)
|
||||||
|
defer cache.Stop()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cache_value string
|
cache_value string
|
||||||
@@ -67,7 +72,7 @@ func (suite *CacheTestSuite) Test_CacheDelete() {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
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)
|
c, ok := cache.Get(tt.name)
|
||||||
suite.Equal(true, ok)
|
suite.Equal(true, ok)
|
||||||
suite.Equal(tt.name, string(c))
|
suite.Equal(tt.name, string(c))
|
||||||
@@ -80,7 +85,9 @@ func (suite *CacheTestSuite) Test_CacheDelete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CacheTestSuite) Test_CacheExpire() {
|
func (suite *CacheTestSuite) Test_CacheExpire() {
|
||||||
cache := New(30 * time.Second)
|
cache := New(5 * time.Second)
|
||||||
|
defer cache.Stop()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cache_value string
|
cache_value string
|
||||||
@@ -89,12 +96,12 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
|
|||||||
{
|
{
|
||||||
name: "test1",
|
name: "test1",
|
||||||
cache_value: "test1-123",
|
cache_value: "test1-123",
|
||||||
ttl: 2 * time.Second,
|
ttl: 100 * time.Millisecond,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test2",
|
name: "test2",
|
||||||
cache_value: "test2-123",
|
cache_value: "test2-123",
|
||||||
ttl: 5 * time.Second,
|
ttl: 200 * time.Millisecond,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -103,7 +110,7 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
|
|||||||
c, ok := cache.Get(tt.name)
|
c, ok := cache.Get(tt.name)
|
||||||
suite.Equal(true, ok)
|
suite.Equal(true, ok)
|
||||||
suite.Equal(tt.name, string(c))
|
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)
|
c, ok = cache.Get(tt.name)
|
||||||
suite.Equal(false, ok)
|
suite.Equal(false, ok)
|
||||||
suite.Equal("", string(c))
|
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.Attempts(uint(retriesMax)),
|
||||||
retry.DelayType(retry.BackOffDelay),
|
retry.DelayType(retry.BackOffDelay),
|
||||||
retry.Delay(time.Duration(qe.retries_delay)),
|
retry.Delay(time.Duration(qe.retries_delay)),
|
||||||
retry.MaxDelay(10 * time.Second),
|
retry.MaxDelay(10*time.Second),
|
||||||
retry.LastErrorOnly(true),
|
retry.LastErrorOnly(true),
|
||||||
)
|
)
|
||||||
if err != nil {
|
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
|
package gql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
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 {
|
func calculateHash(query *Query) string {
|
||||||
hash := md5.Sum(query.JsonQuery)
|
hash := fnv.New64a()
|
||||||
return hex.EncodeToString(hash[:])
|
hash.Write(query.JsonQuery)
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseClient) cacheLookup(hash string) []byte {
|
func (b *BaseClient) cacheLookup(hash string) []byte {
|
||||||
|
|||||||
@@ -11,19 +11,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (b *BaseClient) createHttpClient() (http_client *http.Client) {
|
func (b *BaseClient) createHttpClient() (http_client *http.Client) {
|
||||||
|
// Optimized connection pool settings for better performance
|
||||||
httpTransport := &http.Transport{
|
httpTransport := &http.Transport{
|
||||||
MaxIdleConns: 512,
|
MaxIdleConns: 100, // Reduced from 512
|
||||||
MaxConnsPerHost: 512,
|
MaxConnsPerHost: 50, // Reduced from 512
|
||||||
MaxIdleConnsPerHost: 512,
|
MaxIdleConnsPerHost: 10, // Reduced from 512
|
||||||
IdleConnTimeout: 15 * time.Second,
|
IdleConnTimeout: 30 * time.Second, // Increased for better reuse
|
||||||
ResponseHeaderTimeout: 15 * time.Second,
|
ResponseHeaderTimeout: 10 * time.Second, // Reduced for faster timeouts
|
||||||
DisableKeepAlives: false,
|
DisableKeepAlives: false,
|
||||||
DisableCompression: false,
|
DisableCompression: false,
|
||||||
|
WriteBufferSize: 4096, // Optimize buffer size
|
||||||
|
ReadBufferSize: 4096, // Optimize buffer size
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(b.endpoint, "http://") {
|
if strings.HasPrefix(b.endpoint, "http://") {
|
||||||
http_client = &http.Client{
|
http_client = &http.Client{
|
||||||
Timeout: 15 * time.Second,
|
Timeout: 30 * time.Second, // Increased for better reliability
|
||||||
Transport: httpTransport,
|
Transport: httpTransport,
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
@@ -39,12 +42,14 @@ func (b *BaseClient) createHttpClient() (http_client *http.Client) {
|
|||||||
tlsClientConfig.InsecureSkipVerify = true
|
tlsClientConfig.InsecureSkipVerify = true
|
||||||
}
|
}
|
||||||
http2Transport := &http2.Transport{
|
http2Transport := &http2.Transport{
|
||||||
AllowHTTP: true,
|
AllowHTTP: true,
|
||||||
TLSClientConfig: tlsClientConfig,
|
TLSClientConfig: tlsClientConfig,
|
||||||
ReadIdleTimeout: 15 * time.Second,
|
ReadIdleTimeout: 30 * time.Second, // Increased for better reuse
|
||||||
PingTimeout: 15 * time.Second,
|
PingTimeout: 10 * time.Second, // Reduced for faster detection
|
||||||
|
WriteByteTimeout: 10 * time.Second, // Add write timeout
|
||||||
}
|
}
|
||||||
http_client = &http.Client{
|
http_client = &http.Client{
|
||||||
|
Timeout: 30 * time.Second, // Increased for better reliability
|
||||||
Transport: http2Transport,
|
Transport: http2Transport,
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
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 (
|
var (
|
||||||
// Pre-sized buffer pool for better performance
|
// Tiered buffer pools for different size requirements
|
||||||
bufferPool = sync.Pool{
|
smallBufferPool = sync.Pool{
|
||||||
New: func() interface{} {
|
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))
|
b := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
return b
|
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
|
// Pre-allocate error maps to reduce allocations
|
||||||
errPairsPool = sync.Pool{
|
errPairsPool = sync.Pool{
|
||||||
New: func() interface{} {
|
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
|
package gql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
@@ -11,9 +10,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (b *BaseClient) convertToJSON(v any) []byte {
|
func (b *BaseClient) convertToJSON(v any) []byte {
|
||||||
buf := bufferPool.Get().(*bytes.Buffer)
|
// Estimate size based on query structure for better buffer selection
|
||||||
defer bufferPool.Put(buf)
|
estimatedSize := 512 // Base size
|
||||||
buf.Reset()
|
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
|
// Use json.NewEncoder for better performance with buffers
|
||||||
enc := json.NewEncoder(buf)
|
enc := json.NewEncoder(buf)
|
||||||
@@ -42,6 +49,39 @@ func (b *BaseClient) convertToJSON(v any) []byte {
|
|||||||
return result
|
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 {
|
func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
|
||||||
var query string
|
var query string
|
||||||
var variables map[string]interface{}
|
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) {
|
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 {
|
if compiledQuery.JsonQuery == nil {
|
||||||
b.Logger.Error(&libpack_logger.LogMessage{
|
b.Logger.Error(&libpack_logger.LogMessage{
|
||||||
Message: "Can't compile query",
|
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},
|
Pairs: map[string]interface{}{"query": compiledQuery},
|
||||||
})
|
})
|
||||||
|
|
||||||
enableCache, enableRetries, recompileRequired := compiledQuery.parseHeadersAndVariables(headers)
|
|
||||||
if recompileRequired {
|
|
||||||
compiledQuery = b.compileQuery(query, variables)
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryHash string
|
var queryHash string
|
||||||
if (enableCache || b.cache_global) && strutil.HasPrefix(compiledQuery.Query, "query") {
|
if (enableCache || b.cache_global) && strutil.HasPrefix(compiledQuery.Query, "query") {
|
||||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
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)
|
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 {
|
if mockServer != nil {
|
||||||
mockServer.Close()
|
mockServer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup shared test cache
|
||||||
|
CleanupTestCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *Tests) SetupTest() {
|
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