Files
graphql-monitoring-proxy/request_coalescing_test.go
lukaszraczylo cedee416a8 improvements mid may 2025 (#24)
* General improvements and bug fixes.

* Improve tests coverage.

* fixup! Improve tests coverage.

* Update README.md with latest changes.

* Fix the uint32

* Resolve issue with race condition for logging.

* fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* Fix the test of the rate limiter

* Add default ratelimit.json file

* Update dependencies.

* Significant refactor.

* fixup! Significant refactor.

* fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
2025-09-30 18:27:33 +01:00

408 lines
9.7 KiB
Go

package main
import (
"errors"
"sync"
"sync/atomic"
"testing"
"time"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/stretchr/testify/assert"
)
func TestNewRequestCoalescer(t *testing.T) {
tests := []struct {
name string
enabled bool
}{
{
name: "enabled coalescer",
enabled: true,
},
{
name: "disabled coalescer",
enabled: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := libpack_logger.New()
monitoring := libpack_monitoring.NewMonitoring(&libpack_monitoring.InitConfig{})
rc := NewRequestCoalescer(tt.enabled, logger, monitoring)
assert.NotNil(t, rc)
assert.Equal(t, tt.enabled, rc.enabled)
})
}
}
func TestRequestCoalescer_Do_SingleRequest(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
executed := false
response := &CoalescedResponse{
Body: []byte("test response"),
StatusCode: 200,
}
fn := func() (*CoalescedResponse, error) {
executed = true
return response, nil
}
result, err := rc.Do("test-key", fn)
assert.NoError(t, err)
assert.True(t, executed)
assert.Equal(t, response, result)
stats := rc.GetStats()
assert.Equal(t, int64(1), stats["total_requests"])
assert.Equal(t, int64(1), stats["primary_requests"])
assert.Equal(t, int64(0), stats["coalesced_requests"])
}
func TestRequestCoalescer_Do_ConcurrentRequests(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
var executionCount atomic.Int32
response := &CoalescedResponse{
Body: []byte("test response"),
StatusCode: 200,
}
fn := func() (*CoalescedResponse, error) {
executionCount.Add(1)
time.Sleep(50 * time.Millisecond) // Simulate work
return response, nil
}
// Launch concurrent requests with the same key
concurrentRequests := 10
var wg sync.WaitGroup
wg.Add(concurrentRequests)
results := make([]*CoalescedResponse, concurrentRequests)
errs := make([]error, concurrentRequests)
for i := 0; i < concurrentRequests; i++ {
go func(index int) {
defer wg.Done()
results[index], errs[index] = rc.Do("same-key", fn)
}(i)
}
wg.Wait()
// Function should only execute once
assert.Equal(t, int32(1), executionCount.Load())
// All requests should get the same response
for i := 0; i < concurrentRequests; i++ {
assert.NoError(t, errs[i])
assert.Equal(t, response, results[i])
}
stats := rc.GetStats()
assert.Equal(t, int64(concurrentRequests), stats["total_requests"])
assert.Equal(t, int64(1), stats["primary_requests"])
assert.Equal(t, int64(concurrentRequests-1), stats["coalesced_requests"])
// Check backend savings
backendSavings := stats["backend_savings_pct"].(float64)
assert.Greater(t, backendSavings, 0.0)
}
func TestRequestCoalescer_Do_DifferentKeys(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
var executionCount atomic.Int32
fn := func() (*CoalescedResponse, error) {
executionCount.Add(1)
return &CoalescedResponse{Body: []byte("response")}, nil
}
// Concurrent requests with different keys
var wg sync.WaitGroup
keys := []string{"key1", "key2", "key3"}
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
rc.Do(k, fn)
}(key)
}
wg.Wait()
// Function should execute for each unique key
assert.Equal(t, int32(len(keys)), executionCount.Load())
stats := rc.GetStats()
assert.Equal(t, int64(3), stats["primary_requests"])
assert.Equal(t, int64(0), stats["coalesced_requests"])
}
func TestRequestCoalescer_Do_Error(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
expectedErr := errors.New("test error")
fn := func() (*CoalescedResponse, error) {
return nil, expectedErr
}
result, err := rc.Do("error-key", fn)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Error(t, result.Err)
assert.Equal(t, expectedErr, result.Err)
}
func TestRequestCoalescer_Do_ConcurrentWithError(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
expectedErr := errors.New("test error")
var executionCount atomic.Int32
fn := func() (*CoalescedResponse, error) {
executionCount.Add(1)
time.Sleep(50 * time.Millisecond)
return nil, expectedErr
}
// Launch concurrent requests
concurrentRequests := 5
var wg sync.WaitGroup
wg.Add(concurrentRequests)
results := make([]*CoalescedResponse, concurrentRequests)
for i := 0; i < concurrentRequests; i++ {
go func(index int) {
defer wg.Done()
results[index], _ = rc.Do("error-key", fn)
}(i)
}
wg.Wait()
// Function should only execute once
assert.Equal(t, int32(1), executionCount.Load())
// All requests should get the same error in response
for i := 0; i < concurrentRequests; i++ {
assert.NotNil(t, results[i])
assert.Error(t, results[i].Err)
assert.Equal(t, expectedErr, results[i].Err)
}
}
func TestRequestCoalescer_Do_Disabled(t *testing.T) {
rc := NewRequestCoalescer(false, libpack_logger.New(), nil)
var executionCount atomic.Int32
fn := func() (*CoalescedResponse, error) {
executionCount.Add(1)
return &CoalescedResponse{Body: []byte("response")}, nil
}
// Launch concurrent requests with the same key
concurrentRequests := 5
var wg sync.WaitGroup
wg.Add(concurrentRequests)
for i := 0; i < concurrentRequests; i++ {
go func() {
defer wg.Done()
rc.Do("same-key", fn)
}()
}
wg.Wait()
// When disabled, function should execute for each request
assert.Equal(t, int32(concurrentRequests), executionCount.Load())
stats := rc.GetStats()
assert.Equal(t, false, stats["enabled"])
}
func TestRequestCoalescer_GetStats(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
fn := func() (*CoalescedResponse, error) {
time.Sleep(10 * time.Millisecond)
return &CoalescedResponse{Body: []byte("response")}, nil
}
// Simulate some coalesced requests
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
rc.Do("key1", fn)
}()
}
wg.Wait()
// Add some non-coalesced requests
rc.Do("key2", fn)
rc.Do("key3", fn)
stats := rc.GetStats()
assert.Equal(t, true, stats["enabled"])
assert.Equal(t, int64(12), stats["total_requests"])
assert.Equal(t, int64(3), stats["primary_requests"])
assert.Equal(t, int64(9), stats["coalesced_requests"])
assert.Equal(t, int64(0), stats["inflight_count"])
coalescingRate := stats["coalescing_rate_pct"].(float64)
assert.Greater(t, coalescingRate, 0.0)
assert.LessOrEqual(t, coalescingRate, 100.0)
backendSavings := stats["backend_savings_pct"].(float64)
assert.Greater(t, backendSavings, 0.0)
}
func TestRequestCoalescer_Reset(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
fn := func() (*CoalescedResponse, error) {
return &CoalescedResponse{Body: []byte("response")}, nil
}
// Generate some activity
rc.Do("key1", fn)
rc.Do("key2", fn)
statsBefore := rc.GetStats()
assert.Greater(t, statsBefore["total_requests"].(int64), int64(0))
// Reset
rc.Reset()
statsAfter := rc.GetStats()
assert.Equal(t, int64(0), statsAfter["total_requests"])
assert.Equal(t, int64(0), statsAfter["primary_requests"])
assert.Equal(t, int64(0), statsAfter["coalesced_requests"])
}
func TestRequestCoalescer_RaceCondition(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
var executionCount atomic.Int32
fn := func() (*CoalescedResponse, error) {
executionCount.Add(1)
time.Sleep(5 * time.Millisecond)
return &CoalescedResponse{Body: []byte("response")}, nil
}
// Launch many concurrent requests in waves
waves := 5
requestsPerWave := 20
for wave := 0; wave < waves; wave++ {
var wg sync.WaitGroup
wg.Add(requestsPerWave)
for i := 0; i < requestsPerWave; i++ {
go func() {
defer wg.Done()
rc.Do("race-key", fn)
}()
}
wg.Wait()
time.Sleep(10 * time.Millisecond) // Small delay between waves
}
// Execution count should be much less than total requests
totalRequests := waves * requestsPerWave
assert.Less(t, int(executionCount.Load()), totalRequests)
stats := rc.GetStats()
assert.Equal(t, int64(totalRequests), stats["total_requests"])
}
func TestRequestCoalescer_BackendSavingsCalculation(t *testing.T) {
tests := []struct {
name string
totalRequests int64
coalescedRequests int64
expectedSavings float64
}{
{
name: "50% savings",
totalRequests: 100,
coalescedRequests: 50,
expectedSavings: 100.0, // 50 coalesced / 50 primary = 100%
},
{
name: "90% savings",
totalRequests: 100,
coalescedRequests: 90,
expectedSavings: 900.0, // 90 coalesced / 10 primary = 900%
},
{
name: "no savings",
totalRequests: 100,
coalescedRequests: 0,
expectedSavings: 0.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rc := NewRequestCoalescer(true, libpack_logger.New(), nil)
rc.totalRequests.Store(tt.totalRequests)
rc.coalescedRequests.Store(tt.coalescedRequests)
stats := rc.GetStats()
savings := stats["backend_savings_pct"].(float64)
assert.InDelta(t, tt.expectedSavings, savings, 0.1)
})
}
}
func TestRequestCoalescer_GlobalInstance(t *testing.T) {
rc := InitializeRequestCoalescer(true, libpack_logger.New(), nil)
assert.NotNil(t, rc)
// Should return the same instance
rc2 := GetRequestCoalescer()
assert.Equal(t, rc, rc2)
}
func TestMin(t *testing.T) {
tests := []struct {
a int
b int
expected int
}{
{a: 5, b: 10, expected: 5},
{a: 10, b: 5, expected: 5},
{a: 5, b: 5, expected: 5},
{a: 0, b: 10, expected: 0},
{a: -5, b: 5, expected: -5},
}
for _, tt := range tests {
result := min(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
}
}