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 }