github.com/blend/go-sdk@v1.20220411.3/consistenthash/consistent_hash_test.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package consistenthash 9 10 import ( 11 "encoding/json" 12 "fmt" 13 "testing" 14 15 "github.com/blend/go-sdk/assert" 16 ) 17 18 func Test_ConsistentHash_typical(t *testing.T) { 19 its := assert.New(t) 20 21 const bucketCount = 5 22 const itemCount = 150 23 24 var buckets []string 25 for x := 0; x < bucketCount; x++ { 26 buckets = append(buckets, fmt.Sprintf("worker-%d", x)) 27 } 28 var items []string 29 for x := 0; x < itemCount; x++ { 30 items = append(items, fmt.Sprintf("google-%d", x)) 31 } 32 33 ch := New( 34 OptBuckets( 35 buckets..., 36 ), 37 ) 38 its.Len(ch.hashring, bucketCount*ch.ReplicasOrDefault(), "the internal hashring should mirror the count of the buckets") 39 40 returnedBuckets := ch.Buckets() 41 its.Len(returnedBuckets, bucketCount) 42 setIsSorted(its, returnedBuckets) 43 44 assignments := ch.Assignments(items...) 45 46 worker0items := assignments["worker-0"] 47 its.NotEmpty(worker0items) 48 49 worker1items := assignments["worker-1"] 50 its.NotEmpty(worker1items) 51 52 worker2items := assignments["worker-2"] 53 its.NotEmpty(worker2items) 54 55 worker3items := assignments["worker-3"] 56 its.NotEmpty(worker3items) 57 58 worker4items := assignments["worker-4"] 59 its.NotEmpty(worker4items) 60 61 // verify that all the bucket assignments are disjoint, that is 62 // none of the items in an assignment exist in another assignment 63 setsAreDisjoint(its, worker0items, worker1items, worker2items, worker3items, worker4items) 64 65 // verify this is also the case through the `IsAssigned` method 66 for _, item := range worker0items { 67 its.True(ch.IsAssigned("worker-0", item)) 68 } 69 // verify the worker-0 items are not assigned to 70 // any other nodes 71 for _, item := range worker0items { 72 its.False(ch.IsAssigned("worker-1", item)) 73 its.False(ch.IsAssigned("worker-2", item)) 74 its.False(ch.IsAssigned("worker-3", item)) 75 its.False(ch.IsAssigned("worker-4", item)) 76 } 77 78 t.Log(spew(assignmentCounts(assignments))) 79 // verify the sets are relatively evenly sized 80 // this is generally the most likely test to fail, because 81 // the hash bucket assignments can be lumpy. 82 setsAreEvenlySized(its, itemCount, worker0items, worker1items, worker2items, worker3items, worker4items) 83 84 // verify that all the consistent hash items exist in exactly one bucket assignment 85 setExistsInOtherSets(its, items, worker0items, worker1items, worker2items, worker3items, worker4items) 86 } 87 88 func Test_ConsistentHash_AddBuckets(t *testing.T) { 89 its := assert.New(t) 90 91 ch := New() 92 93 res := ch.AddBuckets("worker-0", "worker-1", "worker-2") 94 its.True(res) 95 res = ch.AddBuckets("worker-0", "worker-1", "worker-2") 96 its.False(res, "we should return false if _no_ new buckets were added") 97 res = ch.AddBuckets("worker-0", "worker-1", "worker-2", "worker-3") 98 its.True(res, "we should return true if _any_ new buckets were added") 99 buckets := ch.Buckets() 100 its.Len(buckets, 4) 101 its.Equal([]string{"worker-0", "worker-1", "worker-2", "worker-3"}, buckets) 102 } 103 104 func Test_ConsistentHash_RemoveBucket(t *testing.T) { 105 its := assert.New(t) 106 107 ch := New() 108 109 res := ch.AddBuckets("worker-0", "worker-1", "worker-2") 110 its.True(res) 111 res = ch.RemoveBucket("worker-3") 112 its.False(res, "we should return false if bucket not found") 113 res = ch.RemoveBucket("worker-2") 114 its.True(res, "we should return true if bucket found") 115 buckets := ch.Buckets() 116 its.Len(buckets, 2) 117 its.Equal([]string{"worker-0", "worker-1"}, buckets) 118 } 119 120 func Test_ConsistentHash_redistribute_addBuckets(t *testing.T) { 121 its := assert.New(t) 122 123 const bucketCount = 5 124 const itemCount = 100 125 const itemsPerBucket = itemCount / bucketCount 126 const maxBucketDelta = (itemsPerBucket / bucketCount) + 4 127 128 var buckets []string 129 for x := 0; x < bucketCount; x++ { 130 buckets = append(buckets, fmt.Sprintf("worker-%d", x)) 131 } 132 var items []string 133 for x := 0; x < itemCount; x++ { 134 items = append(items, fmt.Sprintf("google-%d", x)) 135 } 136 137 ch := New( 138 OptBuckets( 139 buckets..., 140 ), 141 ) 142 143 assignments := ch.Assignments(items...) 144 145 oldWorker0items := assignments["worker-0"] 146 its.NotEmpty(oldWorker0items) 147 148 oldWorker1items := assignments["worker-1"] 149 its.NotEmpty(oldWorker1items) 150 151 oldWorker2items := assignments["worker-2"] 152 its.NotEmpty(oldWorker2items) 153 154 oldWorker3items := assignments["worker-3"] 155 its.NotEmpty(oldWorker3items) 156 157 oldWorker4items := assignments["worker-4"] 158 its.NotEmpty(oldWorker4items) 159 160 // simulate adding a bucket 161 ch.AddBuckets("worker-5") 162 its.Len(ch.buckets, bucketCount+1) 163 its.Len(ch.hashring, (bucketCount+1)*ch.ReplicasOrDefault()) 164 165 newAssignments := ch.Assignments(items...) 166 its.Len(newAssignments, bucketCount+1, "assignments length should mirror buckets") 167 168 worker0items := newAssignments["worker-0"] 169 its.NotEmpty(worker0items) 170 171 worker1items := newAssignments["worker-1"] 172 its.NotEmpty(worker1items) 173 174 worker2items := newAssignments["worker-2"] 175 its.NotEmpty(worker2items) 176 177 worker3items := newAssignments["worker-3"] 178 its.NotEmpty(worker3items) 179 180 worker4items := newAssignments["worker-4"] 181 its.NotEmpty(worker4items) 182 183 worker5items := newAssignments["worker-5"] 184 its.NotEmpty(worker5items) 185 186 // verify that all the bucket assignments are disjoint, that is 187 // none of the items in an assignment exist in another assignment 188 setsAreDisjoint(its, worker0items, worker1items, worker2items, worker3items, worker4items, worker5items) 189 190 // verify that all the consistent hash items exist in a bucket assignment 191 setExistsInOtherSets(its, items, worker0items, worker1items, worker2items, worker3items, worker4items, worker5items) 192 193 // verify that we're moving items around consistently 194 195 t.Log(spew(assignmentCounts(newAssignments))) 196 itsConsistent(its, oldWorker0items, worker0items, maxBucketDelta) 197 itsConsistent(its, oldWorker1items, worker1items, maxBucketDelta) 198 itsConsistent(its, oldWorker2items, worker2items, maxBucketDelta) 199 itsConsistent(its, oldWorker3items, worker3items, maxBucketDelta) 200 itsConsistent(its, oldWorker4items, worker4items, maxBucketDelta) 201 } 202 203 func Test_ConsistentHash_redistribute_removeBucket(t *testing.T) { 204 its := assert.New(t) 205 206 const bucketCount = 5 207 const itemCount = 100 208 const itemsPerBucket = itemCount / bucketCount 209 const maxBucketDelta = (itemsPerBucket / bucketCount) + 4 210 211 var buckets []string 212 for x := 0; x < bucketCount; x++ { 213 buckets = append(buckets, fmt.Sprintf("worker-%d", x)) 214 } 215 var items []string 216 for x := 0; x < itemCount; x++ { 217 items = append(items, fmt.Sprintf("google-%d", x)) 218 } 219 220 ch := New( 221 OptBuckets( 222 buckets..., 223 ), 224 ) 225 assignments := ch.Assignments(items...) 226 227 oldWorker0items := assignments["worker-0"] 228 its.NotEmpty(oldWorker0items) 229 230 oldWorker1items := assignments["worker-1"] 231 its.NotEmpty(oldWorker1items) 232 233 oldWorker2items := assignments["worker-2"] 234 its.NotEmpty(oldWorker2items) 235 236 oldWorker3items := assignments["worker-3"] 237 its.NotEmpty(oldWorker3items) 238 239 oldWorker4items := assignments["worker-4"] 240 its.NotEmpty(oldWorker4items) 241 242 // the maximum number of items to move around is the 243 // single bucket we're removing (plus a fudge factor) 244 245 // simulate dropping a bucket (or node) 246 its.True(ch.RemoveBucket("worker-2")) 247 its.Len(ch.buckets, bucketCount-1) 248 its.Len(ch.hashring, (bucketCount-1)*ch.ReplicasOrDefault()) 249 250 _, ok := ch.buckets["worker-2"] 251 its.False(ok) 252 for _, bucket := range ch.hashring { 253 its.NotEqual("worker-2", bucket.Bucket) 254 } 255 256 assignments = ch.Assignments(items...) 257 its.Len(assignments, len(ch.buckets), "assignments length should mirror buckets") 258 259 worker0items := assignments["worker-0"] 260 its.NotEmpty(oldWorker0items) 261 262 worker1items := assignments["worker-1"] 263 its.NotEmpty(oldWorker1items) 264 265 worker2items := assignments["worker-2"] 266 its.NotEmpty(oldWorker2items) 267 268 worker3items := assignments["worker-3"] 269 its.NotEmpty(oldWorker3items) 270 271 worker4items := assignments["worker-4"] 272 its.NotEmpty(oldWorker4items) 273 274 // verify that all the bucket assignments are disjoint, that is 275 // none of the items in an assignment exist in another assignment 276 setsAreDisjoint(its, worker0items, worker1items, worker2items, worker3items, worker4items) 277 278 // verify that all the consistent hash items exist in a bucket assignment 279 setExistsInOtherSets(its, items, worker0items, worker1items, worker3items, worker4items) 280 281 // verify that we're moving items around consistently 282 itsConsistent(its, oldWorker0items, worker0items, maxBucketDelta) 283 itsConsistent(its, oldWorker1items, worker1items, maxBucketDelta) 284 itsConsistent(its, oldWorker3items, worker3items, maxBucketDelta) 285 itsConsistent(its, oldWorker4items, worker4items, maxBucketDelta) 286 } 287 288 func Test_ConsistentHash_notFound_removeBucket(t *testing.T) { 289 its := assert.New(t) 290 291 const bucketCount = 5 292 const itemCount = 100 293 const itemsPerBucket = itemCount / bucketCount 294 const maxBucketDelta = (itemsPerBucket / bucketCount) + 4 295 296 var buckets []string 297 for x := 0; x < bucketCount; x++ { 298 buckets = append(buckets, fmt.Sprintf("worker-%d", x)) 299 } 300 var items []string 301 for x := 0; x < itemCount; x++ { 302 items = append(items, fmt.Sprintf("google_%d", x)) 303 } 304 305 ch := New( 306 OptBuckets( 307 buckets..., 308 ), 309 ) 310 311 oldAssignments := ch.Assignments(items...) 312 its.False(ch.RemoveBucket("not-worker-0")) 313 newAssignments := ch.Assignments(items...) 314 315 assignmentsAreEqual(its, oldAssignments, newAssignments) 316 } 317 318 func Test_ConsistentHash_String(t *testing.T) { 319 its := assert.New(t) 320 321 const bucketCount = 5 322 323 var buckets []string 324 for x := 0; x < bucketCount; x++ { 325 buckets = append(buckets, fmt.Sprintf("worker-%d", x)) 326 } 327 328 ch := New( 329 OptBuckets( 330 buckets..., 331 ), 332 ) 333 its.NotEmpty(ch.String()) 334 } 335 336 func Test_ConsistentHash_MarshalJSON(t *testing.T) { 337 its := assert.New(t) 338 339 const bucketCount = 5 340 341 var buckets []string 342 for x := 0; x < bucketCount; x++ { 343 buckets = append(buckets, fmt.Sprintf("worker-%d", x)) 344 } 345 346 ch := New( 347 OptBuckets( 348 buckets..., 349 ), 350 ) 351 352 output, err := json.Marshal(ch) 353 its.Nil(err) 354 its.NotEmpty(output) 355 356 var verify []HashedBucket 357 err = json.Unmarshal(output, &verify) 358 its.Nil(err) 359 its.Equal(ch.hashring, verify) 360 }