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  }