/* Copyright IBM Corp. 2017 All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package cluster_test import ( "sync" "testing" "github.com/hyperledger/fabric/common/metrics/disabled" "github.com/hyperledger/fabric/orderer/common/cluster" "github.com/hyperledger/fabric/orderer/common/cluster/mocks" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestConcurrentConnections(t *testing.T) { // Scenario: Have 100 goroutines try to create a connection together at the same time, // wait until one of them succeeds, and then wait until they all return, // and also ensure they all return the same connection reference n := 100 var wg sync.WaitGroup wg.Add(n) dialer := &mocks.SecureDialer{} conn := &grpc.ClientConn{} dialer.On("Dial", mock.Anything, mock.Anything).Return(conn, nil) connStore := cluster.NewConnectionStore(dialer, &disabled.Gauge{}) connect := func() { defer wg.Done() conn2, err := connStore.Connection("", nil) require.NoError(t, err) require.True(t, conn2 == conn) } for i := 0; i < n; i++ { go connect() } wg.Wait() dialer.AssertNumberOfCalls(t, "Dial", 1) } type connectionMapperSpy struct { lookupDelay chan struct{} lookupInvoked chan struct{} cluster.ConnectionMapper } func (cms *connectionMapperSpy) Lookup(cert []byte) (*grpc.ClientConn, bool) { // Signal that Lookup() has been invoked cms.lookupInvoked <- struct{}{} // Wait for the main test to signal to advance. // This is needed because we need to ensure that all instances // of the connectionMapperSpy invoked Lookup() <-cms.lookupDelay return cms.ConnectionMapper.Lookup(cert) } func TestConcurrentLookupMiss(t *testing.T) { // Scenario: 2 concurrent connection attempts are made, // and the first 2 Lookup operations are delayed, // which makes the connection store attempt to connect // at the same time twice. // A single connection should be created regardless. dialer := &mocks.SecureDialer{} conn := &grpc.ClientConn{} dialer.On("Dial", mock.Anything, mock.Anything).Return(conn, nil) connStore := cluster.NewConnectionStore(dialer, &disabled.Gauge{}) // Wrap the connection mapping with a spy that intercepts Lookup() invocations spy := &connectionMapperSpy{ ConnectionMapper: connStore.Connections, lookupDelay: make(chan struct{}, 2), lookupInvoked: make(chan struct{}, 2), } connStore.Connections = spy var goroutinesExited sync.WaitGroup goroutinesExited.Add(2) for i := 0; i < 2; i++ { go func() { defer goroutinesExited.Done() conn2, err := connStore.Connection("", nil) require.NoError(t, err) // Ensure all calls for Connection() return the same reference // of the gRPC connection. require.True(t, conn2 == conn) }() } // Wait for the Lookup() to be invoked by both // goroutines <-spy.lookupInvoked <-spy.lookupInvoked // Signal the goroutines to finish the Lookup() invocations spy.lookupDelay <- struct{}{} spy.lookupDelay <- struct{}{} // Close the channel so that subsequent Lookup() operations won't be blocked close(spy.lookupDelay) // Wait for all goroutines to exit goroutinesExited.Wait() }