github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/connection/peerManager_test.go (about)

     1  package connection_test
     2  
     3  import (
     4  	"context"
     5  	"math/rand"
     6  	"os"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/ipfs/go-log"
    12  	"github.com/libp2p/go-libp2p/core/peer"
    13  	"github.com/rs/zerolog"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/mock"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/stretchr/testify/suite"
    18  
    19  	"github.com/onflow/flow-go/module/irrecoverable"
    20  	"github.com/onflow/flow-go/network/internal/p2pfixtures"
    21  	"github.com/onflow/flow-go/network/p2p/connection"
    22  	"github.com/onflow/flow-go/network/p2p/keyutils"
    23  	mockp2p "github.com/onflow/flow-go/network/p2p/mock"
    24  	"github.com/onflow/flow-go/utils/unittest"
    25  )
    26  
    27  type PeerManagerTestSuite struct {
    28  	suite.Suite
    29  	log zerolog.Logger
    30  }
    31  
    32  func TestPeerManagerTestSuite(t *testing.T) {
    33  	suite.Run(t, new(PeerManagerTestSuite))
    34  }
    35  
    36  func (suite *PeerManagerTestSuite) SetupTest() {
    37  	suite.log = zerolog.New(os.Stderr).Level(zerolog.ErrorLevel)
    38  	log.SetAllLoggers(log.LevelError)
    39  }
    40  
    41  func (suite *PeerManagerTestSuite) generatePeerIDs(n int) peer.IDSlice {
    42  	pids := peer.IDSlice{}
    43  	for i := 0; i < n; i++ {
    44  		key := p2pfixtures.NetworkingKeyFixtures(suite.T())
    45  		pid, err := keyutils.PeerIDFromFlowPublicKey(key.PublicKey())
    46  		require.NoError(suite.T(), err)
    47  		pids = append(pids, pid)
    48  	}
    49  
    50  	return pids
    51  }
    52  
    53  // TestUpdatePeers tests that updatePeers calls the connector with the expected list of ids to connect and disconnect
    54  // from. The tests are cumulative and ordered.
    55  func (suite *PeerManagerTestSuite) TestUpdatePeers() {
    56  	ctx, cancel := context.WithCancel(context.Background())
    57  	defer cancel()
    58  
    59  	// create some test ids
    60  	pids := suite.generatePeerIDs(10)
    61  
    62  	// create the connector mock to check ids requested for connect and disconnect
    63  	peerUpdater := mockp2p.NewPeerUpdater(suite.T())
    64  	peerUpdater.On("UpdatePeers", mock.Anything, mock.AnythingOfType("peer.IDSlice")).
    65  		Run(func(args mock.Arguments) {
    66  			idArg := args[1].(peer.IDSlice)
    67  			assert.ElementsMatch(suite.T(), pids, idArg)
    68  		}).
    69  		Return(nil)
    70  
    71  	// create the peer manager (but don't start it)
    72  	pm := connection.NewPeerManager(suite.log, connection.DefaultPeerUpdateInterval, peerUpdater)
    73  	pm.SetPeersProvider(func() peer.IDSlice {
    74  		return pids
    75  	})
    76  
    77  	// very first call to updatepeer
    78  	suite.Run("updatePeers only connects to all peers the first time", func() {
    79  		pm.ForceUpdatePeers(ctx)
    80  		peerUpdater.AssertNumberOfCalls(suite.T(), "UpdatePeers", 1)
    81  	})
    82  
    83  	// a subsequent call to updatePeers should request a connector.UpdatePeers to existing ids and new ids
    84  	suite.Run("updatePeers connects to old and new peers", func() {
    85  		// create a new id
    86  		newPIDs := suite.generatePeerIDs(1)
    87  		pids = append(pids, newPIDs...)
    88  
    89  		pm.ForceUpdatePeers(ctx)
    90  		peerUpdater.AssertNumberOfCalls(suite.T(), "UpdatePeers", 2)
    91  	})
    92  
    93  	// when ids are only excluded, connector.UpdatePeers should be called
    94  	suite.Run("updatePeers disconnects from extra peers", func() {
    95  		// delete an id
    96  		pids = removeRandomElement(pids)
    97  
    98  		pm.ForceUpdatePeers(ctx)
    99  		peerUpdater.AssertNumberOfCalls(suite.T(), "UpdatePeers", 3)
   100  	})
   101  
   102  	// addition and deletion of ids should result in a call to connector.UpdatePeers
   103  	suite.Run("updatePeers connects to new peers and disconnects from extra peers", func() {
   104  		// remove a couple of ids
   105  		pids = removeRandomElement(pids)
   106  		pids = removeRandomElement(pids)
   107  
   108  		// add a couple of new ids
   109  		newPIDs := suite.generatePeerIDs(2)
   110  		pids = append(pids, newPIDs...)
   111  
   112  		pm.ForceUpdatePeers(ctx)
   113  
   114  		peerUpdater.AssertNumberOfCalls(suite.T(), "UpdatePeers", 4)
   115  	})
   116  }
   117  
   118  func removeRandomElement(pids peer.IDSlice) peer.IDSlice {
   119  	i := rand.Intn(len(pids))
   120  	pids[i] = pids[len(pids)-1]
   121  	return pids[:len(pids)-1]
   122  }
   123  
   124  // TestPeriodicPeerUpdate tests that the peer manager runs periodically
   125  func (suite *PeerManagerTestSuite) TestPeriodicPeerUpdate() {
   126  	ctx, cancel := context.WithCancel(context.Background())
   127  	defer cancel()
   128  
   129  	signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx)
   130  
   131  	// create some test ids
   132  	pids := suite.generatePeerIDs(10)
   133  
   134  	peerUpdater := mockp2p.NewPeerUpdater(suite.T())
   135  	wg := &sync.WaitGroup{} // keeps track of number of calls on `ConnectPeers`
   136  	mu := &sync.Mutex{}     // provides mutual exclusion on calls to `ConnectPeers`
   137  	count := 0
   138  	times := 2 // we expect it to be called twice at least
   139  	wg.Add(times)
   140  	peerUpdater.On("UpdatePeers", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   141  		mu.Lock()
   142  		defer mu.Unlock()
   143  
   144  		if count < times {
   145  			count++
   146  			wg.Done()
   147  		}
   148  	}).Return(nil)
   149  
   150  	peerUpdateInterval := 10 * time.Millisecond
   151  	pm := connection.NewPeerManager(suite.log, peerUpdateInterval, peerUpdater)
   152  	pm.SetPeersProvider(func() peer.IDSlice {
   153  		return pids
   154  	})
   155  
   156  	pm.Start(signalerCtx)
   157  	unittest.RequireCloseBefore(suite.T(), pm.Ready(), 2*time.Second, "could not start peer manager")
   158  
   159  	unittest.RequireReturnsBefore(suite.T(), wg.Wait, 2*peerUpdateInterval,
   160  		"UpdatePeers is not running on UpdateIntervals")
   161  }
   162  
   163  // TestOnDemandPeerUpdate tests that the peer update can be requested on demand and in between the periodic runs
   164  func (suite *PeerManagerTestSuite) TestOnDemandPeerUpdate() {
   165  	ctx, cancel := context.WithCancel(context.Background())
   166  	defer cancel()
   167  
   168  	signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx)
   169  
   170  	// create some test ids
   171  	pids := suite.generatePeerIDs(10)
   172  
   173  	// chooses peer interval rate deliberately long to capture on demand peer update
   174  	peerUpdateInterval := time.Hour
   175  
   176  	// creates mock peerUpdater
   177  	wg := &sync.WaitGroup{} // keeps track of number of calls on `ConnectPeers`
   178  	mu := &sync.Mutex{}     // provides mutual exclusion on calls to `ConnectPeers`
   179  	count := 0
   180  	times := 2 // we expect it to be called twice overall
   181  	wg.Add(1)  // this accounts for one invocation, the other invocation is subsequent
   182  	peerUpdater := mockp2p.NewPeerUpdater(suite.T())
   183  	// captures the first periodic update initiated after start to complete
   184  	peerUpdater.On("UpdatePeers", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   185  		mu.Lock()
   186  		defer mu.Unlock()
   187  
   188  		if count < times {
   189  			count++
   190  			wg.Done()
   191  		}
   192  	}).Return(nil)
   193  
   194  	pm := connection.NewPeerManager(suite.log, peerUpdateInterval, peerUpdater)
   195  	pm.SetPeersProvider(func() peer.IDSlice {
   196  		return pids
   197  	})
   198  	pm.Start(signalerCtx)
   199  	unittest.RequireCloseBefore(suite.T(), pm.Ready(), 2*time.Second, "could not start peer manager")
   200  
   201  	unittest.RequireReturnsBefore(suite.T(), wg.Wait, 1*time.Second,
   202  		"UpdatePeers is not running on startup")
   203  
   204  	// makes a request for peer update
   205  	wg.Add(1) // expects a call to `ConnectPeers` by requesting peer update
   206  	pm.RequestPeerUpdate()
   207  
   208  	// assert that a call to connect to peers is made
   209  	unittest.RequireReturnsBefore(suite.T(), wg.Wait, 1*time.Second,
   210  		"UpdatePeers is not running on request")
   211  }
   212  
   213  // TestConcurrentOnDemandPeerUpdate tests that concurrent on-demand peer update request never block
   214  func (suite *PeerManagerTestSuite) TestConcurrentOnDemandPeerUpdate() {
   215  	ctx, cancel := context.WithCancel(context.Background())
   216  	defer cancel()
   217  
   218  	signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx)
   219  
   220  	// create some test ids
   221  	pids := suite.generatePeerIDs(10)
   222  
   223  	peerUpdater := mockp2p.NewPeerUpdater(suite.T())
   224  	// connectPeerGate channel gates the return of the peerUpdater
   225  	connectPeerGate := make(chan time.Time)
   226  	defer close(connectPeerGate)
   227  
   228  	// choose the periodic interval as a high value so that periodic runs don't interfere with this test
   229  	peerUpdateInterval := time.Hour
   230  
   231  	peerUpdater.On("UpdatePeers", mock.Anything, mock.Anything).Return(nil).
   232  		WaitUntil(connectPeerGate) // blocks call for connectPeerGate channel
   233  	pm := connection.NewPeerManager(suite.log, peerUpdateInterval, peerUpdater)
   234  	pm.SetPeersProvider(func() peer.IDSlice {
   235  		return pids
   236  	})
   237  
   238  	// start the peer manager
   239  	pm.Start(signalerCtx)
   240  
   241  	// this should trigger the first update and which will block on the ConnectPeers to return
   242  	unittest.RequireCloseBefore(suite.T(), pm.Ready(), 2*time.Second, "could not start peer manager")
   243  
   244  	// assert that the first update started
   245  	assert.Eventually(suite.T(), func() bool {
   246  		return peerUpdater.AssertNumberOfCalls(suite.T(), "UpdatePeers", 1)
   247  	}, 3*time.Second, 100*time.Millisecond)
   248  
   249  	// makes 10 concurrent request for peer update
   250  	unittest.RequireConcurrentCallsReturnBefore(suite.T(), pm.RequestPeerUpdate, 10, time.Second,
   251  		"concurrent peer update requests could not return on time")
   252  
   253  	// allow the first periodic update (which should be currently blocked) to finish
   254  	connectPeerGate <- time.Now()
   255  
   256  	// assert that only two calls to UpdatePeers were made (one by the periodic update and one by the on-demand update)
   257  	assert.Eventually(suite.T(), func() bool {
   258  		return peerUpdater.AssertNumberOfCalls(suite.T(), "UpdatePeers", 2)
   259  	}, 10*time.Second, 100*time.Millisecond)
   260  }