Skip to content

Commit 33bb97d

Browse files
authored
Merge branch 'main' into schema-update/v0.64.2-7ee8e196a121397a
2 parents 7162b8d + ccbb875 commit 33bb97d

2 files changed

Lines changed: 48 additions & 7 deletions

File tree

internal/launcher/connection_pool.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,12 @@ func (p *SessionConnectionPool) Stop() {
206206

207207
// Get retrieves a connection from the pool
208208
func (p *SessionConnectionPool) Get(backendID, sessionID string) (*mcp.Connection, bool) {
209-
p.mu.RLock()
210-
defer p.mu.RUnlock()
209+
// Use a write lock for the entire operation to atomically read and update
210+
// metadata. The previous pattern of acquiring a read lock, manually unlocking,
211+
// and then acquiring a write lock introduced a race window in which another
212+
// goroutine could delete the connection between the two lock acquisitions.
213+
p.mu.Lock()
214+
defer p.mu.Unlock()
211215

212216
key := ConnectionKey{BackendID: backendID, SessionID: sessionID}
213217
metadata, exists := p.connections[key]
@@ -225,14 +229,9 @@ func (p *SessionConnectionPool) Get(backendID, sessionID string) (*mcp.Connectio
225229
logPool.Printf("Reusing connection: backend=%s, session=%s, requests=%d",
226230
backendID, sessionID, metadata.RequestCount)
227231

228-
// Update last used time and state (need write lock for this)
229-
p.mu.RUnlock()
230-
p.mu.Lock()
231232
metadata.LastUsedAt = time.Now()
232233
metadata.RequestCount++
233234
metadata.State = ConnectionStateActive
234-
p.mu.Unlock()
235-
p.mu.RLock()
236235

237236
return metadata.Connection, true
238237
}

internal/launcher/connection_pool_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,48 @@ func TestConnectionPoolConcurrency(t *testing.T) {
242242
assert.Equal(t, 1000, metadata.RequestCount)
243243
}
244244

245+
// TestConnectionPoolConcurrencyWithDeletes verifies that concurrent Get and Delete
246+
// operations do not race. Previously, Get used a manual RUnlock/Lock/RLock upgrade
247+
// that created a window in which another goroutine could delete the connection,
248+
// causing Get to update and return a deleted connection.
249+
func TestConnectionPoolConcurrencyWithDeletes(t *testing.T) {
250+
ctx := context.Background()
251+
pool := NewSessionConnectionPool(ctx)
252+
253+
mockConn := &mcp.Connection{}
254+
pool.Set("backend1", "session1", mockConn)
255+
256+
done := make(chan bool)
257+
258+
// Goroutines that continuously Get the connection
259+
for i := 0; i < 5; i++ {
260+
go func() {
261+
for j := 0; j < 200; j++ {
262+
pool.Get("backend1", "session1")
263+
}
264+
done <- true
265+
}()
266+
}
267+
268+
// Goroutines that interleave Set and Delete operations
269+
for i := 0; i < 3; i++ {
270+
go func() {
271+
for j := 0; j < 50; j++ {
272+
pool.Set("backend1", "session1", mockConn)
273+
pool.Delete("backend1", "session1")
274+
}
275+
done <- true
276+
}()
277+
}
278+
279+
for i := 0; i < 8; i++ {
280+
<-done
281+
}
282+
// No assertion needed — the goal is to detect races via -race flag.
283+
// If the race condition is present, this test will fail non-deterministically
284+
// under the Go race detector (go test -race).
285+
}
286+
245287
func TestConnectionPoolDeleteNonExistent(t *testing.T) {
246288
ctx := context.Background()
247289
pool := NewSessionConnectionPool(ctx)

0 commit comments

Comments
 (0)