mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
cedee416a8
* 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
408 lines
9.7 KiB
Go
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)
|
|
}
|
|
}
|