@@ -454,7 +454,7 @@ var _ = Describe("asyncNewConn", func() {
454454 PoolSize : 1 ,
455455 MaxConcurrentDials : 2 ,
456456 DialTimeout : 1 * time .Second ,
457- PoolTimeout : 1 * time .Second ,
457+ PoolTimeout : 2 * time .Second ,
458458 })
459459 defer testPool .Close ()
460460
@@ -463,42 +463,116 @@ var _ = Describe("asyncNewConn", func() {
463463 Expect (err ).NotTo (HaveOccurred ())
464464 Expect (conn1 ).NotTo (BeNil ())
465465
466- // This should trigger asyncNewConn since pool is exhausted
467- conn2 , err := testPool .Get (ctx )
468- Expect (err ).NotTo (HaveOccurred ())
469- Expect (conn2 ).NotTo (BeNil ())
466+ // Get second connection in another goroutine
467+ done := make (chan struct {})
468+ var conn2 * pool.Conn
469+ var err2 error
470+
471+ go func () {
472+ defer GinkgoRecover ()
473+ conn2 , err2 = testPool .Get (ctx )
474+ close (done )
475+ }()
470476
471- // Basic validation - we got two different connections
472- Expect ( conn1 ). NotTo ( Equal ( conn2 ) )
477+ // Wait a bit to let the second Get start waiting
478+ time . Sleep ( 100 * time . Millisecond )
473479
474- // Close connections without Put to avoid queue issues
475- _ = conn1 .Close ()
476- _ = conn2 .Close ()
480+ // Release first connection to let second Get acquire Turn
481+ testPool .Put (ctx , conn1 )
482+
483+ // Wait for second Get to complete
484+ <- done
485+ Expect (err2 ).NotTo (HaveOccurred ())
486+ Expect (conn2 ).NotTo (BeNil ())
487+
488+ // Clean up second connection
489+ testPool .Put (ctx , conn2 )
477490 })
478491
479- It ("should handle context cancellation" , func () {
492+ It ("should handle context cancellation before acquiring dialsInProgress" , func () {
493+ slowDialer := func (ctx context.Context ) (net.Conn , error ) {
494+ // Simulate slow dialing to let first connection creation occupy dialsInProgress
495+ time .Sleep (200 * time .Millisecond )
496+ return newDummyConn (), nil
497+ }
498+
480499 testPool := pool .NewConnPool (& pool.Options {
481- Dialer : dummyDialer ,
482- PoolSize : 1 ,
483- MaxConcurrentDials : 1 ,
500+ Dialer : slowDialer ,
501+ PoolSize : 2 ,
502+ MaxConcurrentDials : 1 , // Limit to 1 so second request cannot get dialsInProgress permission
484503 DialTimeout : 1 * time .Second ,
485504 PoolTimeout : 1 * time .Second ,
486505 })
487506 defer testPool .Close ()
488507
489- // Get the only connection
508+ // Start first connection creation, this will occupy dialsInProgress
509+ done1 := make (chan struct {})
510+ go func () {
511+ defer GinkgoRecover ()
512+ conn1 , err := testPool .Get (ctx )
513+ if err == nil {
514+ defer testPool .Put (ctx , conn1 )
515+ }
516+ close (done1 )
517+ }()
518+
519+ // Wait a bit to ensure first request starts and occupies dialsInProgress
520+ time .Sleep (50 * time .Millisecond )
521+
522+ // Create a context that will be cancelled quickly
523+ cancelCtx , cancel := context .WithTimeout (ctx , 100 * time .Millisecond )
524+ defer cancel ()
525+
526+ // Second request should timeout while waiting for dialsInProgress
527+ _ , err := testPool .Get (cancelCtx )
528+ Expect (err ).To (Equal (context .DeadlineExceeded ))
529+
530+ // Wait for first request to complete
531+ <- done1
532+ })
533+
534+ It ("should handle context cancellation while waiting for connection result" , func () {
535+ // This test focuses on proper error handling when context is cancelled
536+ // during asyncNewConn execution (not testing connection reuse)
537+
538+ slowDialer := func (ctx context.Context ) (net.Conn , error ) {
539+ // Simulate slow dialing
540+ time .Sleep (500 * time .Millisecond )
541+ return newDummyConn (), nil
542+ }
543+
544+ testPool := pool .NewConnPool (& pool.Options {
545+ Dialer : slowDialer ,
546+ PoolSize : 1 ,
547+ MaxConcurrentDials : 2 ,
548+ DialTimeout : 2 * time .Second ,
549+ PoolTimeout : 2 * time .Second ,
550+ })
551+ defer testPool .Close ()
552+
553+ // Get first connection to fill the pool
490554 conn1 , err := testPool .Get (ctx )
491555 Expect (err ).NotTo (HaveOccurred ())
492556
493- // Create cancelled context
494- cancelledCtx , cancel := context .WithCancel (ctx )
495- cancel ()
557+ // Create a context that will be cancelled during connection creation
558+ cancelCtx , cancel := context .WithTimeout (ctx , 200 * time . Millisecond )
559+ defer cancel ()
496560
497- // This should fail immediately with context cancelled
498- _ , err = testPool .Get (cancelledCtx )
499- Expect (err ).To (Equal (context .Canceled ))
561+ // This request should timeout while waiting for connection creation result
562+ // Testing the error handling path in asyncNewConn select statement
563+ done := make (chan struct {})
564+ var err2 error
565+ go func () {
566+ defer GinkgoRecover ()
567+ _ , err2 = testPool .Get (cancelCtx )
568+ close (done )
569+ }()
570+
571+ <- done
572+ Expect (err2 ).To (Equal (context .DeadlineExceeded ))
500573
501- _ = conn1 .Close ()
574+ // Clean up - release the first connection
575+ testPool .Put (ctx , conn1 )
502576 })
503577
504578 It ("should handle dial failures gracefully" , func () {
@@ -515,9 +589,163 @@ var _ = Describe("asyncNewConn", func() {
515589 })
516590 defer testPool .Close ()
517591
518- // This call should fail
592+ // This call should fail, testing error handling branch in goroutine
519593 _ , err := testPool .Get (ctx )
520594 Expect (err ).To (HaveOccurred ())
521595 Expect (err .Error ()).To (ContainSubstring ("dial failed" ))
522596 })
597+
598+ It ("should handle connection creation success with normal delivery" , func () {
599+ // This test verifies normal case where connection creation and delivery both succeed
600+ testPool := pool .NewConnPool (& pool.Options {
601+ Dialer : dummyDialer ,
602+ PoolSize : 1 ,
603+ MaxConcurrentDials : 2 ,
604+ DialTimeout : 1 * time .Second ,
605+ PoolTimeout : 2 * time .Second ,
606+ })
607+ defer testPool .Close ()
608+
609+ // Get first connection
610+ conn1 , err := testPool .Get (ctx )
611+ Expect (err ).NotTo (HaveOccurred ())
612+
613+ // Get second connection in another goroutine
614+ done := make (chan struct {})
615+ var conn2 * pool.Conn
616+ var err2 error
617+
618+ go func () {
619+ defer GinkgoRecover ()
620+ conn2 , err2 = testPool .Get (ctx )
621+ close (done )
622+ }()
623+
624+ // Wait a bit to let second Get start waiting
625+ time .Sleep (100 * time .Millisecond )
626+
627+ // Release first connection
628+ testPool .Put (ctx , conn1 )
629+
630+ // Wait for second Get to complete
631+ <- done
632+ Expect (err2 ).NotTo (HaveOccurred ())
633+ Expect (conn2 ).NotTo (BeNil ())
634+
635+ // Clean up second connection
636+ testPool .Put (ctx , conn2 )
637+ })
638+
639+ It ("should handle MaxConcurrentDials limit" , func () {
640+ testPool := pool .NewConnPool (& pool.Options {
641+ Dialer : dummyDialer ,
642+ PoolSize : 3 ,
643+ MaxConcurrentDials : 1 , // Only allow 1 concurrent dial
644+ DialTimeout : 1 * time .Second ,
645+ PoolTimeout : 1 * time .Second ,
646+ })
647+ defer testPool .Close ()
648+
649+ // Get all connections to fill the pool
650+ var conns []* pool.Conn
651+ for i := 0 ; i < 3 ; i ++ {
652+ conn , err := testPool .Get (ctx )
653+ Expect (err ).NotTo (HaveOccurred ())
654+ conns = append (conns , conn )
655+ }
656+
657+ // Now pool is full, next request needs to create new connection
658+ // But due to MaxConcurrentDials=1, only one concurrent dial is allowed
659+ done := make (chan struct {})
660+ var err4 error
661+ go func () {
662+ defer GinkgoRecover ()
663+ _ , err4 = testPool .Get (ctx )
664+ close (done )
665+ }()
666+
667+ // Release one connection to let the request complete
668+ time .Sleep (100 * time .Millisecond )
669+ testPool .Put (ctx , conns [0 ])
670+
671+ <- done
672+ Expect (err4 ).NotTo (HaveOccurred ())
673+
674+ // Clean up remaining connections
675+ for i := 1 ; i < len (conns ); i ++ {
676+ testPool .Put (ctx , conns [i ])
677+ }
678+ })
679+
680+ It ("should reuse connections created in background after request timeout" , func () {
681+ // This test focuses on connection reuse mechanism:
682+ // When a request times out but background connection creation succeeds,
683+ // the created connection should be added to pool for future reuse
684+
685+ slowDialer := func (ctx context.Context ) (net.Conn , error ) {
686+ // Simulate delay for connection creation
687+ time .Sleep (100 * time .Millisecond )
688+ return newDummyConn (), nil
689+ }
690+
691+ testPool := pool .NewConnPool (& pool.Options {
692+ Dialer : slowDialer ,
693+ PoolSize : 1 ,
694+ MaxConcurrentDials : 1 ,
695+ DialTimeout : 1 * time .Second ,
696+ PoolTimeout : 150 * time .Millisecond , // Short timeout for waiting Turn
697+ })
698+ defer testPool .Close ()
699+
700+ // Fill the pool with one connection
701+ conn1 , err := testPool .Get (ctx )
702+ Expect (err ).NotTo (HaveOccurred ())
703+ // Don't put it back yet, so pool is full
704+
705+ // Start a goroutine that will create a new connection but take time
706+ done1 := make (chan struct {})
707+ go func () {
708+ defer GinkgoRecover ()
709+ defer close (done1 )
710+ // This will trigger asyncNewConn since pool is full
711+ conn , err := testPool .Get (ctx )
712+ if err == nil {
713+ // Put connection back to pool after creation
714+ time .Sleep (50 * time .Millisecond )
715+ testPool .Put (ctx , conn )
716+ }
717+ }()
718+
719+ // Wait a bit to let the goroutine start and begin connection creation
720+ time .Sleep (50 * time .Millisecond )
721+
722+ // Now make a request that should timeout waiting for Turn
723+ start := time .Now ()
724+ _ , err = testPool .Get (ctx )
725+ duration := time .Since (start )
726+
727+ Expect (err ).To (Equal (pool .ErrPoolTimeout ))
728+ // Should timeout around PoolTimeout
729+ Expect (duration ).To (BeNumerically ("~" , 150 * time .Millisecond , 50 * time .Millisecond ))
730+
731+ // Release the first connection to allow the background creation to complete
732+ testPool .Put (ctx , conn1 )
733+
734+ // Wait for background connection creation to complete
735+ <- done1
736+ time .Sleep (100 * time .Millisecond )
737+
738+ // CORE TEST: Verify connection reuse mechanism
739+ // The connection created in background should now be available in pool
740+ start = time .Now ()
741+ conn3 , err := testPool .Get (ctx )
742+ duration = time .Since (start )
743+
744+ Expect (err ).NotTo (HaveOccurred ())
745+ Expect (conn3 ).NotTo (BeNil ())
746+ // Should be fast since connection is from pool (not newly created)
747+ Expect (duration ).To (BeNumerically ("<" , 50 * time .Millisecond ))
748+
749+ testPool .Put (ctx , conn3 )
750+ })
523751})
0 commit comments