Skip to content

Commit 098a671

Browse files
committed
update default values and testcase
1 parent c55133a commit 098a671

File tree

2 files changed

+253
-31
lines changed

2 files changed

+253
-31
lines changed

internal/pool/pool_test.go

Lines changed: 251 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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
})

options.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ type Limiter interface {
3131

3232
// Options keeps the settings to set up redis connection.
3333
type Options struct {
34-
3534
// Network type, either tcp or unix.
3635
//
3736
// default: is tcp.
@@ -266,13 +265,8 @@ func (opt *Options) init() {
266265
opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
267266
}
268267
if opt.MaxConcurrentDials == 0 {
269-
// Default to PoolSize/4+1, at least 1, at most PoolSize
270-
opt.MaxConcurrentDials = opt.PoolSize/4 + 1
271-
if opt.MaxConcurrentDials > opt.PoolSize {
272-
opt.MaxConcurrentDials = opt.PoolSize
273-
}
274-
} else if opt.MaxConcurrentDials < 0 {
275-
// Negative value means unlimited, set to PoolSize
268+
opt.MaxConcurrentDials = opt.PoolSize
269+
} else if opt.MaxConcurrentDials > opt.PoolSize {
276270
opt.MaxConcurrentDials = opt.PoolSize
277271
}
278272
if opt.ReadBufferSize == 0 {

0 commit comments

Comments
 (0)