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  }