Skip to content

Commit c81f9c3

Browse files
Jai Raj ChoudharyJai Raj Choudhary
authored andcommitted
ix: resolve database connection leak issue in ORM extension
1 parent ce1f79f commit c81f9c3

5 files changed

Lines changed: 740 additions & 38 deletions

File tree

cmd/run.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,8 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter
402402
default:
403403
o.limiter.Accept()
404404

405-
ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout)
405+
ctxWithTimeout, cancel := context.WithTimeout(ctx, o.requestTimeout)
406+
defer cancel()
406407
ctxWithTimeout = context.WithValue(ctxWithTimeout, runner.ContextKey("").ParentDir(), loader.GetContext())
407408

408409
output, err = suiteRunner.RunTestCase(&testCase, dataContext, ctxWithTimeout)

docs/fixes/connection_leak_test.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// connection_leak_test.go
2+
// Test file to verify database connection leak fixes in atest-ext-store-orm
3+
4+
package fixes
5+
6+
import (
7+
"context"
8+
"errors"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// Note: This test file is designed to work with the atest-ext-store-orm extension
17+
// after applying the connection leak fix patch. It demonstrates how to test
18+
// connection management and validate the fix is working correctly.
19+
20+
// MockORMStore represents a mock implementation for testing connection leak fixes
21+
type MockORMStore struct {
22+
activeConnections map[string]int
23+
maxConnections int
24+
}
25+
26+
// NewMockORMStore creates a new mock ORM store for testing
27+
func NewMockORMStore(maxConnections int) *MockORMStore {
28+
return &MockORMStore{
29+
activeConnections: make(map[string]int),
30+
maxConnections: maxConnections,
31+
}
32+
}
33+
34+
// GetActiveConnections returns the number of active connections for a database
35+
func (m *MockORMStore) GetActiveConnections(database string) int {
36+
return m.activeConnections[database]
37+
}
38+
39+
// SimulateConnection simulates creating a connection to a database
40+
func (m *MockORMStore) SimulateConnection(database string) error {
41+
if m.activeConnections[database] >= m.maxConnections {
42+
return ErrTooManyConnections
43+
}
44+
m.activeConnections[database]++
45+
return nil
46+
}
47+
48+
// SimulateDisconnection simulates closing a connection
49+
func (m *MockORMStore) SimulateDisconnection(database string) {
50+
if m.activeConnections[database] > 0 {
51+
m.activeConnections[database]--
52+
}
53+
}
54+
55+
// ErrTooManyConnections represents the error when connection limit is exceeded
56+
var ErrTooManyConnections = errors.New("too many connections")
57+
58+
// TestDatabaseConnectionLeak verifies that database connections are properly managed
59+
// and don't leak when switching between different databases
60+
func TestDatabaseConnectionLeak(t *testing.T) {
61+
if testing.Short() {
62+
t.Skip("Skipping connection leak test in short mode")
63+
}
64+
65+
mockStore := NewMockORMStore(10)
66+
67+
// Test rapid database switching without connection leaks
68+
databases := []string{"db1", "db2", "db3"}
69+
70+
// Simulate multiple rapid switches
71+
for i := 0; i < 50; i++ {
72+
for _, db := range databases {
73+
// Simulate connection creation
74+
err := mockStore.SimulateConnection(db)
75+
require.NoError(t, err, "Should not exceed connection limit on iteration %d for db %s", i, db)
76+
77+
// Verify connection count is reasonable
78+
count := mockStore.GetActiveConnections(db)
79+
assert.LessOrEqual(t, count, 3, "Too many connections for db %s: %d", db, count)
80+
81+
// Simulate some work
82+
time.Sleep(time.Millisecond)
83+
84+
// Simulate connection cleanup
85+
mockStore.SimulateDisconnection(db)
86+
}
87+
}
88+
89+
// Verify all connections are cleaned up
90+
for _, db := range databases {
91+
count := mockStore.GetActiveConnections(db)
92+
assert.Equal(t, 0, count, "Connections not properly cleaned up for db %s", db)
93+
}
94+
}
95+
96+
// TestConnectionPoolConfiguration verifies connection pool settings work correctly
97+
func TestConnectionPoolConfiguration(t *testing.T) {
98+
tests := []struct {
99+
name string
100+
maxConnections int
101+
expectError bool
102+
}{
103+
{"Normal pool size", 10, false},
104+
{"Large pool size", 100, false},
105+
{"Small pool size", 1, false},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
mockStore := NewMockORMStore(tt.maxConnections)
111+
112+
// Try to create connections up to the limit
113+
for i := 0; i < tt.maxConnections; i++ {
114+
err := mockStore.SimulateConnection("testdb")
115+
assert.NoError(t, err, "Should not error within connection limit")
116+
}
117+
118+
// Try to exceed the limit
119+
err := mockStore.SimulateConnection("testdb")
120+
if tt.expectError {
121+
assert.Error(t, err, "Should error when exceeding connection limit")
122+
} else {
123+
// For our mock, we expect error when exceeding limit
124+
assert.Error(t, err, "Should error when exceeding connection limit")
125+
}
126+
})
127+
}
128+
}
129+
130+
// TestConnectionReuse verifies that connections are properly reused
131+
func TestConnectionReuse(t *testing.T) {
132+
mockStore := NewMockORMStore(10)
133+
134+
// Create and close connections multiple times
135+
for i := 0; i < 20; i++ {
136+
err := mockStore.SimulateConnection("reusedb")
137+
require.NoError(t, err, "Connection creation should not fail on iteration %d", i)
138+
139+
// Verify connection count
140+
count := mockStore.GetActiveConnections("reusedb")
141+
assert.LessOrEqual(t, count, 10, "Connection count should not exceed pool size")
142+
143+
mockStore.SimulateDisconnection("reusedb")
144+
}
145+
146+
// Final verification
147+
finalCount := mockStore.GetActiveConnections("reusedb")
148+
assert.Equal(t, 0, finalCount, "All connections should be closed")
149+
}
150+
151+
// TestConcurrentDatabaseAccess simulates concurrent access to multiple databases
152+
func TestConcurrentDatabaseAccess(t *testing.T) {
153+
if testing.Short() {
154+
t.Skip("Skipping concurrent test in short mode")
155+
}
156+
157+
mockStore := NewMockORMStore(5)
158+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
159+
defer cancel()
160+
161+
// Simulate concurrent access patterns
162+
done := make(chan bool, 2)
163+
164+
// Worker 1: Access db1 repeatedly
165+
go func() {
166+
defer func() { done <- true }()
167+
for {
168+
select {
169+
case <-ctx.Done():
170+
return
171+
default:
172+
err := mockStore.SimulateConnection("db1")
173+
if err == nil {
174+
time.Sleep(time.Millisecond * 10)
175+
mockStore.SimulateDisconnection("db1")
176+
}
177+
time.Sleep(time.Millisecond * 5)
178+
}
179+
}
180+
}()
181+
182+
// Worker 2: Access db2 repeatedly
183+
go func() {
184+
defer func() { done <- true }()
185+
for {
186+
select {
187+
case <-ctx.Done():
188+
return
189+
default:
190+
err := mockStore.SimulateConnection("db2")
191+
if err == nil {
192+
time.Sleep(time.Millisecond * 10)
193+
mockStore.SimulateDisconnection("db2")
194+
}
195+
time.Sleep(time.Millisecond * 5)
196+
}
197+
}
198+
}()
199+
200+
// Let workers run for a short time
201+
time.Sleep(time.Millisecond * 100)
202+
cancel()
203+
204+
// Wait for workers to finish
205+
<-done
206+
<-done
207+
208+
// Verify no connections are leaked
209+
db1Count := mockStore.GetActiveConnections("db1")
210+
db2Count := mockStore.GetActiveConnections("db2")
211+
assert.LessOrEqual(t, db1Count, 5, "db1 should not have excessive connections")
212+
assert.LessOrEqual(t, db2Count, 5, "db2 should not have excessive connections")
213+
}
214+
215+
// TestCacheKeyGeneration verifies that cache keys are properly generated
216+
func TestCacheKeyGeneration(t *testing.T) {
217+
tests := []struct {
218+
store string
219+
database string
220+
expected string
221+
}{
222+
{"mysql", "testdb", "mysql:testdb"},
223+
{"postgres", "proddb", "postgres:proddb"},
224+
{"sqlite", "local.db", "sqlite:local.db"},
225+
{"mysql", "", "mysql:"},
226+
{"", "testdb", ":testdb"},
227+
}
228+
229+
for _, tt := range tests {
230+
t.Run(tt.expected, func(t *testing.T) {
231+
cacheKey := generateCacheKey(tt.store, tt.database)
232+
assert.Equal(t, tt.expected, cacheKey, "Cache key should match expected format")
233+
})
234+
}
235+
}
236+
237+
// generateCacheKey creates a composite cache key for store and database
238+
func generateCacheKey(store, database string) string {
239+
return store + ":" + database
240+
}

0 commit comments

Comments
 (0)