github.com/uber/kraken@v0.1.4/lib/hrw/rendezvous_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 hrw
    15  
    16  import (
    17  	"crypto/md5"
    18  	"crypto/sha256"
    19  	"encoding/binary"
    20  	"math"
    21  	"reflect"
    22  	"runtime"
    23  	"sort"
    24  	"testing"
    25  
    26  	"github.com/spaolacci/murmur3"
    27  	"github.com/stretchr/testify/assert"
    28  )
    29  
    30  func TestScoreFunctionFloatPrecision(t *testing.T) {
    31  	t.Parallel()
    32  
    33  	byteLength := []int{8, 16, 32} // 64, 128, 256 bits
    34  
    35  	for index, bl := range byteLength {
    36  		for indexScore, scoreFunc := range []UIntToFloat{BigIntToFloat64, UInt64ToFloat64} {
    37  			// UInt64ToFloat64 can't work on values > 64 bits
    38  			if index > 0 && indexScore > 0 {
    39  				continue
    40  			}
    41  			maxHashValue := make([]byte, bl)
    42  			val := make([]byte, bl)
    43  
    44  			for i := 0; i < bl; i++ {
    45  				maxHashValue[i] = 0xFF
    46  				val[i] = 0
    47  			}
    48  
    49  			val[len(val)-1] = 1
    50  			floatVal := scoreFunc(val, maxHashValue, nil)
    51  			assert.NotEqual(t, floatVal, 0.0)
    52  			assert.Equal(t, math.IsNaN(math.Log(floatVal)), false)
    53  			assert.Equal(t, math.IsInf(math.Log(floatVal), 1), false)
    54  			assert.Equal(t, math.IsInf(math.Log(floatVal), -1), false)
    55  		}
    56  	}
    57  }
    58  
    59  func TestScoreFunctionUint64ToFloat64BadValues(t *testing.T) {
    60  	t.Parallel()
    61  
    62  	maxHashValue := make([]byte, 8)
    63  	for i := 0; i < 8; i++ {
    64  		maxHashValue[i] = 0xFF
    65  	}
    66  
    67  	u64val := (1 << 53)
    68  	for i := 0; i <= 11; i++ {
    69  		b := make([]byte, 8)
    70  		binary.BigEndian.PutUint64(b, uint64(u64val))
    71  
    72  		floatVal := UInt64ToFloat64(b, maxHashValue, nil)
    73  
    74  		assert.Equal(t, floatVal, 0.0)
    75  
    76  		floatVal = UInt64ToFloat64(b, maxHashValue, murmur3.New64())
    77  
    78  		assert.NotEqual(t, floatVal, 0.0)
    79  		assert.Equal(t, math.IsNaN(math.Log(floatVal)), false)
    80  		assert.Equal(t, math.IsInf(math.Log(floatVal), 1), false)
    81  		assert.Equal(t, math.IsInf(math.Log(floatVal), -1), false)
    82  		u64val = u64val << 1
    83  
    84  	}
    85  	u64val = (1 << 53)
    86  	for i := 0; i <= 11; i++ {
    87  		b := make([]byte, 8)
    88  		binary.BigEndian.PutUint64(b, uint64(u64val))
    89  
    90  		floatVal := UInt64ToFloat64(b, maxHashValue, murmur3.New64())
    91  
    92  		assert.NotEqual(t, floatVal, 0.0)
    93  		assert.Equal(t, math.IsNaN(math.Log(floatVal)), false)
    94  		assert.Equal(t, math.IsInf(math.Log(floatVal), 1), false)
    95  		assert.Equal(t, math.IsInf(math.Log(floatVal), -1), false)
    96  		u64val = u64val << 1
    97  	}
    98  }
    99  
   100  func TestKeyDistributionAndNodeChanges(t *testing.T) {
   101  	t.Parallel()
   102  
   103  	hashes := []struct {
   104  		name string
   105  		f    HashFactory
   106  	}{
   107  		{"murmur3", Murmur3Hash},
   108  		{"sha256", sha256.New},
   109  		{"md5", md5.New},
   110  	}
   111  
   112  	scoreFuncs := []struct {
   113  		name string
   114  		f    UIntToFloat
   115  	}{
   116  		{"BigIntToFloat64", BigIntToFloat64},
   117  		{"UInt64ToFloat64", UInt64ToFloat64},
   118  	}
   119  	numKeys := 1000
   120  
   121  	tests := []func(int, HashFactory, UIntToFloat, *testing.T){
   122  		testKeyDistribution,
   123  		testAddNodes,
   124  		testRemoveNodes,
   125  		testReturnNodesLength,
   126  		testReturnNodesOrder,
   127  		testAddingCapacity,
   128  		testRemovingCapacity,
   129  	}
   130  
   131  	for _, hash := range hashes {
   132  		for _, scoreFunc := range scoreFuncs {
   133  			t.Run(hash.name+scoreFunc.name, func(t *testing.T) {
   134  				for _, test := range tests {
   135  					testName := runtime.FuncForPC(reflect.ValueOf(test).Pointer()).Name()
   136  					t.Run(testName, func(*testing.T) {
   137  						test(numKeys, hash.f, scoreFunc.f, t)
   138  					})
   139  				}
   140  			})
   141  		}
   142  	}
   143  }
   144  
   145  func testKeyDistribution(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   146  	rh, nodekeys := RendezvousHashFixture(numKeys, hash, scoreFunc, 100, 200, 400, 800)
   147  	assertKeyDistribution(t, rh, nodekeys, numKeys, 1500.0, 0.1)
   148  }
   149  
   150  func testAddNodes(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   151  	rh, nodekeys := RendezvousHashFixture(numKeys, hash, scoreFunc, 100, 200, 400, 800)
   152  
   153  	rh.RemoveNode("1")
   154  	assert.Equal(t, len(rh.Nodes), 3)
   155  
   156  	for name, v := range nodekeys {
   157  		if name == "1" {
   158  			// "1" node is going to be relocated to other nodes.
   159  			continue
   160  		}
   161  		// The rmaining nodes should not change their allocation buckets.
   162  		for key := range v {
   163  			nodes := rh.GetOrderedNodes(key, 1)
   164  			assert.Equal(t, nodes[0].Label, name)
   165  		}
   166  	}
   167  }
   168  
   169  func testRemoveNodes(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   170  	rh, nodekeys := RendezvousHashFixture(numKeys, hash, scoreFunc, 100, 200, 400, 800)
   171  
   172  	rh.AddNode("4", 200)
   173  	nodekeys["4"] = make(map[string]struct{})
   174  
   175  	assert.Equal(t, len(rh.Nodes), 5)
   176  
   177  	for name, v := range nodekeys {
   178  		if name == "4" {
   179  			// New node "4" will get some keys from other nodes.
   180  			continue
   181  		}
   182  		// Th remaining nodes should not change their allocation buckets.
   183  		for key := range v {
   184  			nodes := rh.GetOrderedNodes(key, 1)
   185  			if nodes[0].Label != name {
   186  				assert.Equal(t, nodes[0].Label, "4")
   187  				nodekeys[nodes[0].Label][key] = struct{}{}
   188  				delete(nodekeys[name], key)
   189  			}
   190  		}
   191  	}
   192  	assertKeyDistribution(t, rh, nodekeys, numKeys, 1700.0, 0.1)
   193  }
   194  
   195  func testReturnNodesLength(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   196  	rh, _ := RendezvousHashFixture(0, hash, scoreFunc, 100, 200, 400, 800)
   197  	keys := HashKeyFixture(1, hash)
   198  
   199  	var scores []float64
   200  	for _, node := range rh.Nodes {
   201  		score := node.Score(keys[0])
   202  		scores = append(scores, score)
   203  	}
   204  	sort.Sort(ByScore(scores))
   205  	nodes := rh.GetOrderedNodes(keys[0], 4)
   206  	assert.Equal(t, len(nodes), 4)
   207  }
   208  
   209  func testReturnNodesOrder(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   210  	rh, _ := RendezvousHashFixture(0, hash, scoreFunc, 100, 200, 400, 800)
   211  	keys := HashKeyFixture(1, hash)
   212  
   213  	var scores []float64
   214  	for _, node := range rh.Nodes {
   215  		score := node.Score(keys[0])
   216  		scores = append(scores, score)
   217  	}
   218  	sort.Sort(ByScore(scores))
   219  	nodes := rh.GetOrderedNodes(keys[0], 4)
   220  	for index, node := range nodes {
   221  		score := node.Score(keys[0])
   222  		assert.Equal(t, score, scores[4-index-1])
   223  	}
   224  }
   225  
   226  func testAddingCapacity(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   227  	rh, nodekeys := RendezvousHashFixture(numKeys, hash, scoreFunc, 100, 200, 400, 800)
   228  
   229  	_, index := rh.GetNode("3")
   230  	rh.Nodes[index].Weight = 1000
   231  
   232  	for name, v := range nodekeys {
   233  		// Some keys in nodes should change their allocation buckets
   234  		// accomdate for new capacity on node "3".
   235  		for key := range v {
   236  			nodes := rh.GetOrderedNodes(key, 1)
   237  			if nodes[0].Label != name {
   238  				assert.Equal(t, nodes[0].Label, "3")
   239  				nodekeys[nodes[0].Label][key] = struct{}{}
   240  				delete(nodekeys[name], key)
   241  			} else {
   242  				assert.Equal(t, nodes[0].Label, name)
   243  			}
   244  		}
   245  	}
   246  
   247  	// Make sure we still keep the target distribution after resharding.
   248  	assertKeyDistribution(t, rh, nodekeys, numKeys, 1700.0, 0.1)
   249  }
   250  
   251  func testRemovingCapacity(numKeys int, hash HashFactory, scoreFunc UIntToFloat, t *testing.T) {
   252  	rh, nodekeys := RendezvousHashFixture(numKeys, hash, scoreFunc, 100, 200, 400, 800)
   253  
   254  	_, index := rh.GetNode("3")
   255  	rh.Nodes[index].Weight = 200
   256  
   257  	for name, v := range nodekeys {
   258  		// The remaining nodes should not change their allocation buckets.
   259  		for key := range v {
   260  			nodes := rh.GetOrderedNodes(key, 1)
   261  			if nodes[0].Label != name {
   262  				assert.Equal(t, name, "3")
   263  				assert.NotEqual(t, nodes[0].Label, "3")
   264  				nodekeys[nodes[0].Label][key] = struct{}{}
   265  				delete(nodekeys[name], key)
   266  			} else {
   267  				assert.Equal(t, nodes[0].Label, name)
   268  			}
   269  		}
   270  	}
   271  
   272  	// Make sure we still keep the target distribution after resharding.
   273  	assertKeyDistribution(t, rh, nodekeys, numKeys, 900.0, 0.1)
   274  }