@@ -604,6 +604,11 @@ var _ = Describe("queuedNewConn", func() {
604604
605605 // Wait for first request to complete
606606 <- done1
607+
608+ // Verify all turns are released after requests complete
609+ Eventually (func () int {
610+ return testPool .QueueLen ()
611+ }, "1s" , "50ms" ).Should (Equal (0 ), "All turns should be released after requests complete" )
607612 })
608613
609614 It ("should handle context cancellation while waiting for connection result" , func () {
@@ -646,8 +651,19 @@ var _ = Describe("queuedNewConn", func() {
646651 <- done
647652 Expect (err2 ).To (Equal (context .DeadlineExceeded ))
648653
654+ // Verify turn state - background goroutine may still hold turn
655+ // Note: Background connection creation will complete and release turn
656+ Eventually (func () int {
657+ return testPool .QueueLen ()
658+ }, "1s" , "50ms" ).Should (Equal (1 ), "Only conn1's turn should be held" )
659+
649660 // Clean up - release the first connection
650661 testPool .Put (ctx , conn1 )
662+
663+ // Verify all turns are released after cleanup
664+ Eventually (func () int {
665+ return testPool .QueueLen ()
666+ }, "1s" , "50ms" ).Should (Equal (0 ), "All turns should be released after cleanup" )
651667 })
652668
653669 It ("should handle dial failures gracefully" , func () {
@@ -668,6 +684,11 @@ var _ = Describe("queuedNewConn", func() {
668684 _ , err := testPool .Get (ctx )
669685 Expect (err ).To (HaveOccurred ())
670686 Expect (err .Error ()).To (ContainSubstring ("dial failed" ))
687+
688+ // Verify turn is released after dial failure
689+ Eventually (func () int {
690+ return testPool .QueueLen ()
691+ }, "1s" , "50ms" ).Should (Equal (0 ), "Turn should be released after dial failure" )
671692 })
672693
673694 It ("should handle connection creation success with normal delivery" , func () {
@@ -909,6 +930,112 @@ var _ = Describe("queuedNewConn", func() {
909930
910931 // Cleanup
911932 testPool .Put (context .Background (), conn2 )
933+
934+ // Verify turn is released after putIdleConn path completes
935+ // This is critical: ensures freeTurn() was called in the putIdleConn branch
936+ Eventually (func () int {
937+ return testPool .QueueLen ()
938+ }, "1s" , "50ms" ).Should (Equal (0 ),
939+ "Turn should be released after putIdleConn path completes" )
940+ })
941+
942+ It ("should not leak turn when delivering connection via putIdleConn" , func () {
943+ // This test verifies that freeTurn() is called when putIdleConn successfully
944+ // delivers a connection to another waiting request
945+ //
946+ // Scenario:
947+ // 1. Request A: timeout 150ms, connection creation takes 200ms
948+ // 2. Request B: timeout 500ms, connection creation takes 400ms
949+ // 3. Both requests enter dialsQueue and start async connection creation
950+ // 4. Request A times out at 150ms
951+ // 5. Request A's connection completes at 200ms
952+ // 6. putIdleConn delivers Request A's connection to Request B
953+ // 7. queuedNewConn must call freeTurn()
954+ // 8. Check: QueueLen should be 1 (only B holding turn), not 2 (A's turn leaked)
955+
956+ callCount := int32 (0 )
957+
958+ controlledDialer := func (ctx context.Context ) (net.Conn , error ) {
959+ count := atomic .AddInt32 (& callCount , 1 )
960+ if count == 1 {
961+ // Request A's connection: takes 200ms
962+ time .Sleep (200 * time .Millisecond )
963+ } else {
964+ // Request B's connection: takes 400ms (longer, so A's connection is used)
965+ time .Sleep (400 * time .Millisecond )
966+ }
967+ return newDummyConn (), nil
968+ }
969+
970+ testPool := pool .NewConnPool (& pool.Options {
971+ Dialer : controlledDialer ,
972+ PoolSize : 2 , // Allows both requests to get turns
973+ MaxConcurrentDials : 2 , // Allows both connections to be created simultaneously
974+ DialTimeout : 500 * time .Millisecond ,
975+ PoolTimeout : 1 * time .Second ,
976+ })
977+ defer testPool .Close ()
978+
979+ // Verify initial state
980+ Expect (testPool .QueueLen ()).To (Equal (0 ))
981+
982+ // Request A: Short timeout (150ms), connection takes 200ms
983+ reqADone := make (chan error , 1 )
984+ go func () {
985+ defer GinkgoRecover ()
986+ shortCtx , cancel := context .WithTimeout (ctx , 150 * time .Millisecond )
987+ defer cancel ()
988+ _ , err := testPool .Get (shortCtx )
989+ reqADone <- err
990+ }()
991+
992+ // Wait for Request A to acquire turn and enter dialsQueue
993+ time .Sleep (50 * time .Millisecond )
994+ Expect (testPool .QueueLen ()).To (Equal (1 ), "Request A should occupy turn" )
995+
996+ // Request B: Long timeout (500ms), will receive Request A's connection
997+ reqBDone := make (chan struct {})
998+ var reqBConn * pool.Conn
999+ var reqBErr error
1000+ go func () {
1001+ defer GinkgoRecover ()
1002+ longCtx , cancel := context .WithTimeout (ctx , 500 * time .Millisecond )
1003+ defer cancel ()
1004+ reqBConn , reqBErr = testPool .Get (longCtx )
1005+ close (reqBDone )
1006+ }()
1007+
1008+ // Wait for Request B to acquire turn and enter dialsQueue
1009+ time .Sleep (50 * time .Millisecond )
1010+ Expect (testPool .QueueLen ()).To (Equal (2 ), "Both requests should occupy turns" )
1011+
1012+ // Request A times out at 150ms
1013+ reqAErr := <- reqADone
1014+ Expect (reqAErr ).To (HaveOccurred (), "Request A should timeout" )
1015+
1016+ // Request A's connection completes at 200ms
1017+ // putIdleConn delivers it to Request B via tryDeliver
1018+ // queuedNewConn MUST call freeTurn() to release Request A's turn
1019+ <- reqBDone
1020+ Expect (reqBErr ).NotTo (HaveOccurred (), "Request B should receive Request A's connection" )
1021+ Expect (reqBConn ).NotTo (BeNil ())
1022+
1023+ // CRITICAL CHECK: Turn leak detection
1024+ // 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
1037+ testPool .Put (ctx , reqBConn )
1038+ Eventually (func () int { return testPool .QueueLen () }, "500ms" ).Should (Equal (0 ))
9121039 })
9131040})
9141041
0 commit comments