github.com/uber/kraken@v0.1.4/lib/hashring/ring_test.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package hashring
    15  
    16  import (
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/uber/kraken/core"
    21  	"github.com/uber/kraken/lib/healthcheck"
    22  	"github.com/uber/kraken/lib/hostlist"
    23  	"github.com/uber/kraken/mocks/lib/hashring"
    24  	"github.com/uber/kraken/mocks/lib/hostlist"
    25  	"github.com/uber/kraken/utils/randutil"
    26  	"github.com/uber/kraken/utils/stringset"
    27  
    28  	"github.com/golang/mock/gomock"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  func addrsFixture(n int) []string {
    33  	var addrs []string
    34  	for i := 0; i < n; i++ {
    35  		addrs = append(addrs, randutil.Addr())
    36  	}
    37  	return addrs
    38  }
    39  
    40  func TestRingLocationsDistribution(t *testing.T) {
    41  	tests := []struct {
    42  		desc                 string
    43  		clusterSize          int
    44  		maxReplica           int
    45  		expectedDistribution float64
    46  	}{
    47  		{"single host", 1, 1, 1.0},
    48  		{"all replicas", 3, 3, 1.0},
    49  		{"below max replica", 2, 3, 1.0},
    50  		{"above max replica", 6, 3, 0.5},
    51  	}
    52  	for _, test := range tests {
    53  		t.Run(test.desc, func(t *testing.T) {
    54  			require := require.New(t)
    55  
    56  			addrs := addrsFixture(test.clusterSize)
    57  
    58  			r := New(
    59  				Config{MaxReplica: test.maxReplica},
    60  				hostlist.Fixture(addrs...),
    61  				healthcheck.IdentityFilter{})
    62  
    63  			sampleSize := 2000
    64  
    65  			counts := make(map[string]int)
    66  			for i := 0; i < sampleSize; i++ {
    67  				for _, addr := range r.Locations(core.DigestFixture()) {
    68  					counts[addr]++
    69  				}
    70  			}
    71  
    72  			for _, addr := range addrs {
    73  				distribution := float64(counts[addr]) / float64(sampleSize)
    74  				require.InDelta(test.expectedDistribution, distribution, 0.05)
    75  			}
    76  		})
    77  	}
    78  }
    79  
    80  func TestRingLocationsFiltersOutUnhealthyHosts(t *testing.T) {
    81  	require := require.New(t)
    82  
    83  	filter := healthcheck.NewManualFilter()
    84  
    85  	r := New(
    86  		Config{MaxReplica: 3},
    87  		hostlist.Fixture(addrsFixture(10)...),
    88  		filter)
    89  
    90  	d := core.DigestFixture()
    91  
    92  	replicas := r.Locations(d)
    93  	require.Len(replicas, 3)
    94  
    95  	filter.Unhealthy.Add(replicas[0])
    96  	r.Refresh()
    97  
    98  	result := r.Locations(d)
    99  	require.Equal(replicas[1:], result)
   100  }
   101  
   102  func TestRingLocationsReturnsNextHealthyHostWhenReplicaSetUnhealthy(t *testing.T) {
   103  	require := require.New(t)
   104  
   105  	filter := healthcheck.NewManualFilter()
   106  
   107  	r := New(
   108  		Config{MaxReplica: 3},
   109  		hostlist.Fixture(addrsFixture(10)...),
   110  		filter)
   111  
   112  	d := core.DigestFixture()
   113  
   114  	replicas := r.Locations(d)
   115  	require.Len(replicas, 3)
   116  
   117  	// Mark all the original replicas as unhealthy.
   118  	for _, addr := range replicas {
   119  		filter.Unhealthy.Add(addr)
   120  	}
   121  	r.Refresh()
   122  
   123  	// Should consistently select the next address.
   124  	var next []string
   125  	for i := 0; i < 10; i++ {
   126  		next = r.Locations(d)
   127  		require.Len(next, 1)
   128  		require.NotContains(replicas, next[0])
   129  	}
   130  
   131  	// Mark the next address as unhealthy.
   132  	filter.Unhealthy.Add(next[0])
   133  	r.Refresh()
   134  
   135  	// Should consistently select the address after next.
   136  	for i := 0; i < 10; i++ {
   137  		nextNext := r.Locations(d)
   138  		require.Len(nextNext, 1)
   139  		require.NotContains(append(replicas, next[0]), nextNext[0])
   140  	}
   141  }
   142  
   143  func TestRingLocationsReturnsFirstHostWhenAllHostsUnhealthy(t *testing.T) {
   144  	require := require.New(t)
   145  
   146  	filter := healthcheck.NewBinaryFilter()
   147  
   148  	r := New(
   149  		Config{MaxReplica: 3},
   150  		hostlist.Fixture(addrsFixture(10)...),
   151  		filter)
   152  
   153  	d := core.DigestFixture()
   154  
   155  	replicas := r.Locations(d)
   156  	require.Len(replicas, 3)
   157  
   158  	filter.Healthy = false
   159  	r.Refresh()
   160  
   161  	// Should consistently select the first replica once all are unhealthy.
   162  	for i := 0; i < 10; i++ {
   163  		result := r.Locations(d)
   164  		require.Len(result, 1)
   165  		require.Equal(replicas[0], result[0])
   166  	}
   167  }
   168  
   169  func TestRingContains(t *testing.T) {
   170  	require := require.New(t)
   171  
   172  	x := "x:80"
   173  	y := "y:80"
   174  	z := "z:80"
   175  
   176  	r := New(Config{}, hostlist.Fixture(x, y), healthcheck.IdentityFilter{})
   177  
   178  	require.True(r.Contains(x))
   179  	require.True(r.Contains(y))
   180  	require.False(r.Contains(z))
   181  }
   182  
   183  func TestRingMonitor(t *testing.T) {
   184  	require := require.New(t)
   185  
   186  	ctrl := gomock.NewController(t)
   187  	defer ctrl.Finish()
   188  
   189  	cluster := mockhostlist.NewMockList(ctrl)
   190  
   191  	x := "x:80"
   192  	y := "y:80"
   193  
   194  	gomock.InOrder(
   195  		cluster.EXPECT().Resolve().Return(stringset.New(x)),
   196  		cluster.EXPECT().Resolve().Return(stringset.New(y)),
   197  	)
   198  
   199  	r := New(
   200  		Config{RefreshInterval: time.Second},
   201  		cluster,
   202  		healthcheck.IdentityFilter{})
   203  
   204  	stop := make(chan struct{})
   205  	defer close(stop)
   206  	go r.Monitor(stop)
   207  
   208  	d := core.DigestFixture()
   209  
   210  	require.Equal([]string{x}, r.Locations(d))
   211  
   212  	// Monitor should refresh the ring.
   213  	time.Sleep(1250 * time.Millisecond)
   214  
   215  	require.Equal([]string{y}, r.Locations(d))
   216  }
   217  
   218  func TestRingRefreshUpdatesMembership(t *testing.T) {
   219  	require := require.New(t)
   220  
   221  	ctrl := gomock.NewController(t)
   222  	defer ctrl.Finish()
   223  
   224  	cluster := mockhostlist.NewMockList(ctrl)
   225  
   226  	x := "x:80"
   227  	y := "y:80"
   228  	z := "z:80"
   229  
   230  	// x is removed and z is added on the 2nd resolve.
   231  	gomock.InOrder(
   232  		cluster.EXPECT().Resolve().Return(stringset.New(x, y)),
   233  		cluster.EXPECT().Resolve().Return(stringset.New(y, z)),
   234  	)
   235  
   236  	r := New(Config{}, cluster, healthcheck.IdentityFilter{})
   237  
   238  	d := core.DigestFixture()
   239  
   240  	require.ElementsMatch([]string{x, y}, r.Locations(d))
   241  
   242  	r.Refresh()
   243  
   244  	require.ElementsMatch([]string{y, z}, r.Locations(d))
   245  }
   246  
   247  func TestRingNotifiesWatchersOnMembershipChanges(t *testing.T) {
   248  	ctrl := gomock.NewController(t)
   249  	defer ctrl.Finish()
   250  
   251  	cluster := mockhostlist.NewMockList(ctrl)
   252  
   253  	watcher := mockhashring.NewMockWatcher(ctrl)
   254  
   255  	x := "x:80"
   256  	y := "y:80"
   257  	z := "z:80"
   258  
   259  	gomock.InOrder(
   260  		// Called during initial refresh when ring is created.
   261  		cluster.EXPECT().Resolve().Return(stringset.New(x, y)),
   262  		watcher.EXPECT().Notify(stringset.New(x, y)),
   263  
   264  		// Called on subsequent refresh.
   265  		cluster.EXPECT().Resolve().Return(stringset.New(x, y, z)),
   266  		watcher.EXPECT().Notify(stringset.New(x, y, z)),
   267  
   268  		// No changes does not notify.
   269  		cluster.EXPECT().Resolve().Return(stringset.New(x, y, z)),
   270  	)
   271  
   272  	r := New(Config{}, cluster, healthcheck.IdentityFilter{}, WithWatcher(watcher))
   273  	r.Refresh()
   274  	r.Refresh()
   275  }