TestDoubleFreeTurnBug demonstrates the double freeTurn bug where: 1. Dial goroutine creates a connection 2. Original waiter times out 3. putIdleConn delivers connection to another waiter 4. Dial goroutine calls freeTurn() (FIRST FREE) 5. Second waiter uses connection and calls Put() 6. Put() calls f
(t *testing.T)
| 20 | // This causes the semaphore to be released twice, allowing more concurrent |
| 21 | // operations than PoolSize allows. |
| 22 | func TestDoubleFreeTurnBug(t *testing.T) { |
| 23 | var dialCount atomic.Int32 |
| 24 | var putCount atomic.Int32 |
| 25 | |
| 26 | // Slow dialer - 150ms per dial |
| 27 | slowDialer := func(ctx context.Context) (net.Conn, error) { |
| 28 | dialCount.Add(1) |
| 29 | select { |
| 30 | case <-time.After(150 * time.Millisecond): |
| 31 | server, client := net.Pipe() |
| 32 | go func() { |
| 33 | defer server.Close() |
| 34 | buf := make([]byte, 1024) |
| 35 | for { |
| 36 | _, err := server.Read(buf) |
| 37 | if err != nil { |
| 38 | return |
| 39 | } |
| 40 | } |
| 41 | }() |
| 42 | return client, nil |
| 43 | case <-ctx.Done(): |
| 44 | return nil, ctx.Err() |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | opt := &Options{ |
| 49 | Dialer: slowDialer, |
| 50 | PoolSize: 10, // Small pool to make bug easier to trigger |
| 51 | MaxConcurrentDials: 10, |
| 52 | MinIdleConns: 0, |
| 53 | PoolTimeout: 100 * time.Millisecond, |
| 54 | DialTimeout: 5 * time.Second, |
| 55 | } |
| 56 | |
| 57 | connPool := NewConnPool(opt) |
| 58 | defer connPool.Close() |
| 59 | |
| 60 | // Scenario: |
| 61 | // 1. Request A starts dial (100ms timeout - will timeout before dial completes) |
| 62 | // 2. Request B arrives (500ms timeout - will wait in queue) |
| 63 | // 3. Request A times out at 100ms |
| 64 | // 4. Dial completes at 150ms |
| 65 | // 5. putIdleConn delivers connection to Request B |
| 66 | // 6. Dial goroutine calls freeTurn() - FIRST FREE |
| 67 | // 7. Request B uses connection and calls Put() |
| 68 | // 8. Put() calls freeTurn() - SECOND FREE (BUG!) |
| 69 | |
| 70 | var wg sync.WaitGroup |
| 71 | |
| 72 | // Request A: Short timeout, will timeout before dial completes |
| 73 | wg.Add(1) |
| 74 | go func() { |
| 75 | defer wg.Done() |
| 76 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) |
| 77 | defer cancel() |
| 78 | |
| 79 | cn, err := connPool.Get(ctx) |