Files
steampipe/pkg/db/db_client/db_client_session_test.go
Nathan Wallace 189a1e38a2 Fix race condition on customSearchPath slice closes #4792 (#4837)
* Add test for #4792: customSearchPath data race

This test demonstrates the data race on the customSearchPath slice when
accessed concurrently from multiple goroutines without synchronization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix #4792: Add mutex protection for customSearchPath slice

The customSearchPath slice was being accessed concurrently from multiple
goroutines without synchronization, causing data races. This fix adds a
dedicated mutex (searchPathMutex) to protect all reads and writes to the
customSearchPath and searchPathPrefix fields.

Changes:
- Added searchPathMutex field to DbClient struct
- Initialize searchPathMutex in NewDbClient constructor
- Protected all customSearchPath writes in SetRequiredSessionSearchPath
- Protected all customSearchPath reads in GetRequiredSessionSearchPath
- Protected all customSearchPath reads in GetCustomSearchPath
- Fixed logging in ensureSessionSearchPath to use already-fetched value
- Updated test to initialize searchPathMutex for proper testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-16 11:51:41 -05:00

222 lines
6.4 KiB
Go

package db_client
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/turbot/steampipe/v2/pkg/db/db_common"
)
// TestDbClient_SessionRegistration verifies session registration in sessions map
func TestDbClient_SessionRegistration(t *testing.T) {
client := &DbClient{
sessions: make(map[uint32]*db_common.DatabaseSession),
sessionsMutex: &sync.Mutex{},
}
// Simulate session registration
backendPid := uint32(12345)
session := db_common.NewDBSession(backendPid)
client.sessionsMutex.Lock()
client.sessions[backendPid] = session
client.sessionsMutex.Unlock()
// Verify session is registered
client.sessionsMutex.Lock()
registeredSession, found := client.sessions[backendPid]
client.sessionsMutex.Unlock()
assert.True(t, found, "Session should be registered")
assert.Equal(t, backendPid, registeredSession.BackendPid, "Backend PID should match")
}
// TestDbClient_SessionUnregistration verifies session cleanup via BeforeClose
func TestDbClient_SessionUnregistration(t *testing.T) {
client := &DbClient{
sessions: make(map[uint32]*db_common.DatabaseSession),
sessionsMutex: &sync.Mutex{},
}
// Add sessions
backendPid1 := uint32(100)
backendPid2 := uint32(200)
client.sessionsMutex.Lock()
client.sessions[backendPid1] = db_common.NewDBSession(backendPid1)
client.sessions[backendPid2] = db_common.NewDBSession(backendPid2)
client.sessionsMutex.Unlock()
assert.Len(t, client.sessions, 2, "Should have 2 sessions")
// Simulate BeforeClose callback for one session
client.sessionsMutex.Lock()
delete(client.sessions, backendPid1)
client.sessionsMutex.Unlock()
// Verify only one session remains
client.sessionsMutex.Lock()
_, found1 := client.sessions[backendPid1]
_, found2 := client.sessions[backendPid2]
client.sessionsMutex.Unlock()
assert.False(t, found1, "First session should be removed")
assert.True(t, found2, "Second session should still exist")
assert.Len(t, client.sessions, 1, "Should have 1 session remaining")
}
// TestDbClient_ConcurrentSessionRegistration tests concurrent session additions
func TestDbClient_ConcurrentSessionRegistration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping concurrent test in short mode")
}
client := &DbClient{
sessions: make(map[uint32]*db_common.DatabaseSession),
sessionsMutex: &sync.Mutex{},
}
var wg sync.WaitGroup
numGoroutines := 100
// Concurrently add sessions
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id uint32) {
defer wg.Done()
backendPid := id
session := db_common.NewDBSession(backendPid)
client.sessionsMutex.Lock()
client.sessions[backendPid] = session
client.sessionsMutex.Unlock()
}(uint32(i))
}
wg.Wait()
// Verify all sessions were added
assert.Len(t, client.sessions, numGoroutines, "All sessions should be registered")
}
// TestDbClient_SessionMapGrowthUnbounded tests for potential memory leaks
// This verifies that sessions don't accumulate indefinitely
func TestDbClient_SessionMapGrowthUnbounded(t *testing.T) {
if testing.Short() {
t.Skip("Skipping large dataset test in short mode")
}
client := &DbClient{
sessions: make(map[uint32]*db_common.DatabaseSession),
sessionsMutex: &sync.Mutex{},
}
// Simulate many connections
numSessions := 10000
for i := 0; i < numSessions; i++ {
backendPid := uint32(i)
session := db_common.NewDBSession(backendPid)
client.sessionsMutex.Lock()
client.sessions[backendPid] = session
client.sessionsMutex.Unlock()
}
assert.Len(t, client.sessions, numSessions, "Should have all sessions")
// Simulate cleanup (BeforeClose callbacks)
for i := 0; i < numSessions; i++ {
backendPid := uint32(i)
client.sessionsMutex.Lock()
delete(client.sessions, backendPid)
client.sessionsMutex.Unlock()
}
// Verify all sessions are cleaned up
assert.Len(t, client.sessions, 0, "All sessions should be cleaned up")
}
// TestDbClient_SearchPathUpdates verifies session search path management
func TestDbClient_SearchPathUpdates(t *testing.T) {
client := &DbClient{
sessions: make(map[uint32]*db_common.DatabaseSession),
sessionsMutex: &sync.Mutex{},
customSearchPath: []string{"schema1", "schema2"},
}
// Add a session
backendPid := uint32(12345)
session := db_common.NewDBSession(backendPid)
client.sessionsMutex.Lock()
client.sessions[backendPid] = session
client.sessionsMutex.Unlock()
// Verify custom search path is set
assert.NotNil(t, client.customSearchPath, "Custom search path should be set")
assert.Len(t, client.customSearchPath, 2, "Should have 2 schemas in search path")
}
// TestDbClient_SessionConnectionNilSafety verifies handling of nil connections
func TestDbClient_SessionConnectionNilSafety(t *testing.T) {
session := db_common.NewDBSession(12345)
// Session is created with nil connection initially
assert.Nil(t, session.Connection, "New session should have nil connection initially")
}
// TestDbClient_SessionSearchPathUpdatesThreadSafe verifies that concurrent access
// to customSearchPath does not cause data races.
// Reference: https://github.com/turbot/steampipe/issues/4792
//
// This test simulates concurrent goroutines accessing and modifying the customSearchPath
// slice. Without proper synchronization, this causes a data race.
//
// Run with: go test -race -run TestDbClient_SessionSearchPathUpdatesThreadSafe
func TestDbClient_SessionSearchPathUpdatesThreadSafe(t *testing.T) {
// Create a DbClient with the fields we need for testing
client := &DbClient{
customSearchPath: []string{"public", "internal"},
userSearchPath: []string{"public"},
searchPathMutex: &sync.Mutex{},
}
// Number of concurrent operations to test
const numGoroutines = 100
var wg sync.WaitGroup
wg.Add(numGoroutines * 3)
// Simulate concurrent readers calling GetRequiredSessionSearchPath
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
_ = client.GetRequiredSessionSearchPath()
}()
}
// Simulate concurrent readers calling GetCustomSearchPath
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
_ = client.GetCustomSearchPath()
}()
}
// Simulate concurrent writers calling SetRequiredSessionSearchPath
// This is the most dangerous operation as it modifies the slice
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
ctx := context.Background()
// This will write to customSearchPath
_ = client.SetRequiredSessionSearchPath(ctx)
}()
}
wg.Wait()
}