Files
insights-go/concurrency_test.go
zeekayandHanzo Dev 8ee8950230 Go 1.27.1, and the code it can say more simply
The module declares go 1.27.1 and the adapter image follows. go fix
applied the modernizers and left ptrString and ptrBool with no callers,
so they are gone; omitzero is not applied, since it changes what the
JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:52:31 -07:00

482 lines
12 KiB
Go

package insights
import (
"fmt"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestConcurrentEnqueue tests thread-safety of the client under concurrent load
func TestConcurrentEnqueue(t *testing.T) {
t.Parallel()
goroutines := 20
eventsPerGoroutine := 50
totalEvents := goroutines * eventsPerGoroutine
pool := NewEventPool(totalEvents)
var received atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var b batch
json.NewDecoder(r.Body).Decode(&b)
received.Add(int64(len(b.Messages)))
w.WriteHeader(200)
}))
defer server.Close()
client, err := NewWithConfig("test-key", Config{
Endpoint: server.URL,
// Uses production defaults for BatchSize, MaxEnqueuedRequests
})
require.NoError(t, err)
var wg sync.WaitGroup
for g := range goroutines {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := range eventsPerGoroutine {
idx := goroutineID*eventsPerGoroutine + i
err := client.Enqueue(pool.Get(idx))
if err != nil {
t.Errorf("Enqueue failed: %v", err)
}
}
}(g)
}
wg.Wait()
client.Close()
require.Equal(t, int64(totalEvents), received.Load(),
"All events should be delivered")
}
// TestConcurrentEnqueueDifferentMessageTypes tests concurrent enqueueing of different message types
func TestConcurrentEnqueueDifferentMessageTypes(t *testing.T) {
t.Parallel()
goroutines := 10
messagesPerGoroutine := 20
var received atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var b batch
json.NewDecoder(r.Body).Decode(&b)
received.Add(int64(len(b.Messages)))
w.WriteHeader(200)
}))
defer server.Close()
client, err := NewWithConfig("test-key", Config{
Endpoint: server.URL,
// Uses production defaults for BatchSize, MaxEnqueuedRequests
})
require.NoError(t, err)
var wg sync.WaitGroup
for g := range goroutines {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := range messagesPerGoroutine {
msgType := i % 4
switch msgType {
case 0:
client.Enqueue(Capture{
DistinctId: fmt.Sprintf("user_%d", goroutineID),
Event: fmt.Sprintf("event_%d", i),
})
case 1:
client.Enqueue(Identify{
DistinctId: fmt.Sprintf("user_%d", goroutineID),
Properties: Properties{"name": fmt.Sprintf("User %d", goroutineID)},
})
case 2:
client.Enqueue(Alias{
DistinctId: fmt.Sprintf("user_%d", goroutineID),
Alias: fmt.Sprintf("alias_%d", goroutineID),
})
case 3:
client.Enqueue(GroupIdentify{
Type: "company",
Key: fmt.Sprintf("company_%d", goroutineID),
Properties: Properties{
"name": fmt.Sprintf("Company %d", goroutineID),
},
})
}
}
}(g)
}
wg.Wait()
client.Close()
expectedTotal := int64(goroutines * messagesPerGoroutine)
require.Equal(t, expectedTotal, received.Load(),
"All messages should be delivered")
}
// TestConcurrentFeatureFlagEvaluation tests concurrent flag evaluation with cache updates
func TestConcurrentFeatureFlagEvaluation(t *testing.T) {
t.Parallel()
// Setup mock server with feature flags
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
// Return proper format for both local evaluation and flags endpoints
if r.URL.Path == "/flags/definitions" {
json.NewEncoder(w).Encode(map[string]any{
"flags": []map[string]any{
{
"id": 1,
"key": "test-flag",
"active": true,
"filters": map[string]any{
"groups": []map[string]any{
{
"rollout_percentage": 100,
},
},
},
},
},
"group_type_mapping": map[string]string{},
})
} else {
json.NewEncoder(w).Encode(map[string]any{
"featureFlags": map[string]any{
"test-flag": true,
},
})
}
}))
defer server.Close()
client, err := NewWithConfig("test-key", Config{
Endpoint: server.URL,
PersonalApiKey: "test-key",
})
require.NoError(t, err)
defer client.Close()
// Wait for initial flag fetch to complete
time.Sleep(100 * time.Millisecond)
goroutines := 100
evaluationsPerGoroutine := 50
var wg sync.WaitGroup
errorCount := atomic.Int64{}
for g := range goroutines {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := range evaluationsPerGoroutine {
distinctId := fmt.Sprintf("user_%d_%d", goroutineID, i)
_, err := client.GetFeatureFlag(FeatureFlagPayload{
Key: "test-flag",
DistinctId: distinctId,
})
if err != nil {
errorCount.Add(1)
}
}
}(g)
}
wg.Wait()
// With httptest.Server, there are no network issues - all evaluations should succeed
require.Equal(t, int64(0), errorCount.Load(),
"All feature flag evaluations should succeed - httptest.Server has no network issues")
}
// TestConcurrentClientOperations tests mixing Enqueue with Close operations
func TestConcurrentClientOperations(t *testing.T) {
t.Parallel()
// This tests that Close() properly waits for all pending operations
for iteration := range 5 {
t.Run(fmt.Sprintf("iteration_%d", iteration), func(t *testing.T) {
var received atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var b batch
json.NewDecoder(r.Body).Decode(&b)
received.Add(int64(len(b.Messages)))
w.WriteHeader(200)
}))
defer server.Close()
client, err := NewWithConfig("test-key", Config{
Endpoint: server.URL,
BatchSize: 50,
// Uses production defaults for MaxEnqueuedRequests
})
require.NoError(t, err)
// Start enqueueing
enqueueCount := 50
var wg sync.WaitGroup
for i := range enqueueCount {
wg.Add(1)
go func(idx int) {
defer wg.Done()
client.Enqueue(Capture{
DistinctId: fmt.Sprintf("user_%d", idx),
Event: "test_event",
})
}(i)
}
// Wait for all enqueues to complete
wg.Wait()
// Close should wait for all events to be sent
err = client.Close()
require.NoError(t, err)
// All events should have been delivered by the time Close returns
require.Equal(t, int64(enqueueCount), received.Load(),
"All events should be delivered before Close returns")
})
}
}
// TestConcurrentEnqueueWithSlowServer tests behavior with a slow server
func TestConcurrentEnqueueWithSlowServer(t *testing.T) {
goroutines := 20
eventsPerGoroutine := 10
totalEvents := goroutines * eventsPerGoroutine
pool := NewEventPool(totalEvents)
var received atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate slow server
time.Sleep(10 * time.Millisecond)
var b batch
json.NewDecoder(r.Body).Decode(&b)
received.Add(int64(len(b.Messages)))
w.WriteHeader(200)
}))
defer server.Close()
client, err := NewWithConfig("test-key", Config{
Endpoint: server.URL,
// Uses production defaults for BatchSize, MaxEnqueuedRequests
})
require.NoError(t, err)
var wg sync.WaitGroup
start := time.Now()
for g := range goroutines {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := range eventsPerGoroutine {
idx := goroutineID*eventsPerGoroutine + i
client.Enqueue(pool.Get(idx))
}
}(g)
}
wg.Wait()
client.Close()
elapsed := time.Since(start)
t.Logf("Slow server test: %d events in %v", totalEvents, elapsed)
require.Equal(t, int64(totalEvents), received.Load(),
"All events should be delivered even with slow server")
}
// TestConcurrentPrepareForSend tests that prepareForSend is thread-safe
func TestConcurrentPrepareForSend(t *testing.T) {
t.Parallel()
capture := Capture{
Type: "capture",
DistinctId: "test_user",
Event: "test_event",
Properties: generatePropertiesWithCardinality(42, CardinalityMedium),
Groups: generateGroupsWithCardinality(42, CardinalityMedium),
}
goroutines := 100
callsPerGoroutine := 100
var wg sync.WaitGroup
results := make([]int, goroutines)
for g := range goroutines {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
var total int
for range callsPerGoroutine {
data, _, err := prepareForSend(capture)
if err != nil {
t.Errorf("prepareForSend error: %v", err)
return
}
total += len(data)
}
results[goroutineID] = total / callsPerGoroutine
}(g)
}
wg.Wait()
// All goroutines should get the same average result
first := results[0]
for i, result := range results {
if result != first {
t.Errorf("Inconsistent prepareForSend size: goroutine 0 got %d, goroutine %d got %d",
first, i, result)
}
}
}
// TestConcurrentEventPoolAccess tests thread-safe access to EventPool
func TestConcurrentEventPoolAccess(t *testing.T) {
t.Parallel()
poolSize := 1000
pool := NewEventPool(poolSize)
goroutines := 50
accessesPerGoroutine := 200
var wg sync.WaitGroup
errorCount := atomic.Int64{}
for range goroutines {
wg.Go(func() {
for range accessesPerGoroutine {
event := pool.Next()
if event.DistinctId == "" || event.Event == "" {
errorCount.Add(1)
}
}
})
}
wg.Wait()
require.Equal(t, int64(0), errorCount.Load(),
"No errors should occur during concurrent pool access")
}
// TestConcurrentCallbackExecution tests that callbacks are executed thread-safely
func TestConcurrentCallbackExecution(t *testing.T) {
t.Parallel()
callback := NewUnifiedCallback(t)
var received atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var b batch
json.NewDecoder(r.Body).Decode(&b)
received.Add(int64(len(b.Messages)))
w.WriteHeader(200)
}))
defer server.Close()
client, err := NewWithConfig("test-key", Config{
Endpoint: server.URL,
BatchSize: 50,
Callback: callback,
// Uses production defaults for MaxEnqueuedRequests
})
require.NoError(t, err)
goroutines := 10
eventsPerGoroutine := 20
var wg sync.WaitGroup
for g := range goroutines {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := range eventsPerGoroutine {
client.Enqueue(Capture{
DistinctId: fmt.Sprintf("user_%d", goroutineID),
Event: fmt.Sprintf("event_%d", i),
})
}
}(g)
}
wg.Wait()
client.Close()
successCount, failureCount := callback.GetCounts()
t.Logf("Callback counts: success=%d, failure=%d", successCount, failureCount)
// All events should have been delivered with zero failures
totalEvents := int64(goroutines * eventsPerGoroutine)
require.Equal(t, totalEvents, received.Load(),
"All events should be delivered")
require.Equal(t, 0, failureCount,
"No callback failures should occur")
}
// TestRaceConditionDetection is specifically designed to trigger race detection
// Run with: go test -race -run TestRaceConditionDetection
func TestRaceConditionDetection(t *testing.T) {
t.Parallel()
t.Run("concurrent_enqueue_and_close", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
defer server.Close()
for range 10 {
client, _ := NewWithConfig("test-key", Config{
Endpoint: server.URL,
// Uses production defaults for BatchSize, MaxEnqueuedRequests
})
var wg sync.WaitGroup
// Concurrent enqueues
for j := range 20 {
wg.Add(1)
go func(idx int) {
defer wg.Done()
client.Enqueue(Capture{
DistinctId: fmt.Sprintf("user_%d", idx),
Event: "test",
})
}(j)
}
wg.Wait()
client.Close()
}
})
t.Run("concurrent_pool_access", func(t *testing.T) {
pool := NewEventPool(100)
var wg sync.WaitGroup
for range 100 {
wg.Go(func() {
for range 50 {
_ = pool.Next()
}
})
}
wg.Wait()
})
}