go.uber.org/yarpc@v1.72.1/peer/roundrobin/list_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package roundrobin
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"net"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/golang/mock/gomock"
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  	"go.uber.org/multierr"
    35  	"go.uber.org/yarpc"
    36  	"go.uber.org/yarpc/api/peer"
    37  	. "go.uber.org/yarpc/api/peer/peertest"
    38  	"go.uber.org/yarpc/api/transport"
    39  	"go.uber.org/yarpc/api/x/introspection"
    40  	"go.uber.org/yarpc/internal/testtime"
    41  	"go.uber.org/yarpc/internal/whitespace"
    42  	"go.uber.org/yarpc/peer/abstractpeer"
    43  	"go.uber.org/yarpc/peer/hostport"
    44  	"go.uber.org/yarpc/transport/http"
    45  	"go.uber.org/yarpc/yarpcconfig"
    46  	"go.uber.org/yarpc/yarpcerrors"
    47  	"go.uber.org/yarpc/yarpctest"
    48  	"go.uber.org/zap/zaptest"
    49  )
    50  
    51  var _notRunningErrorFormat = `"round-robin" peer list is not running: %s`
    52  
    53  func newNotRunningError(err string) error {
    54  	return yarpcerrors.FailedPreconditionErrorf(_notRunningErrorFormat, err)
    55  }
    56  
    57  func TestRoundRobinList(t *testing.T) {
    58  	type testStruct struct {
    59  		msg string
    60  
    61  		// PeerIDs that will be returned from the transport's OnRetain with "Available" status
    62  		retainedAvailablePeerIDs []string
    63  
    64  		// PeerIDs that will be returned from the transport's OnRetain with "Unavailable" status
    65  		retainedUnavailablePeerIDs []string
    66  
    67  		// PeerIDs that will be released from the transport
    68  		releasedPeerIDs []string
    69  
    70  		// PeerIDs that will return "retainErr" from the transport's OnRetain function
    71  		errRetainedPeerIDs []string
    72  		retainErr          error
    73  
    74  		// PeerIDs that will return "releaseErr" from the transport's OnRelease function
    75  		errReleasedPeerIDs []string
    76  		releaseErr         error
    77  
    78  		// A list of actions that will be applied on the PeerList
    79  		peerListActions []PeerListAction
    80  
    81  		// PeerIDs expected to be in the PeerList's "Available" list after the actions have been applied
    82  		expectedAvailablePeers []string
    83  
    84  		// PeerIDs expected to be in the PeerList's "Unavailable" list after the actions have been applied
    85  		expectedUnavailablePeers []string
    86  
    87  		// PeerIDs expected to be in the PeerList's "Uninitialized" list after the actions have been applied
    88  		expectedUninitializedPeers []string
    89  
    90  		// Boolean indicating whether the PeerList is "running" after the actions have been applied
    91  		expectedRunning bool
    92  
    93  		// Boolean indicating whether peers should be shuffled
    94  		shuffle bool
    95  	}
    96  	tests := []testStruct{
    97  		{
    98  			msg:                      "setup",
    99  			retainedAvailablePeerIDs: []string{"1"},
   100  			expectedAvailablePeers:   []string{"1"},
   101  			peerListActions: []PeerListAction{
   102  				StartAction{},
   103  				UpdateAction{AddedPeerIDs: []string{"1"}},
   104  			},
   105  			expectedRunning: true,
   106  		},
   107  		{
   108  			msg:                        "setup with disconnected",
   109  			retainedAvailablePeerIDs:   []string{"1"},
   110  			retainedUnavailablePeerIDs: []string{"2"},
   111  			peerListActions: []PeerListAction{
   112  				StartAction{},
   113  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   114  			},
   115  			expectedAvailablePeers:   []string{"1"},
   116  			expectedUnavailablePeers: []string{"2"},
   117  			expectedRunning:          true,
   118  		},
   119  		{
   120  			msg:                      "start",
   121  			retainedAvailablePeerIDs: []string{"1"},
   122  			expectedAvailablePeers:   []string{"1"},
   123  			peerListActions: []PeerListAction{
   124  				StartAction{},
   125  				UpdateAction{AddedPeerIDs: []string{"1"}},
   126  				ChooseAction{
   127  					ExpectedPeer: "1",
   128  				},
   129  			},
   130  			expectedRunning: true,
   131  		},
   132  		{
   133  			msg:                        "start stop",
   134  			retainedAvailablePeerIDs:   []string{"1", "2", "3", "4", "5", "6"},
   135  			retainedUnavailablePeerIDs: []string{"7", "8", "9"},
   136  			releasedPeerIDs:            []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"},
   137  			peerListActions: []PeerListAction{
   138  				StartAction{},
   139  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}},
   140  				StopAction{},
   141  				ChooseAction{
   142  					ExpectedErr:         newNotRunningError("could not wait for instance to start running: current state is \"stopped\""),
   143  					InputContextTimeout: 10 * time.Millisecond,
   144  				},
   145  			},
   146  			expectedUninitializedPeers: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"},
   147  			expectedRunning:            false,
   148  		},
   149  		{
   150  			msg:                      "start many and choose",
   151  			retainedAvailablePeerIDs: []string{"1", "2", "3", "4", "5", "6"},
   152  			expectedAvailablePeers:   []string{"1", "2", "3", "4", "5", "6"},
   153  			peerListActions: []PeerListAction{
   154  				StartAction{},
   155  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3", "4", "5", "6"}},
   156  				ChooseAction{ExpectedPeer: "1"},
   157  				ChooseAction{ExpectedPeer: "2"},
   158  				ChooseAction{ExpectedPeer: "3"},
   159  				ChooseAction{ExpectedPeer: "4"},
   160  				ChooseAction{ExpectedPeer: "5"},
   161  				ChooseAction{ExpectedPeer: "6"},
   162  				ChooseAction{ExpectedPeer: "1"},
   163  			},
   164  			expectedRunning: true,
   165  		},
   166  		{
   167  			msg:                      "assure start is idempotent",
   168  			retainedAvailablePeerIDs: []string{"1"},
   169  			expectedAvailablePeers:   []string{"1"},
   170  			peerListActions: []PeerListAction{
   171  				StartAction{},
   172  				UpdateAction{AddedPeerIDs: []string{"1"}},
   173  				StartAction{},
   174  				StartAction{},
   175  				ChooseAction{
   176  					ExpectedPeer: "1",
   177  				},
   178  			},
   179  			expectedRunning: true,
   180  		},
   181  		{
   182  			msg:                      "stop no start",
   183  			retainedAvailablePeerIDs: []string{},
   184  			releasedPeerIDs:          []string{},
   185  			peerListActions: []PeerListAction{
   186  				StopAction{},
   187  			},
   188  			expectedRunning: false,
   189  		},
   190  		{
   191  			msg:                "update retain error",
   192  			errRetainedPeerIDs: []string{"1"},
   193  			retainErr:          peer.ErrInvalidPeerType{},
   194  			peerListActions: []PeerListAction{
   195  				StartAction{},
   196  				UpdateAction{AddedPeerIDs: []string{"1"}, ExpectedErr: peer.ErrInvalidPeerType{}},
   197  			},
   198  			expectedRunning: true,
   199  		},
   200  		{
   201  			msg:                      "update retain multiple errors",
   202  			retainedAvailablePeerIDs: []string{"2"},
   203  			errRetainedPeerIDs:       []string{"1", "3"},
   204  			retainErr:                peer.ErrInvalidPeerType{},
   205  			peerListActions: []PeerListAction{
   206  				StartAction{},
   207  				UpdateAction{
   208  					AddedPeerIDs: []string{"1", "2", "3"},
   209  					ExpectedErr:  multierr.Combine(peer.ErrInvalidPeerType{}, peer.ErrInvalidPeerType{}),
   210  				},
   211  			},
   212  			expectedAvailablePeers: []string{"2"},
   213  			expectedRunning:        true,
   214  		},
   215  		{
   216  			msg:                      "start stop release error",
   217  			retainedAvailablePeerIDs: []string{"1"},
   218  			errReleasedPeerIDs:       []string{"1"},
   219  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   220  			peerListActions: []PeerListAction{
   221  				StartAction{},
   222  				UpdateAction{AddedPeerIDs: []string{"1"}},
   223  				StopAction{
   224  					ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   225  				},
   226  			},
   227  			expectedUninitializedPeers: []string{"1"},
   228  			expectedRunning:            false,
   229  		},
   230  		{
   231  			msg:                      "assure stop is idempotent",
   232  			retainedAvailablePeerIDs: []string{"1"},
   233  			errReleasedPeerIDs:       []string{"1"},
   234  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   235  			peerListActions: []PeerListAction{
   236  				StartAction{},
   237  				UpdateAction{AddedPeerIDs: []string{"1"}},
   238  				ConcurrentAction{
   239  					Actions: []PeerListAction{
   240  						StopAction{
   241  							ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   242  						},
   243  						StopAction{
   244  							ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   245  						},
   246  						StopAction{
   247  							ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   248  						},
   249  					},
   250  				},
   251  			},
   252  			expectedUninitializedPeers: []string{"1"},
   253  			expectedRunning:            false,
   254  		},
   255  		{
   256  			msg:                      "start stop release multiple errors",
   257  			retainedAvailablePeerIDs: []string{"1", "2", "3"},
   258  			releasedPeerIDs:          []string{"2"},
   259  			errReleasedPeerIDs:       []string{"1", "3"},
   260  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   261  			peerListActions: []PeerListAction{
   262  				StartAction{},
   263  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3"}},
   264  				StopAction{
   265  					ExpectedErr: multierr.Combine(
   266  						peer.ErrTransportHasNoReferenceToPeer{},
   267  						peer.ErrTransportHasNoReferenceToPeer{},
   268  					),
   269  				},
   270  			},
   271  			expectedUninitializedPeers: []string{"1", "2", "3"},
   272  			expectedRunning:            false,
   273  		},
   274  		{
   275  			msg: "choose before start",
   276  			peerListActions: []PeerListAction{
   277  				ChooseAction{
   278  					ExpectedErr:         newNotRunningError("context finished while waiting for instance to start: context deadline exceeded"),
   279  					InputContextTimeout: 10 * time.Millisecond,
   280  				},
   281  				ChooseAction{
   282  					ExpectedErr:         newNotRunningError("context finished while waiting for instance to start: context deadline exceeded"),
   283  					InputContextTimeout: 10 * time.Millisecond,
   284  				},
   285  			},
   286  			expectedRunning: false,
   287  		},
   288  		{
   289  			msg:                      "update before start",
   290  			retainedAvailablePeerIDs: []string{"1"},
   291  			expectedAvailablePeers:   []string{"1"},
   292  			peerListActions: []PeerListAction{
   293  				UpdateAction{AddedPeerIDs: []string{"1"}},
   294  				StartAction{},
   295  			},
   296  			expectedRunning: true,
   297  		},
   298  		{
   299  			msg: "start choose no peers",
   300  			peerListActions: []PeerListAction{
   301  				StartAction{},
   302  				ChooseAction{
   303  					InputContextTimeout: 20 * time.Millisecond,
   304  					ExpectedErrMsg:      "peer list has no peers",
   305  				},
   306  			},
   307  			expectedRunning: true,
   308  		},
   309  		{
   310  			msg:                      "start then add",
   311  			retainedAvailablePeerIDs: []string{"1", "2"},
   312  			expectedAvailablePeers:   []string{"1", "2"},
   313  			peerListActions: []PeerListAction{
   314  				StartAction{},
   315  				UpdateAction{AddedPeerIDs: []string{"1"}},
   316  				UpdateAction{AddedPeerIDs: []string{"2"}},
   317  				ChooseAction{ExpectedPeer: "1"},
   318  				ChooseAction{ExpectedPeer: "2"},
   319  				ChooseAction{ExpectedPeer: "1"},
   320  			},
   321  			expectedRunning: true,
   322  		},
   323  		{
   324  			msg:                      "start remove",
   325  			retainedAvailablePeerIDs: []string{"1", "2"},
   326  			expectedAvailablePeers:   []string{"2"},
   327  			releasedPeerIDs:          []string{"1"},
   328  			peerListActions: []PeerListAction{
   329  				StartAction{},
   330  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   331  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   332  				ChooseAction{ExpectedPeer: "2"},
   333  			},
   334  			expectedRunning: true,
   335  		},
   336  		{
   337  			msg:                      "start add many and remove many",
   338  			retainedAvailablePeerIDs: []string{"1", "2", "3-r", "4-r", "5-a-r", "6-a-r", "7-a", "8-a"},
   339  			releasedPeerIDs:          []string{"3-r", "4-r", "5-a-r", "6-a-r"},
   340  			expectedAvailablePeers:   []string{"1", "2", "7-a", "8-a"},
   341  			peerListActions: []PeerListAction{
   342  				StartAction{},
   343  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3-r", "4-r"}},
   344  				UpdateAction{
   345  					AddedPeerIDs: []string{"5-a-r", "6-a-r", "7-a", "8-a"},
   346  				},
   347  				UpdateAction{
   348  					RemovedPeerIDs: []string{"5-a-r", "6-a-r", "3-r", "4-r"},
   349  				},
   350  				ChooseAction{ExpectedPeer: "1"},
   351  				ChooseAction{ExpectedPeer: "2"},
   352  				ChooseAction{ExpectedPeer: "7-a"},
   353  				ChooseAction{ExpectedPeer: "8-a"},
   354  				ChooseAction{ExpectedPeer: "1"},
   355  			},
   356  			expectedRunning: true,
   357  		},
   358  		{
   359  			msg:                      "start add many and remove many with shuffle",
   360  			retainedAvailablePeerIDs: []string{"1", "2", "3-r", "4-r", "5-a-r", "6-a-r", "7-a", "8-a"},
   361  			releasedPeerIDs:          []string{"3-r", "4-r", "5-a-r", "6-a-r"},
   362  			expectedAvailablePeers:   []string{"1", "2", "7-a", "8-a"},
   363  			peerListActions: []PeerListAction{
   364  				StartAction{},
   365  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3-r", "4-r"}},
   366  				UpdateAction{
   367  					AddedPeerIDs: []string{"5-a-r", "6-a-r", "7-a", "8-a"},
   368  				},
   369  				UpdateAction{
   370  					RemovedPeerIDs: []string{"5-a-r", "6-a-r", "3-r", "4-r"},
   371  				},
   372  				ChooseAction{ExpectedPeer: "2"},
   373  				ChooseAction{ExpectedPeer: "1"},
   374  				ChooseAction{ExpectedPeer: "8-a"},
   375  				ChooseAction{ExpectedPeer: "7-a"},
   376  				ChooseAction{ExpectedPeer: "2"},
   377  			},
   378  			expectedRunning: true,
   379  			shuffle:         true,
   380  		},
   381  		{
   382  			msg:                      "add retain error",
   383  			retainedAvailablePeerIDs: []string{"1", "2"},
   384  			expectedAvailablePeers:   []string{"1", "2"},
   385  			errRetainedPeerIDs:       []string{"3"},
   386  			retainErr:                peer.ErrInvalidPeerType{},
   387  			peerListActions: []PeerListAction{
   388  				StartAction{},
   389  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   390  				UpdateAction{
   391  					AddedPeerIDs: []string{"3"},
   392  					ExpectedErr:  peer.ErrInvalidPeerType{},
   393  				},
   394  				ChooseAction{ExpectedPeer: "1"},
   395  				ChooseAction{ExpectedPeer: "2"},
   396  				ChooseAction{ExpectedPeer: "1"},
   397  			},
   398  			expectedRunning: true,
   399  		},
   400  		{
   401  			msg:                      "add duplicate peer",
   402  			retainedAvailablePeerIDs: []string{"1", "2"},
   403  			expectedAvailablePeers:   []string{"1", "2"},
   404  			peerListActions: []PeerListAction{
   405  				StartAction{},
   406  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   407  				UpdateAction{
   408  					AddedPeerIDs: []string{"2"},
   409  					ExpectedErr:  peer.ErrPeerAddAlreadyInList("2"),
   410  				},
   411  				ChooseAction{ExpectedPeer: "1"},
   412  				ChooseAction{ExpectedPeer: "2"},
   413  				ChooseAction{ExpectedPeer: "1"},
   414  			},
   415  			expectedRunning: true,
   416  		},
   417  		{
   418  			msg:                      "remove peer not in list",
   419  			retainedAvailablePeerIDs: []string{"1", "2"},
   420  			expectedAvailablePeers:   []string{"1", "2"},
   421  			peerListActions: []PeerListAction{
   422  				StartAction{},
   423  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   424  				UpdateAction{
   425  					RemovedPeerIDs: []string{"3"},
   426  					ExpectedErr:    peer.ErrPeerRemoveNotInList("3"),
   427  				},
   428  				ChooseAction{ExpectedPeer: "1"},
   429  				ChooseAction{ExpectedPeer: "2"},
   430  				ChooseAction{ExpectedPeer: "1"},
   431  			},
   432  			expectedRunning: true,
   433  		},
   434  		{
   435  			msg:                      "remove release error",
   436  			retainedAvailablePeerIDs: []string{"1", "2"},
   437  			errReleasedPeerIDs:       []string{"2"},
   438  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   439  			expectedAvailablePeers:   []string{"1"},
   440  			peerListActions: []PeerListAction{
   441  				StartAction{},
   442  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   443  				UpdateAction{
   444  					RemovedPeerIDs: []string{"2"},
   445  					ExpectedErr:    peer.ErrTransportHasNoReferenceToPeer{},
   446  				},
   447  				ChooseAction{ExpectedPeer: "1"},
   448  				ChooseAction{ExpectedPeer: "1"},
   449  			},
   450  			expectedRunning: true,
   451  		},
   452  		// Flaky in CI
   453  		// {
   454  		// 	msg: "block until add",
   455  		// 	retainedAvailablePeerIDs: []string{"1"},
   456  		// 	expectedAvailablePeers:   []string{"1"},
   457  		// 	peerListActions: []PeerListAction{
   458  		// 		StartAction{},
   459  		// 		ConcurrentAction{
   460  		// 			Actions: []PeerListAction{
   461  		// 				ChooseAction{
   462  		// 					InputContextTimeout: 200 * time.Millisecond,
   463  		// 					ExpectedPeer:        "1",
   464  		// 				},
   465  		// 				UpdateAction{AddedPeerIDs: []string{"1"}},
   466  		// 			},
   467  		// 			Wait: 20 * time.Millisecond,
   468  		// 		},
   469  		// 		ChooseAction{ExpectedPeer: "1"},
   470  		// 	},
   471  		// 	expectedRunning: true,
   472  		// },
   473  		// {
   474  		// 	msg: "multiple blocking until add",
   475  		// 	retainedAvailablePeerIDs: []string{"1"},
   476  		// 	expectedAvailablePeers:   []string{"1"},
   477  		// 	peerListActions: []PeerListAction{
   478  		// 		StartAction{},
   479  		// 		ConcurrentAction{
   480  		// 			Actions: []PeerListAction{
   481  		// 				ChooseAction{
   482  		// 					InputContextTimeout: 200 * time.Millisecond,
   483  		// 					ExpectedPeer:        "1",
   484  		// 				},
   485  		// 				ChooseAction{
   486  		// 					InputContextTimeout: 200 * time.Millisecond,
   487  		// 					ExpectedPeer:        "1",
   488  		// 				},
   489  		// 				ChooseAction{
   490  		// 					InputContextTimeout: 200 * time.Millisecond,
   491  		// 					ExpectedPeer:        "1",
   492  		// 				},
   493  		// 				UpdateAction{AddedPeerIDs: []string{"1"}},
   494  		// 			},
   495  		// 			Wait: 10 * time.Millisecond,
   496  		// 		},
   497  		// 		ChooseAction{ExpectedPeer: "1"},
   498  		// 	},
   499  		// 	expectedRunning: true,
   500  		// },
   501  		{
   502  			msg:                      "block but added too late",
   503  			retainedAvailablePeerIDs: []string{"1"},
   504  			expectedAvailablePeers:   []string{"1"},
   505  			peerListActions: []PeerListAction{
   506  				StartAction{},
   507  				ConcurrentAction{
   508  					Actions: []PeerListAction{
   509  						ChooseAction{
   510  							InputContextTimeout: 10 * time.Millisecond,
   511  							ExpectedErrMsg:      "peer list has no peers",
   512  						},
   513  						UpdateAction{AddedPeerIDs: []string{"1"}},
   514  					},
   515  					Wait: 20 * time.Millisecond,
   516  				},
   517  				ChooseAction{ExpectedPeer: "1"},
   518  			},
   519  			expectedRunning: true,
   520  		},
   521  		// Flaky in CI
   522  		// {
   523  		// 	msg: "block until new peer after removal of only peer",
   524  		// 	retainedAvailablePeerIDs: []string{"1", "2"},
   525  		// 	releasedPeerIDs:          []string{"1"},
   526  		// 	expectedAvailablePeers:   []string{"2"},
   527  		// 	peerListActions: []PeerListAction{
   528  		// 		StartAction{},
   529  		// 		UpdateAction{AddedPeerIDs: []string{"1"}},
   530  		// 		UpdateAction{RemovedPeerIDs: []string{"1"}},
   531  		// 		ConcurrentAction{
   532  		// 			Actions: []PeerListAction{
   533  		// 				ChooseAction{
   534  		// 					InputContextTimeout: 200 * time.Millisecond,
   535  		// 					ExpectedPeer:        "2",
   536  		// 				},
   537  		// 				UpdateAction{AddedPeerIDs: []string{"2"}},
   538  		// 			},
   539  		// 			Wait: 20 * time.Millisecond,
   540  		// 		},
   541  		// 		ChooseAction{ExpectedPeer: "2"},
   542  		// 	},
   543  		// 	expectedRunning: true,
   544  		// },
   545  		{
   546  			msg:                        "add unavailable peer",
   547  			retainedAvailablePeerIDs:   []string{"1"},
   548  			retainedUnavailablePeerIDs: []string{"2"},
   549  			expectedAvailablePeers:     []string{"1"},
   550  			expectedUnavailablePeers:   []string{"2"},
   551  			peerListActions: []PeerListAction{
   552  				StartAction{},
   553  				UpdateAction{AddedPeerIDs: []string{"1"}},
   554  				UpdateAction{AddedPeerIDs: []string{"2"}},
   555  				ChooseAction{ExpectedPeer: "1"},
   556  				ChooseAction{ExpectedPeer: "1"},
   557  			},
   558  			expectedRunning: true,
   559  		},
   560  		{
   561  			msg:                        "remove unavailable peer",
   562  			retainedUnavailablePeerIDs: []string{"1"},
   563  			releasedPeerIDs:            []string{"1"},
   564  			peerListActions: []PeerListAction{
   565  				StartAction{},
   566  				UpdateAction{AddedPeerIDs: []string{"1"}},
   567  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   568  				ChooseAction{
   569  					InputContextTimeout: 10 * time.Millisecond,
   570  					ExpectedErrMsg:      "has no peers",
   571  				},
   572  			},
   573  			expectedRunning: true,
   574  		},
   575  		{
   576  			msg:                        "notify peer is now available",
   577  			retainedUnavailablePeerIDs: []string{"1"},
   578  			expectedAvailablePeers:     []string{"1"},
   579  			peerListActions: []PeerListAction{
   580  				StartAction{},
   581  				UpdateAction{AddedPeerIDs: []string{"1"}},
   582  				ChooseAction{
   583  					InputContextTimeout: 10 * time.Millisecond,
   584  					ExpectedErrMsg:      "has 1 peer but it is not responsive",
   585  				},
   586  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   587  				ChooseAction{ExpectedPeer: "1"},
   588  			},
   589  			expectedRunning: true,
   590  		},
   591  		{
   592  			msg:                      "notify peer is still available",
   593  			retainedAvailablePeerIDs: []string{"1"},
   594  			expectedAvailablePeers:   []string{"1"},
   595  			peerListActions: []PeerListAction{
   596  				StartAction{},
   597  				UpdateAction{AddedPeerIDs: []string{"1"}},
   598  				ChooseAction{ExpectedPeer: "1"},
   599  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   600  				ChooseAction{ExpectedPeer: "1"},
   601  			},
   602  			expectedRunning: true,
   603  		},
   604  		{
   605  			msg:                      "notify peer is now unavailable",
   606  			retainedAvailablePeerIDs: []string{"1"},
   607  			expectedUnavailablePeers: []string{"1"},
   608  			peerListActions: []PeerListAction{
   609  				StartAction{},
   610  				UpdateAction{AddedPeerIDs: []string{"1"}},
   611  				ChooseAction{ExpectedPeer: "1"},
   612  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Unavailable},
   613  				ChooseAction{
   614  					InputContextTimeout: 10 * time.Millisecond,
   615  					ExpectedErrMsg:      "has 1 peer but it is not responsive",
   616  				},
   617  			},
   618  			expectedRunning: true,
   619  		},
   620  		{
   621  			msg:                        "notify peer is still unavailable",
   622  			retainedUnavailablePeerIDs: []string{"1"},
   623  			expectedUnavailablePeers:   []string{"1"},
   624  			peerListActions: []PeerListAction{
   625  				StartAction{},
   626  				UpdateAction{AddedPeerIDs: []string{"1"}},
   627  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Unavailable},
   628  				ChooseAction{
   629  					InputContextTimeout: 10 * time.Millisecond,
   630  					ExpectedErrMsg:      "has 1 peer but it is not responsive",
   631  				},
   632  			},
   633  			expectedRunning: true,
   634  		},
   635  		{
   636  			msg:                      "notify invalid peer",
   637  			retainedAvailablePeerIDs: []string{"1"},
   638  			releasedPeerIDs:          []string{"1"},
   639  			peerListActions: []PeerListAction{
   640  				StartAction{},
   641  				UpdateAction{AddedPeerIDs: []string{"1"}},
   642  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   643  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   644  			},
   645  			expectedRunning: true,
   646  		},
   647  		{
   648  			// v: Available, u: Unavailable, a: Added, r: Removed
   649  			msg:                        "notify peer stress test",
   650  			retainedAvailablePeerIDs:   []string{"1v", "2va", "3vau", "4var", "5vaur"},
   651  			retainedUnavailablePeerIDs: []string{"6u", "7ua", "8uav", "9uar", "10uavr"},
   652  			releasedPeerIDs:            []string{"4var", "5vaur", "9uar", "10uavr"},
   653  			expectedAvailablePeers:     []string{"1v", "2va", "8uav"},
   654  			expectedUnavailablePeers:   []string{"3vau", "6u", "7ua"},
   655  			peerListActions: []PeerListAction{
   656  				StartAction{},
   657  				UpdateAction{AddedPeerIDs: []string{"1v", "6u"}},
   658  
   659  				// Added Peers
   660  				UpdateAction{
   661  					AddedPeerIDs: []string{"2va", "3vau", "4var", "5vaur", "7ua", "8uav", "9uar", "10uavr"},
   662  				},
   663  
   664  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "3vau", "4var", "5vaur"}},
   665  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "3vau", "4var", "5vaur"}},
   666  
   667  				// Change Status to Unavailable
   668  				NotifyStatusChangeAction{PeerID: "3vau", NewConnectionStatus: peer.Unavailable},
   669  				NotifyStatusChangeAction{PeerID: "5vaur", NewConnectionStatus: peer.Unavailable},
   670  
   671  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "4var"}},
   672  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "4var"}},
   673  
   674  				// Change Status to Available
   675  				NotifyStatusChangeAction{PeerID: "8uav", NewConnectionStatus: peer.Available},
   676  				NotifyStatusChangeAction{PeerID: "10uavr", NewConnectionStatus: peer.Available},
   677  
   678  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "4var", "8uav", "10uavr"}},
   679  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "4var", "8uav", "10uavr"}},
   680  
   681  				// Remove Peers
   682  				UpdateAction{
   683  					RemovedPeerIDs: []string{"4var", "5vaur", "9uar", "10uavr"},
   684  				},
   685  
   686  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "8uav"}},
   687  				ChooseMultiAction{ExpectedPeers: []string{"1v", "2va", "8uav"}},
   688  			},
   689  			expectedRunning: true,
   690  		},
   691  		// Flaky in CI
   692  		// {
   693  		// 	msg: "block until notify available",
   694  		// 	retainedUnavailablePeerIDs: []string{"1"},
   695  		// 	expectedAvailablePeers:     []string{"1"},
   696  		// 	peerListActions: []PeerListAction{
   697  		// 		StartAction{},
   698  		// 		UpdateAction{AddedPeerIDs: []string{"1"}},
   699  		// 		ConcurrentAction{
   700  		// 			Actions: []PeerListAction{
   701  		// 				ChooseAction{
   702  		// 					InputContextTimeout: 200 * time.Millisecond,
   703  		// 					ExpectedPeer:        "1",
   704  		// 				},
   705  		// 				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   706  		// 			},
   707  		// 			Wait: 20 * time.Millisecond,
   708  		// 		},
   709  		// 		ChooseAction{ExpectedPeer: "1"},
   710  		// 	},
   711  		// 	expectedRunning: true,
   712  		// },
   713  		{
   714  			msg: "update no start",
   715  			peerListActions: []PeerListAction{
   716  				UpdateAction{AddedPeerIDs: []string{"1"}},
   717  				UpdateAction{AddedPeerIDs: []string{"2"}},
   718  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   719  			},
   720  			expectedUninitializedPeers: []string{"2"},
   721  			expectedRunning:            false,
   722  		},
   723  		{
   724  			msg: "update after stop",
   725  			peerListActions: []PeerListAction{
   726  				StartAction{},
   727  				StopAction{},
   728  				UpdateAction{AddedPeerIDs: []string{"1"}},
   729  				UpdateAction{AddedPeerIDs: []string{"2"}},
   730  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   731  			},
   732  			expectedUninitializedPeers: []string{"2"},
   733  			expectedRunning:            false,
   734  		},
   735  		{
   736  			msg: "remove peer not in list before start",
   737  			peerListActions: []PeerListAction{
   738  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   739  				UpdateAction{
   740  					RemovedPeerIDs: []string{"3"},
   741  					ExpectedErr:    peer.ErrPeerRemoveNotInList("3"),
   742  				},
   743  			},
   744  			expectedUninitializedPeers: []string{"1", "2"},
   745  			expectedRunning:            false,
   746  		},
   747  		{
   748  			msg:                      "update before start",
   749  			retainedAvailablePeerIDs: []string{"1", "2"},
   750  			expectedAvailablePeers:   []string{"1", "2"},
   751  			peerListActions: []PeerListAction{
   752  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   753  				StartAction{},
   754  			},
   755  			expectedRunning: true,
   756  		},
   757  		{
   758  			msg:                      "update before start, and stop",
   759  			retainedAvailablePeerIDs: []string{"1", "2"},
   760  			releasedPeerIDs:          []string{"1", "2"},
   761  			peerListActions: []PeerListAction{
   762  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   763  				StartAction{},
   764  				StopAction{},
   765  			},
   766  			expectedUninitializedPeers: []string{"1", "2"},
   767  			expectedRunning:            false,
   768  		},
   769  		{
   770  			msg:                      "update before start, and update after stop",
   771  			retainedAvailablePeerIDs: []string{"1", "2"},
   772  			releasedPeerIDs:          []string{"1", "2"},
   773  			peerListActions: []PeerListAction{
   774  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   775  				StartAction{},
   776  				StopAction{},
   777  				UpdateAction{AddedPeerIDs: []string{"3"}, RemovedPeerIDs: []string{"1"}},
   778  			},
   779  			expectedUninitializedPeers: []string{"2", "3"},
   780  			expectedRunning:            false,
   781  		},
   782  		{
   783  			msg:                      "concurrent update and start",
   784  			retainedAvailablePeerIDs: []string{"1", "2"},
   785  			expectedAvailablePeers:   []string{"1", "2"},
   786  			peerListActions: []PeerListAction{
   787  				UpdateAction{AddedPeerIDs: []string{"1"}},
   788  				ConcurrentAction{
   789  					Actions: []PeerListAction{
   790  						StartAction{},
   791  						UpdateAction{AddedPeerIDs: []string{"2"}},
   792  						StartAction{},
   793  					},
   794  				},
   795  			},
   796  			expectedRunning: true,
   797  		},
   798  		{
   799  			msg:                      "concurrent update and stop",
   800  			retainedAvailablePeerIDs: []string{"1", "2"},
   801  			releasedPeerIDs:          []string{"1", "2"},
   802  			peerListActions: []PeerListAction{
   803  				StartAction{},
   804  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   805  				ConcurrentAction{
   806  					Actions: []PeerListAction{
   807  						StopAction{},
   808  						UpdateAction{RemovedPeerIDs: []string{"2"}},
   809  						StopAction{},
   810  					},
   811  				},
   812  			},
   813  			expectedUninitializedPeers: []string{"1"},
   814  			expectedRunning:            false,
   815  		},
   816  		{
   817  			msg: "notify before start",
   818  			peerListActions: []PeerListAction{
   819  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   820  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available, Unretained: true},
   821  				NotifyStatusChangeAction{PeerID: "2", NewConnectionStatus: peer.Unavailable, Unretained: true},
   822  			},
   823  			expectedUninitializedPeers: []string{"1", "2"},
   824  			expectedRunning:            false,
   825  		},
   826  		{
   827  			msg:                      "notify after stop",
   828  			retainedAvailablePeerIDs: []string{"1", "2"},
   829  			releasedPeerIDs:          []string{"1", "2"},
   830  			peerListActions: []PeerListAction{
   831  				StartAction{},
   832  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   833  				StopAction{},
   834  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   835  				NotifyStatusChangeAction{PeerID: "2", NewConnectionStatus: peer.Unavailable},
   836  			},
   837  			expectedUninitializedPeers: []string{"1", "2"},
   838  			expectedRunning:            false,
   839  		},
   840  		{
   841  			msg:                        "start with available and unavailable",
   842  			retainedAvailablePeerIDs:   []string{"1", "2"},
   843  			retainedUnavailablePeerIDs: []string{"3", "4"},
   844  			expectedAvailablePeers:     []string{"1", "2"},
   845  			expectedUnavailablePeers:   []string{"3", "4"},
   846  			peerListActions: []PeerListAction{
   847  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3", "4"}},
   848  				StartAction{},
   849  			},
   850  			expectedRunning: true,
   851  		},
   852  		{
   853  			msg:                        "stop with available and unavailable",
   854  			retainedAvailablePeerIDs:   []string{"1", "2"},
   855  			retainedUnavailablePeerIDs: []string{"3", "4"},
   856  			releasedPeerIDs:            []string{"1", "2", "3", "4"},
   857  			peerListActions: []PeerListAction{
   858  				StartAction{},
   859  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3", "4"}},
   860  				StopAction{},
   861  			},
   862  			expectedUninitializedPeers: []string{"1", "2", "3", "4"},
   863  			expectedRunning:            false,
   864  		},
   865  	}
   866  
   867  	for _, tt := range tests {
   868  		t.Run(tt.msg, func(t *testing.T) {
   869  			mockCtrl := gomock.NewController(t)
   870  			defer mockCtrl.Finish()
   871  
   872  			transport := NewMockTransport(mockCtrl)
   873  
   874  			// Healthy Transport Retain/Release
   875  			peerMap := ExpectPeerRetains(
   876  				transport,
   877  				tt.retainedAvailablePeerIDs,
   878  				tt.retainedUnavailablePeerIDs,
   879  			)
   880  			ExpectPeerReleases(transport, tt.releasedPeerIDs, nil)
   881  
   882  			// Unhealthy Transport Retain/Release
   883  			ExpectPeerRetainsWithError(transport, tt.errRetainedPeerIDs, tt.retainErr)
   884  			ExpectPeerReleases(transport, tt.errReleasedPeerIDs, tt.releaseErr)
   885  
   886  			opts := []ListOption{seed(0), Logger(zaptest.NewLogger(t))}
   887  			if !tt.shuffle {
   888  				opts = append(opts, noShuffle)
   889  			}
   890  			pl := New(transport, opts...)
   891  
   892  			deps := ListActionDeps{
   893  				Peers: peerMap,
   894  			}
   895  			ApplyPeerListActions(t, pl, tt.peerListActions, deps)
   896  
   897  			assert.Equal(t, len(tt.expectedAvailablePeers), pl.list.NumAvailable(), "invalid available peerlist size")
   898  			for _, expectedRingPeer := range tt.expectedAvailablePeers {
   899  				ok := pl.list.Available(hostport.PeerIdentifier(expectedRingPeer))
   900  				assert.True(t, ok, fmt.Sprintf("expected peer: %s was not in available peerlist", expectedRingPeer))
   901  			}
   902  
   903  			assert.Equal(t, len(tt.expectedUnavailablePeers), pl.list.NumUnavailable(), "invalid unavailable peerlist size")
   904  			for _, expectedUnavailablePeer := range tt.expectedUnavailablePeers {
   905  				ok := !pl.list.Available(hostport.PeerIdentifier(expectedUnavailablePeer))
   906  				assert.True(t, ok, fmt.Sprintf("expected peer: %s was not in unavailable peerlist", expectedUnavailablePeer))
   907  			}
   908  
   909  			assert.Equal(t, len(tt.expectedUninitializedPeers), pl.list.NumUninitialized(), "invalid uninitialized peerlist size")
   910  			for _, expectedUninitializedPeer := range tt.expectedUninitializedPeers {
   911  				ok := pl.list.Uninitialized(hostport.PeerIdentifier(expectedUninitializedPeer))
   912  				assert.True(t, ok, fmt.Sprintf("expected peer: %s was not in uninitialized peerlist", expectedUninitializedPeer))
   913  			}
   914  
   915  			assert.Equal(t, tt.expectedRunning, pl.IsRunning(), "List was not in the expected state")
   916  		})
   917  	}
   918  }
   919  
   920  func TestIntrospect(t *testing.T) {
   921  	trans := yarpctest.NewFakeTransport(yarpctest.InitialConnectionStatus(peer.Unavailable))
   922  	pl := New(trans, noShuffle)
   923  	assert.NoError(t, pl.Update(peer.ListUpdates{
   924  		Additions: []peer.Identifier{
   925  			abstractpeer.Identify("foo"),
   926  			abstractpeer.Identify("bar"),
   927  			abstractpeer.Identify("baz"),
   928  		},
   929  	}))
   930  	require.NoError(t, pl.Start())
   931  
   932  	trans.SimulateConnect(abstractpeer.Identify("bar"))
   933  	trans.SimulateConnect(abstractpeer.Identify("baz"))
   934  
   935  	// Simulate some load.
   936  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   937  	defer cancel()
   938  	{
   939  		p, _, err := pl.Choose(ctx, &transport.Request{})
   940  		require.NoError(t, err)
   941  		assert.Equal(t, p.Identifier(), "bar")
   942  	}
   943  	{
   944  		p, _, err := pl.Choose(ctx, &transport.Request{})
   945  		require.NoError(t, err)
   946  		assert.Equal(t, p.Identifier(), "baz")
   947  	}
   948  	{
   949  		p, _, err := pl.Choose(ctx, &transport.Request{})
   950  		require.NoError(t, err)
   951  		assert.Equal(t, p.Identifier(), "bar")
   952  	}
   953  
   954  	chooserStatus := pl.Introspect()
   955  	assert.Equal(t, "round-robin", chooserStatus.Name)
   956  	assert.Equal(t, "Running (2/3 available)", chooserStatus.State)
   957  
   958  	peerIdentifierToPeerStatus := make(map[string]introspection.PeerStatus, len(chooserStatus.Peers))
   959  	for _, peerStatus := range chooserStatus.Peers {
   960  		peerIdentifierToPeerStatus[peerStatus.Identifier] = peerStatus
   961  	}
   962  	checkPeerStatus(t, peerIdentifierToPeerStatus, "foo", "Unavailable, 0 pending request(s)")
   963  	checkPeerStatus(t, peerIdentifierToPeerStatus, "bar", "Available, 2 pending request(s)")
   964  	checkPeerStatus(t, peerIdentifierToPeerStatus, "baz", "Available, 1 pending request(s)")
   965  }
   966  
   967  func checkPeerStatus(
   968  	t *testing.T,
   969  	peerIdentifierToPeerStatus map[string]introspection.PeerStatus,
   970  	identifier string,
   971  	expectedState string,
   972  ) {
   973  	peerStatus, ok := peerIdentifierToPeerStatus[identifier]
   974  	assert.True(t, ok)
   975  	assert.Equal(t, expectedState, peerStatus.State)
   976  }
   977  
   978  var noShuffle ListOption = func(c *listConfig) {
   979  	c.shuffle = false
   980  }
   981  
   982  func seed(seed int64) ListOption {
   983  	return func(c *listConfig) {
   984  		c.seed = seed
   985  	}
   986  }
   987  
   988  func TestFailFastConfig(t *testing.T) {
   989  	conn, err := net.Listen("tcp", "127.0.0.1:0")
   990  	require.NoError(t, err)
   991  	require.NoError(t, conn.Close())
   992  
   993  	serviceName := "test"
   994  	config := whitespace.Expand(fmt.Sprintf(`
   995  		outbounds:
   996  			nowhere:
   997  				http:
   998  					round-robin:
   999  						peers:
  1000  							- %q
  1001  						capacity: 10
  1002  						failFast: true
  1003  	`, conn.Addr()))
  1004  	cfgr := yarpcconfig.New()
  1005  	cfgr.MustRegisterTransport(http.TransportSpec())
  1006  	cfgr.MustRegisterPeerList(Spec())
  1007  	cfg, err := cfgr.LoadConfigFromYAML(serviceName, strings.NewReader(config))
  1008  	require.NoError(t, err)
  1009  
  1010  	d := yarpc.NewDispatcher(cfg)
  1011  	d.Start()
  1012  	defer d.Stop()
  1013  
  1014  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
  1015  	defer cancel()
  1016  
  1017  	client := d.MustOutboundConfig("nowhere")
  1018  	_, err = client.Outbounds.Unary.Call(ctx, &transport.Request{
  1019  		Service:   "service",
  1020  		Caller:    "caller",
  1021  		Encoding:  transport.Encoding("blank"),
  1022  		Procedure: "bogus",
  1023  		Body:      strings.NewReader("nada"),
  1024  	})
  1025  	require.Error(t, err)
  1026  	assert.Contains(t, err.Error(), "has 1 peer but it is not responsive")
  1027  }