Skip to content

Commit ae622f0

Browse files
committed
fix(pool): prevent double freeTurn in queuedNewConn
This commit fixes a critical race condition where freeTurn() could be called twice in the connection pool's queuedNewConn flow, causing turn counter inconsistency. Problem: - When a new connection creation failed in queuedNewConn, both the defer handler and the dialing goroutine could call freeTurn() - This led to turn counter underflow and queue length inconsistency Solution: - Modified putIdleConn to return a boolean indicating whether the caller needs to call freeTurn() - Returns true: connection was put back to pool, caller must free turn - Returns false: connection was delivered to a waiting request, turn will be freed by the receiving goroutine - Updated queuedNewConn to only call freeTurn() when putIdleConn returns true - Improved error handling flow in the dialing goroutine Changes: - putIdleConn now returns bool instead of void - Added comprehensive documentation for putIdleConn behavior - Refactored error handling in queuedNewConn goroutine - Updated test cases to reflect correct turn state expectations This ensures each turn is freed exactly once, preventing resource leaks and maintaining correct queue state.
1 parent f711eb0 commit ae622f0

File tree

2 files changed

+33
-25
lines changed

2 files changed

+33
-25
lines changed

internal/pool/pool.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,7 @@ func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) {
568568
var err error
569569
defer func() {
570570
if err != nil {
571-
if cn := w.cancel(); cn != nil {
572-
p.putIdleConn(ctx, cn)
571+
if cn := w.cancel(); cn != nil && p.putIdleConn(ctx, cn) {
573572
p.freeTurn()
574573
}
575574
}
@@ -593,14 +592,15 @@ func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) {
593592

594593
dialCtx := w.getCtxForDial()
595594
cn, cnErr := p.newConn(dialCtx, true)
596-
delivered := w.tryDeliver(cn, cnErr)
597-
if cnErr == nil && delivered {
598-
return
599-
} else if cnErr == nil && !delivered {
600-
p.putIdleConn(dialCtx, cn)
595+
if cnErr != nil {
596+
w.tryDeliver(nil, cnErr) // deliver error to caller, notify connection creation failed
601597
p.freeTurn()
602598
freeTurnCalled = true
603-
} else {
599+
return
600+
}
601+
602+
delivered := w.tryDeliver(cn, cnErr)
603+
if !delivered && p.putIdleConn(dialCtx, cn) {
604604
p.freeTurn()
605605
freeTurnCalled = true
606606
}
@@ -616,14 +616,20 @@ func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) {
616616
}
617617
}
618618

619-
func (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) {
619+
// putIdleConn puts a connection back to the pool or passes it to the next waiting request.
620+
//
621+
// It returns true if the connection was put back to the pool,
622+
// which means the turn needs to be freed directly by the caller,
623+
// or false if the connection was passed to the next waiting request,
624+
// which means the turn will be freed by the waiting goroutine after it returns.
625+
func (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) bool {
620626
for {
621627
w, ok := p.dialsQueue.dequeue()
622628
if !ok {
623629
break
624630
}
625631
if w.tryDeliver(cn, nil) {
626-
return
632+
return false
627633
}
628634
}
629635

@@ -632,12 +638,14 @@ func (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) {
632638

633639
if p.closed() {
634640
_ = cn.Close()
635-
return
641+
return true
636642
}
637643

638644
// poolSize is increased in newConn
639645
p.idleConns = append(p.idleConns, cn)
640646
p.idleConnsLen.Add(1)
647+
648+
return true
641649
}
642650

643651
func (p *ConnPool) waitTurn(ctx context.Context) error {

internal/pool/pool_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,22 +1020,22 @@ var _ = Describe("queuedNewConn", func() {
10201020
Expect(reqBErr).NotTo(HaveOccurred(), "Request B should receive Request A's connection")
10211021
Expect(reqBConn).NotTo(BeNil())
10221022

1023-
// CRITICAL CHECK: Turn leak detection
1023+
// FIRST CRITICAL CHECK: Turn state after connection delivery
10241024
// After Request B receives connection from putIdleConn:
1025-
// - Request A's turn SHOULD be released (via freeTurn)
1026-
// - Request B's turn is still held (will release on Put)
1027-
// Expected QueueLen: 1 (only Request B)
1028-
// If Bug exists (missing freeTurn): QueueLen: 2 (Request A's turn leaked)
1029-
time.Sleep(100 * time.Millisecond) // Allow time for turn release
1030-
currentQueueLen := testPool.QueueLen()
1031-
1032-
Expect(currentQueueLen).To(Equal(1),
1033-
"QueueLen should be 1 (only Request B holding turn). "+
1034-
"If it's 2, Request A's turn leaked due to missing freeTurn()")
1035-
1036-
// Cleanup
1025+
// - Request A's turn is held by Request B (connection delivered)
1026+
// - Request B's turn is still held by Request B's dial to complete the connection
1027+
// Expected QueueLen: 2 (Request B holding turn for connection usage)
1028+
time.Sleep(100 * time.Millisecond) // ~300ms total
1029+
Expect(testPool.QueueLen()).To(Equal(2))
1030+
1031+
// SECOND CRITICAL CHECK: Turn release after dial completion
1032+
// Wait for Request B's dial result to complete
1033+
time.Sleep(300 * time.Millisecond) // ~600ms total
1034+
Expect(testPool.QueueLen()).To(Equal(1))
1035+
1036+
// Cleanup and verify turn is released
10371037
testPool.Put(ctx, reqBConn)
1038-
Eventually(func() int { return testPool.QueueLen() }, "500ms").Should(Equal(0))
1038+
Eventually(func() int { return testPool.QueueLen() }, "600ms").Should(Equal(0))
10391039
})
10401040
})
10411041

0 commit comments

Comments
 (0)