github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rpc/connection/cache_test.go (about) 1 package connection 2 3 import ( 4 "net" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/onflow/crypto" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 "go.uber.org/atomic" 13 "google.golang.org/grpc" 14 "google.golang.org/grpc/credentials/insecure" 15 16 "github.com/onflow/flow-go/module/metrics" 17 "github.com/onflow/flow-go/utils/unittest" 18 ) 19 20 func TestCachedClientShutdown(t *testing.T) { 21 // Test that a completely uninitialized client can be closed without panics 22 t.Run("uninitialized client", func(t *testing.T) { 23 client := &CachedClient{ 24 closeRequested: atomic.NewBool(false), 25 } 26 client.Close() 27 assert.True(t, client.closeRequested.Load()) 28 }) 29 30 // Test closing a client with no outstanding requests 31 // Close() should return quickly 32 t.Run("with no outstanding requests", func(t *testing.T) { 33 client := &CachedClient{ 34 closeRequested: atomic.NewBool(false), 35 conn: setupGRPCServer(t), 36 } 37 38 unittest.RequireReturnsBefore(t, func() { 39 client.Close() 40 }, 100*time.Millisecond, "client timed out closing connection") 41 42 assert.True(t, client.closeRequested.Load()) 43 }) 44 45 // Test closing a client with outstanding requests waits for requests to complete 46 // Close() should block until the request completes 47 t.Run("with some outstanding requests", func(t *testing.T) { 48 client := &CachedClient{ 49 closeRequested: atomic.NewBool(false), 50 conn: setupGRPCServer(t), 51 } 52 done := client.AddRequest() 53 54 doneCalled := atomic.NewBool(false) 55 go func() { 56 defer done() 57 time.Sleep(50 * time.Millisecond) 58 doneCalled.Store(true) 59 }() 60 61 unittest.RequireReturnsBefore(t, func() { 62 client.Close() 63 }, 100*time.Millisecond, "client timed out closing connection") 64 65 assert.True(t, client.closeRequested.Load()) 66 assert.True(t, doneCalled.Load()) 67 }) 68 69 // Test closing a client that is already closing does not block 70 // Close() should return immediately 71 t.Run("already closing", func(t *testing.T) { 72 client := &CachedClient{ 73 closeRequested: atomic.NewBool(true), // close already requested 74 conn: setupGRPCServer(t), 75 } 76 done := client.AddRequest() 77 78 doneCalled := atomic.NewBool(false) 79 go func() { 80 defer done() 81 82 // use a long delay and require Close() to complete faster 83 time.Sleep(5 * time.Second) 84 doneCalled.Store(true) 85 }() 86 87 // should return immediately 88 unittest.RequireReturnsBefore(t, func() { 89 client.Close() 90 }, 10*time.Millisecond, "client timed out closing connection") 91 92 assert.True(t, client.closeRequested.Load()) 93 assert.False(t, doneCalled.Load()) 94 }) 95 96 // Test closing a client that is locked during connection setup 97 // Close() should wait for the lock before shutting down 98 t.Run("connection setting up", func(t *testing.T) { 99 client := &CachedClient{ 100 closeRequested: atomic.NewBool(false), 101 } 102 103 // simulate an in-progress connection setup 104 client.mu.Lock() 105 106 go func() { 107 // unlock after setting up the connection 108 defer client.mu.Unlock() 109 110 // pause before setting the connection to cause client.Close() to block 111 time.Sleep(100 * time.Millisecond) 112 client.conn = setupGRPCServer(t) 113 }() 114 115 // should wait at least 100 milliseconds before returning 116 unittest.RequireReturnsBefore(t, func() { 117 client.Close() 118 }, 500*time.Millisecond, "client timed out closing connection") 119 120 assert.True(t, client.closeRequested.Load()) 121 assert.NotNil(t, client.conn) 122 }) 123 } 124 125 // Test that rapid connections and disconnects do not cause a panic. 126 func TestConcurrentConnectionsAndDisconnects(t *testing.T) { 127 logger := unittest.Logger() 128 metrics := metrics.NewNoopCollector() 129 130 cache, err := NewCache(logger, metrics, 1) 131 require.NoError(t, err) 132 133 connectionCount := 100_000 134 conn := setupGRPCServer(t) 135 136 t.Run("test concurrent connections", func(t *testing.T) { 137 wg := sync.WaitGroup{} 138 wg.Add(connectionCount) 139 callCount := atomic.NewInt32(0) 140 for i := 0; i < connectionCount; i++ { 141 go func() { 142 defer wg.Done() 143 cachedConn, err := cache.GetConnected("foo", DefaultClientTimeout, nil, func(string, time.Duration, crypto.PublicKey, *CachedClient) (*grpc.ClientConn, error) { 144 callCount.Inc() 145 return conn, nil 146 }) 147 require.NoError(t, err) 148 149 done := cachedConn.AddRequest() 150 time.Sleep(1 * time.Millisecond) 151 done() 152 }() 153 } 154 unittest.RequireReturnsBefore(t, wg.Wait, time.Second, "timed out waiting for connections to finish") 155 156 // the client should be cached, so only a single connection is created 157 assert.Equal(t, int32(1), callCount.Load()) 158 }) 159 160 t.Run("test rapid connections and invalidations", func(t *testing.T) { 161 wg := sync.WaitGroup{} 162 wg.Add(connectionCount) 163 callCount := atomic.NewInt32(0) 164 for i := 0; i < connectionCount; i++ { 165 go func() { 166 defer wg.Done() 167 cachedConn, err := cache.GetConnected("foo", DefaultClientTimeout, nil, func(string, time.Duration, crypto.PublicKey, *CachedClient) (*grpc.ClientConn, error) { 168 callCount.Inc() 169 return conn, nil 170 }) 171 require.NoError(t, err) 172 173 done := cachedConn.AddRequest() 174 time.Sleep(1 * time.Millisecond) 175 cachedConn.Invalidate() 176 done() 177 }() 178 } 179 wg.Wait() 180 181 // since all connections are invalidated, the cache should be empty at the end 182 require.Eventually(t, func() bool { 183 return cache.Len() == 0 184 }, time.Second, 20*time.Millisecond, "cache should be empty") 185 186 // Many connections should be created, but some will be shared 187 assert.Greater(t, callCount.Load(), int32(1)) 188 assert.LessOrEqual(t, callCount.Load(), int32(connectionCount)) 189 }) 190 } 191 192 // setupGRPCServer starts a dummy grpc server for connection tests 193 func setupGRPCServer(t *testing.T) *grpc.ClientConn { 194 l, err := net.Listen("tcp", net.JoinHostPort("localhost", "0")) 195 require.NoError(t, err) 196 197 server := grpc.NewServer() 198 199 t.Cleanup(func() { 200 server.Stop() 201 }) 202 203 go func() { 204 err = server.Serve(l) 205 require.NoError(t, err) 206 }() 207 208 conn, err := grpc.Dial(l.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) 209 require.NoError(t, err) 210 211 return conn 212 }