go.uber.org/yarpc@v1.72.1/peer/pendingheap/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 pendingheap
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"net"
    27  	"sort"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/golang/mock/gomock"
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  	"go.uber.org/multierr"
    36  	"go.uber.org/yarpc"
    37  	"go.uber.org/yarpc/api/peer"
    38  	. "go.uber.org/yarpc/api/peer/peertest"
    39  	"go.uber.org/yarpc/api/transport"
    40  	"go.uber.org/yarpc/internal/testtime"
    41  	"go.uber.org/yarpc/internal/whitespace"
    42  	"go.uber.org/yarpc/transport/http"
    43  	"go.uber.org/yarpc/yarpcconfig"
    44  	"go.uber.org/yarpc/yarpcerrors"
    45  	"go.uber.org/zap/zaptest"
    46  )
    47  
    48  func newNotRunningError(err string) error {
    49  	return yarpcerrors.FailedPreconditionErrorf(`"fewest-pending-requests" peer list is not running: %s`, err)
    50  }
    51  
    52  // InsertionOrder is a test option that yields control over random insertion
    53  // ordering. Each number corresponds to the position to swap the newly inserted
    54  // peer's 'last' value.
    55  //
    56  // The function MUST return a number in [0, numPeers)
    57  func InsertionOrder(f func(numPeers int) int) ListOption {
    58  	return func(c *listConfig) {
    59  		c.nextRand = f
    60  	}
    61  }
    62  
    63  // DisableRandomInsertion disables random insertions.
    64  func DisableRandomInsertion() ListOption {
    65  	// avoid swaps by always returning the last index
    66  	return InsertionOrder(func(numPeers int) int { return numPeers - 1 })
    67  }
    68  
    69  func nextRandFromSlice(indicies []int) func(int) int {
    70  	i := -1
    71  	return func(_ int) int {
    72  		i++
    73  		return indicies[i]
    74  	}
    75  }
    76  
    77  func TestPeerHeapList(t *testing.T) {
    78  	type testStruct struct {
    79  		msg string
    80  
    81  		// nextRand is used with the InsertionOrder(...) option. If nil, this defaults
    82  		// to DisableRandomInsertion()
    83  		nextRand func(int) int
    84  
    85  		// PeerIDs that will be returned from the transport's OnRetain with "Available" status
    86  		retainedAvailablePeerIDs []string
    87  
    88  		// PeerIDs that will be returned from the transport's OnRetain with "Unavailable" status
    89  		retainedUnavailablePeerIDs []string
    90  
    91  		// PeerIDs that will be released from the transport
    92  		releasedPeerIDs []string
    93  
    94  		// PeerIDs that will return "retainErr" from the transport's OnRetain function
    95  		errRetainedPeerIDs []string
    96  		retainErr          error
    97  
    98  		// PeerIDs that will return "releaseErr" from the transport's OnRelease function
    99  		errReleasedPeerIDs []string
   100  		releaseErr         error
   101  
   102  		// A list of actions that will be applied on the PeerList
   103  		peerListActions []PeerListAction
   104  
   105  		// PeerIDs expected to be in the PeerList's "Available" list after the actions have been applied
   106  		expectedAvailablePeers []string
   107  
   108  		// PeerIDs expected to be in the PeerList's "Unavailable" list after the actions have been applied
   109  		expectedUnavailablePeers []string
   110  
   111  		// Boolean indicating whether the PeerList is "running" after the actions have been applied
   112  		expectedRunning bool
   113  	}
   114  	tests := []testStruct{
   115  		{
   116  			msg:                      "setup",
   117  			retainedAvailablePeerIDs: []string{"1"},
   118  			expectedAvailablePeers:   []string{"1"},
   119  			peerListActions: []PeerListAction{
   120  				StartAction{},
   121  				UpdateAction{AddedPeerIDs: []string{"1"}},
   122  			},
   123  			expectedRunning: true,
   124  		},
   125  		{
   126  			msg:                        "setup with disconnected",
   127  			retainedAvailablePeerIDs:   []string{"1"},
   128  			retainedUnavailablePeerIDs: []string{"2"},
   129  			peerListActions: []PeerListAction{
   130  				StartAction{},
   131  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   132  			},
   133  			expectedAvailablePeers:   []string{"1"},
   134  			expectedUnavailablePeers: []string{"2"},
   135  			expectedRunning:          true,
   136  		},
   137  		{
   138  			msg:                      "start",
   139  			retainedAvailablePeerIDs: []string{"1"},
   140  			expectedAvailablePeers:   []string{"1"},
   141  			peerListActions: []PeerListAction{
   142  				StartAction{},
   143  				UpdateAction{AddedPeerIDs: []string{"1"}},
   144  				ChooseAction{
   145  					ExpectedPeer: "1",
   146  				},
   147  			},
   148  			expectedRunning: true,
   149  		},
   150  		{
   151  			msg:                        "start stop",
   152  			retainedAvailablePeerIDs:   []string{"1", "2", "3", "4", "5", "6"},
   153  			retainedUnavailablePeerIDs: []string{"7", "8", "9"},
   154  			releasedPeerIDs:            []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"},
   155  			peerListActions: []PeerListAction{
   156  				StartAction{},
   157  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}},
   158  				StopAction{},
   159  				ChooseAction{
   160  					ExpectedErrMsg:      "is not running",
   161  					InputContextTimeout: 10 * time.Millisecond,
   162  				},
   163  			},
   164  			expectedRunning: false,
   165  		},
   166  		{
   167  			msg:                      "update, start, and choose",
   168  			retainedAvailablePeerIDs: []string{"1"},
   169  			expectedAvailablePeers:   []string{"1"},
   170  			peerListActions: []PeerListAction{
   171  				UpdateAction{AddedPeerIDs: []string{"1"}},
   172  				StartAction{},
   173  				ChooseAction{ExpectedPeer: "1"},
   174  			},
   175  			expectedRunning: true,
   176  		},
   177  		{
   178  			msg:                      "start many and choose",
   179  			retainedAvailablePeerIDs: []string{"1", "2", "3", "4", "5", "6"},
   180  			expectedAvailablePeers:   []string{"1", "2", "3", "4", "5", "6"},
   181  			peerListActions: []PeerListAction{
   182  				StartAction{},
   183  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3", "4", "5", "6"}},
   184  				ChooseAction{ExpectedPeer: "1"},
   185  				ChooseAction{ExpectedPeer: "2"},
   186  				ChooseAction{ExpectedPeer: "3"},
   187  				ChooseAction{ExpectedPeer: "4"},
   188  				ChooseAction{ExpectedPeer: "5"},
   189  				ChooseAction{ExpectedPeer: "6"},
   190  				ChooseAction{ExpectedPeer: "1"},
   191  			},
   192  			expectedRunning: true,
   193  		},
   194  		{
   195  			msg:                      "assure start is idempotent",
   196  			retainedAvailablePeerIDs: []string{"1"},
   197  			expectedAvailablePeers:   []string{"1"},
   198  			peerListActions: []PeerListAction{
   199  				StartAction{},
   200  				UpdateAction{AddedPeerIDs: []string{"1"}},
   201  				StartAction{},
   202  				StartAction{},
   203  				ChooseAction{
   204  					ExpectedPeer: "1",
   205  				},
   206  			},
   207  			expectedRunning: true,
   208  		},
   209  		{
   210  			msg:                      "stop no start",
   211  			retainedAvailablePeerIDs: []string{},
   212  			releasedPeerIDs:          []string{},
   213  			peerListActions: []PeerListAction{
   214  				StopAction{},
   215  				UpdateAction{AddedPeerIDs: []string{"1"}},
   216  			},
   217  			expectedRunning: false,
   218  		},
   219  		{
   220  			msg:                "update retain error",
   221  			errRetainedPeerIDs: []string{"1"},
   222  			retainErr:          peer.ErrInvalidPeerType{},
   223  			peerListActions: []PeerListAction{
   224  				StartAction{},
   225  				UpdateAction{AddedPeerIDs: []string{"1"}, ExpectedErr: peer.ErrInvalidPeerType{}},
   226  			},
   227  			expectedRunning: true,
   228  		},
   229  		{
   230  			msg:                      "update retain multiple errors",
   231  			retainedAvailablePeerIDs: []string{"2"},
   232  			errRetainedPeerIDs:       []string{"1", "3"},
   233  			retainErr:                peer.ErrInvalidPeerType{},
   234  			peerListActions: []PeerListAction{
   235  				StartAction{},
   236  				UpdateAction{
   237  					AddedPeerIDs: []string{"1", "2", "3"},
   238  					ExpectedErr:  multierr.Combine(peer.ErrInvalidPeerType{}, peer.ErrInvalidPeerType{}),
   239  				},
   240  			},
   241  			expectedAvailablePeers: []string{"2"},
   242  			expectedRunning:        true,
   243  		},
   244  		{
   245  			msg:                      "start stop release error",
   246  			retainedAvailablePeerIDs: []string{"1"},
   247  			errReleasedPeerIDs:       []string{"1"},
   248  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   249  			peerListActions: []PeerListAction{
   250  				StartAction{},
   251  				UpdateAction{AddedPeerIDs: []string{"1"}},
   252  				StopAction{
   253  					ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   254  				},
   255  			},
   256  			expectedRunning: false,
   257  		},
   258  		{
   259  			msg:                      "assure stop is idempotent",
   260  			retainedAvailablePeerIDs: []string{"1"},
   261  			errReleasedPeerIDs:       []string{"1"},
   262  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   263  			peerListActions: []PeerListAction{
   264  				StartAction{},
   265  				UpdateAction{AddedPeerIDs: []string{"1"}},
   266  				ConcurrentAction{
   267  					Actions: []PeerListAction{
   268  						StopAction{
   269  							ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   270  						},
   271  						StopAction{
   272  							ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   273  						},
   274  						StopAction{
   275  							ExpectedErr: peer.ErrTransportHasNoReferenceToPeer{},
   276  						},
   277  					},
   278  				},
   279  			},
   280  			expectedRunning: false,
   281  		},
   282  		{
   283  			msg:                      "start stop release multiple errors",
   284  			retainedAvailablePeerIDs: []string{"1", "2", "3"},
   285  			releasedPeerIDs:          []string{"2"},
   286  			errReleasedPeerIDs:       []string{"1", "3"},
   287  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   288  			peerListActions: []PeerListAction{
   289  				StartAction{},
   290  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3"}},
   291  				StopAction{
   292  					ExpectedErr: multierr.Combine(
   293  						peer.ErrTransportHasNoReferenceToPeer{},
   294  						peer.ErrTransportHasNoReferenceToPeer{},
   295  					),
   296  				},
   297  			},
   298  			expectedRunning: false,
   299  		},
   300  		{
   301  			msg: "choose before start",
   302  			peerListActions: []PeerListAction{
   303  				ChooseAction{
   304  					ExpectedErr:         newNotRunningError("context finished while waiting for instance to start: context deadline exceeded"),
   305  					InputContextTimeout: 10 * time.Millisecond,
   306  				},
   307  				ChooseAction{
   308  					ExpectedErr:         newNotRunningError("context finished while waiting for instance to start: context deadline exceeded"),
   309  					InputContextTimeout: 10 * time.Millisecond,
   310  				},
   311  			},
   312  			expectedRunning: false,
   313  		},
   314  		{
   315  			msg:                      "update before start",
   316  			retainedAvailablePeerIDs: []string{"1"},
   317  			expectedAvailablePeers:   []string{"1"},
   318  			peerListActions: []PeerListAction{
   319  				ConcurrentAction{
   320  					Actions: []PeerListAction{
   321  						UpdateAction{AddedPeerIDs: []string{"1"}},
   322  						StartAction{},
   323  					},
   324  					Wait: 20 * time.Millisecond,
   325  				},
   326  			},
   327  			expectedRunning: true,
   328  		},
   329  		{
   330  			msg: "start choose no peers",
   331  			peerListActions: []PeerListAction{
   332  				StartAction{},
   333  				ChooseAction{
   334  					InputContextTimeout: 20 * time.Millisecond,
   335  					ExpectedErrMsg:      "peer list has no peers",
   336  				},
   337  			},
   338  			expectedRunning: true,
   339  		},
   340  		{
   341  			msg:                      "start then add",
   342  			retainedAvailablePeerIDs: []string{"1", "2"},
   343  			expectedAvailablePeers:   []string{"1", "2"},
   344  			peerListActions: []PeerListAction{
   345  				StartAction{},
   346  				UpdateAction{AddedPeerIDs: []string{"1"}},
   347  				UpdateAction{AddedPeerIDs: []string{"2"}},
   348  				ChooseAction{ExpectedPeer: "1"},
   349  				ChooseAction{ExpectedPeer: "2"},
   350  				ChooseAction{ExpectedPeer: "1"},
   351  			},
   352  			expectedRunning: true,
   353  		},
   354  		{
   355  			msg:                      "start remove",
   356  			retainedAvailablePeerIDs: []string{"1", "2"},
   357  			expectedAvailablePeers:   []string{"2"},
   358  			releasedPeerIDs:          []string{"1"},
   359  			peerListActions: []PeerListAction{
   360  				StartAction{},
   361  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   362  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   363  				ChooseAction{ExpectedPeer: "2"},
   364  			},
   365  			expectedRunning: true,
   366  		},
   367  		{
   368  			msg:                      "start add many and remove many",
   369  			retainedAvailablePeerIDs: []string{"1", "2", "3-r", "4-r", "5-a-r", "6-a-r", "7-a", "8-a"},
   370  			releasedPeerIDs:          []string{"3-r", "4-r", "5-a-r", "6-a-r"},
   371  			expectedAvailablePeers:   []string{"1", "2", "7-a", "8-a"},
   372  			peerListActions: []PeerListAction{
   373  				StartAction{},
   374  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3-r", "4-r"}},
   375  				UpdateAction{
   376  					AddedPeerIDs: []string{"5-a-r", "6-a-r", "7-a", "8-a"},
   377  				},
   378  				UpdateAction{
   379  					RemovedPeerIDs: []string{"5-a-r", "6-a-r", "3-r", "4-r"},
   380  				},
   381  				ChooseAction{ExpectedPeer: "1"},
   382  				ChooseAction{ExpectedPeer: "2"},
   383  				ChooseAction{ExpectedPeer: "7-a"},
   384  				ChooseAction{ExpectedPeer: "8-a"},
   385  				ChooseAction{ExpectedPeer: "1"},
   386  			},
   387  			expectedRunning: true,
   388  		},
   389  		{
   390  			msg:                      "add retain error",
   391  			retainedAvailablePeerIDs: []string{"1", "2"},
   392  			expectedAvailablePeers:   []string{"1", "2"},
   393  			errRetainedPeerIDs:       []string{"3"},
   394  			retainErr:                peer.ErrInvalidPeerType{},
   395  			peerListActions: []PeerListAction{
   396  				StartAction{},
   397  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   398  				UpdateAction{
   399  					AddedPeerIDs: []string{"3"},
   400  					ExpectedErr:  peer.ErrInvalidPeerType{},
   401  				},
   402  				ChooseAction{ExpectedPeer: "1"},
   403  				ChooseAction{ExpectedPeer: "2"},
   404  				ChooseAction{ExpectedPeer: "1"},
   405  			},
   406  			expectedRunning: true,
   407  		},
   408  		{
   409  			msg:                      "add duplicate peer",
   410  			retainedAvailablePeerIDs: []string{"1", "2"},
   411  			expectedAvailablePeers:   []string{"1", "2"},
   412  			peerListActions: []PeerListAction{
   413  				StartAction{},
   414  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   415  				UpdateAction{
   416  					AddedPeerIDs: []string{"2"},
   417  					ExpectedErr:  peer.ErrPeerAddAlreadyInList("2"),
   418  				},
   419  				ChooseAction{ExpectedPeer: "1"},
   420  				ChooseAction{ExpectedPeer: "2"},
   421  				ChooseAction{ExpectedPeer: "1"},
   422  			},
   423  			expectedRunning: true,
   424  		},
   425  		{
   426  			msg:                      "remove peer not in list",
   427  			retainedAvailablePeerIDs: []string{"1", "2"},
   428  			expectedAvailablePeers:   []string{"1", "2"},
   429  			peerListActions: []PeerListAction{
   430  				StartAction{},
   431  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   432  				UpdateAction{
   433  					RemovedPeerIDs: []string{"3"},
   434  					ExpectedErr:    peer.ErrPeerRemoveNotInList("3"),
   435  				},
   436  				ChooseAction{ExpectedPeer: "1"},
   437  				ChooseAction{ExpectedPeer: "2"},
   438  				ChooseAction{ExpectedPeer: "1"},
   439  			},
   440  			expectedRunning: true,
   441  		},
   442  		{
   443  			msg:                      "remove release error",
   444  			retainedAvailablePeerIDs: []string{"1", "2"},
   445  			errReleasedPeerIDs:       []string{"2"},
   446  			releaseErr:               peer.ErrTransportHasNoReferenceToPeer{},
   447  			expectedAvailablePeers:   []string{"1"},
   448  			peerListActions: []PeerListAction{
   449  				StartAction{},
   450  				UpdateAction{AddedPeerIDs: []string{"1", "2"}},
   451  				UpdateAction{
   452  					RemovedPeerIDs: []string{"2"},
   453  					ExpectedErr:    peer.ErrTransportHasNoReferenceToPeer{},
   454  				},
   455  				ChooseAction{ExpectedPeer: "1"},
   456  				ChooseAction{ExpectedPeer: "1"},
   457  			},
   458  			expectedRunning: true,
   459  		},
   460  		{
   461  			msg:                      "block but added too late",
   462  			retainedAvailablePeerIDs: []string{"1"},
   463  			expectedAvailablePeers:   []string{"1"},
   464  			peerListActions: []PeerListAction{
   465  				StartAction{},
   466  				ConcurrentAction{
   467  					Actions: []PeerListAction{
   468  						ChooseAction{
   469  							InputContextTimeout: 10 * time.Millisecond,
   470  							ExpectedErrMsg:      "peer list has no peers",
   471  						},
   472  						UpdateAction{AddedPeerIDs: []string{"1"}},
   473  					},
   474  					Wait: 20 * time.Millisecond,
   475  				},
   476  				ChooseAction{ExpectedPeer: "1"},
   477  			},
   478  			expectedRunning: true,
   479  		},
   480  		{
   481  			msg:                        "add unavailable peer",
   482  			retainedAvailablePeerIDs:   []string{"1"},
   483  			retainedUnavailablePeerIDs: []string{"2"},
   484  			expectedAvailablePeers:     []string{"1"},
   485  			expectedUnavailablePeers:   []string{"2"},
   486  			peerListActions: []PeerListAction{
   487  				StartAction{},
   488  				UpdateAction{AddedPeerIDs: []string{"1"}},
   489  				UpdateAction{AddedPeerIDs: []string{"2"}},
   490  				ChooseAction{
   491  					ExpectedPeer:        "1",
   492  					InputContextTimeout: 20 * time.Millisecond,
   493  				},
   494  				ChooseAction{
   495  					ExpectedPeer:        "1",
   496  					InputContextTimeout: 20 * time.Millisecond,
   497  				},
   498  			},
   499  			expectedRunning: true,
   500  		},
   501  		{
   502  			msg:                        "remove unavailable peer",
   503  			retainedUnavailablePeerIDs: []string{"1"},
   504  			releasedPeerIDs:            []string{"1"},
   505  			peerListActions: []PeerListAction{
   506  				StartAction{},
   507  				UpdateAction{AddedPeerIDs: []string{"1"}},
   508  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   509  				ChooseAction{
   510  					InputContextTimeout: 10 * time.Millisecond,
   511  					ExpectedErrMsg:      "peer list has no peers",
   512  				},
   513  			},
   514  			expectedRunning: true,
   515  		},
   516  		{
   517  			msg:                        "notify peer is now available",
   518  			retainedUnavailablePeerIDs: []string{"1"},
   519  			expectedAvailablePeers:     []string{"1"},
   520  			peerListActions: []PeerListAction{
   521  				StartAction{},
   522  				UpdateAction{AddedPeerIDs: []string{"1"}},
   523  				ChooseAction{
   524  					InputContextTimeout: 10 * time.Millisecond,
   525  					ExpectedErrMsg:      "list has 1 peer but it is not responsive",
   526  				},
   527  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   528  				ChooseAction{ExpectedPeer: "1"},
   529  			},
   530  			expectedRunning: true,
   531  		},
   532  		{
   533  			msg:                      "notify peer is still available",
   534  			retainedAvailablePeerIDs: []string{"1"},
   535  			expectedAvailablePeers:   []string{"1"},
   536  			peerListActions: []PeerListAction{
   537  				StartAction{},
   538  				UpdateAction{AddedPeerIDs: []string{"1"}},
   539  				ChooseAction{ExpectedPeer: "1"},
   540  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   541  				ChooseAction{ExpectedPeer: "1"},
   542  			},
   543  			expectedRunning: true,
   544  		},
   545  		{
   546  			msg:                      "notify peer is now unavailable",
   547  			retainedAvailablePeerIDs: []string{"1"},
   548  			expectedUnavailablePeers: []string{"1"},
   549  			peerListActions: []PeerListAction{
   550  				StartAction{},
   551  				UpdateAction{AddedPeerIDs: []string{"1"}},
   552  				ChooseAction{ExpectedPeer: "1"},
   553  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Unavailable},
   554  				ChooseAction{
   555  					InputContextTimeout: 10 * time.Millisecond,
   556  					ExpectedErrMsg:      "peer list has 1 peer but it is not responsive",
   557  				},
   558  			},
   559  			expectedRunning: true,
   560  		},
   561  		{
   562  			msg:                        "notify peer is still unavailable",
   563  			retainedUnavailablePeerIDs: []string{"1"},
   564  			expectedUnavailablePeers:   []string{"1"},
   565  			peerListActions: []PeerListAction{
   566  				StartAction{},
   567  				UpdateAction{AddedPeerIDs: []string{"1"}},
   568  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Unavailable},
   569  				ChooseAction{
   570  					InputContextTimeout: 10 * time.Millisecond,
   571  					ExpectedErrMsg:      "has 1 peer but it is not responsive",
   572  				},
   573  			},
   574  			expectedRunning: true,
   575  		},
   576  		{
   577  			msg:                      "notify invalid peer",
   578  			retainedAvailablePeerIDs: []string{"1"},
   579  			releasedPeerIDs:          []string{"1"},
   580  			peerListActions: []PeerListAction{
   581  				StartAction{},
   582  				UpdateAction{AddedPeerIDs: []string{"1"}},
   583  				UpdateAction{RemovedPeerIDs: []string{"1"}},
   584  				NotifyStatusChangeAction{PeerID: "1", NewConnectionStatus: peer.Available},
   585  			},
   586  			expectedRunning: true,
   587  		},
   588  		{
   589  			msg: "random insertion",
   590  			// all scores are equal, degenerating to round-robin behavior
   591  			// peer ordering is therefore by 'last'
   592  			nextRand: nextRandFromSlice([]int{
   593  				0, // insert p1 at end of list
   594  				1, // insert p2 at end of list
   595  				2, // insert p3 at end of list
   596  				0, // swap p4 with index 0
   597  				1, // swap p5 with index 1
   598  			}),
   599  			retainedAvailablePeerIDs: []string{"1", "2", "3", "4", "5"},
   600  			expectedAvailablePeers:   []string{"1", "2", "3", "4", "5"},
   601  			peerListActions: []PeerListAction{
   602  				StartAction{},
   603  				UpdateAction{AddedPeerIDs: []string{"1", "2", "3"}},
   604  				UpdateAction{AddedPeerIDs: []string{"4", "5"}},
   605  				ChooseAction{ExpectedPeer: "4"},
   606  				ChooseAction{ExpectedPeer: "5"},
   607  				ChooseAction{ExpectedPeer: "3"},
   608  				ChooseAction{ExpectedPeer: "1"},
   609  				ChooseAction{ExpectedPeer: "2"},
   610  			},
   611  			expectedRunning: true,
   612  		},
   613  	}
   614  
   615  	for _, tt := range tests {
   616  		t.Run(tt.msg, func(t *testing.T) {
   617  			mockCtrl := gomock.NewController(t)
   618  			defer mockCtrl.Finish()
   619  
   620  			transport := NewMockTransport(mockCtrl)
   621  
   622  			// Healthy Transport Retain/Release
   623  			peerMap := ExpectPeerRetains(
   624  				transport,
   625  				tt.retainedAvailablePeerIDs,
   626  				tt.retainedUnavailablePeerIDs,
   627  			)
   628  			ExpectPeerReleases(transport, tt.releasedPeerIDs, nil)
   629  
   630  			// Unhealthy Transport Retain/Release
   631  			ExpectPeerRetainsWithError(transport, tt.errRetainedPeerIDs, tt.retainErr)
   632  			ExpectPeerReleases(transport, tt.errReleasedPeerIDs, tt.releaseErr)
   633  
   634  			logger := zaptest.NewLogger(t)
   635  
   636  			randOption := DisableRandomInsertion()
   637  			if tt.nextRand != nil {
   638  				randOption = InsertionOrder(tt.nextRand)
   639  			}
   640  			opts := []ListOption{Capacity(0), noShuffle, randOption, Logger(logger), Seed(0)}
   641  
   642  			pl := New(transport, opts...)
   643  
   644  			deps := ListActionDeps{
   645  				Peers: peerMap,
   646  			}
   647  			ApplyPeerListActions(t, pl, tt.peerListActions, deps)
   648  
   649  			var availablePeers []string
   650  			var unavailablePeers []string
   651  			for _, p := range pl.Peers() {
   652  				ps := p.Status()
   653  				if ps.ConnectionStatus == peer.Available {
   654  					availablePeers = append(availablePeers, p.Identifier())
   655  				} else if ps.ConnectionStatus == peer.Unavailable {
   656  					unavailablePeers = append(unavailablePeers, p.Identifier())
   657  				}
   658  			}
   659  			sort.Strings(availablePeers)
   660  			sort.Strings(unavailablePeers)
   661  
   662  			assert.Equal(t, tt.expectedAvailablePeers, availablePeers, "incorrect available peers")
   663  			assert.Equal(t, tt.expectedUnavailablePeers, unavailablePeers, "incorrect unavailable peers")
   664  			assert.Equal(t, tt.expectedRunning, pl.IsRunning(), "Peer list should match expected final running state")
   665  		})
   666  	}
   667  }
   668  
   669  var noShuffle ListOption = func(c *listConfig) {
   670  	c.shuffle = false
   671  }
   672  
   673  func TestFailFastConfig(t *testing.T) {
   674  	conn, err := net.Listen("tcp", "127.0.0.1:0")
   675  	require.NoError(t, err)
   676  	require.NoError(t, conn.Close())
   677  
   678  	serviceName := "test"
   679  	config := whitespace.Expand(fmt.Sprintf(`
   680  		outbounds:
   681  			nowhere:
   682  				http:
   683  					fewest-pending-requests:
   684  						peers:
   685  							- %q
   686  						capacity: 10
   687  						failFast: true
   688  	`, conn.Addr()))
   689  	cfgr := yarpcconfig.New()
   690  	cfgr.MustRegisterTransport(http.TransportSpec())
   691  	cfgr.MustRegisterPeerList(Spec())
   692  	cfg, err := cfgr.LoadConfigFromYAML(serviceName, strings.NewReader(config))
   693  	require.NoError(t, err)
   694  
   695  	d := yarpc.NewDispatcher(cfg)
   696  	d.Start()
   697  	defer d.Stop()
   698  
   699  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   700  	defer cancel()
   701  
   702  	client := d.MustOutboundConfig("nowhere")
   703  	_, err = client.Outbounds.Unary.Call(ctx, &transport.Request{
   704  		Service:   "service",
   705  		Caller:    "caller",
   706  		Encoding:  transport.Encoding("blank"),
   707  		Procedure: "bogus",
   708  		Body:      strings.NewReader("nada"),
   709  	})
   710  	require.Error(t, err)
   711  	assert.Contains(t, err.Error(), "peer list has 1 peer but it is not responsive")
   712  }