knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/hash/hash_test.go (about)

     1  /*
     2  Copyright 2020 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      https://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package hash
    18  
    19  import (
    20  	"fmt"
    21  	"hash/fnv"
    22  	"math"
    23  	"sort"
    24  	"testing"
    25  
    26  	"github.com/davecgh/go-spew/spew"
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/google/uuid"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  )
    31  
    32  func ExampleChooseSubset_selectOne() {
    33  	// This example shows how to do consistent bucket
    34  	// assignment using ChooseSubset.
    35  
    36  	tasks := sets.New[string]("task1", "task2", "task3")
    37  
    38  	ret := ChooseSubset(tasks, 1, "my-key1")
    39  	fmt.Println(ret.UnsortedList()[0])
    40  
    41  	ret = ChooseSubset(tasks, 1, "something/another-key")
    42  	fmt.Println(ret.UnsortedList()[0])
    43  	// Output: task3
    44  	// task2
    45  }
    46  
    47  func ExampleChooseSubset_selectMany() {
    48  	// This example shows how to do consistent bucket
    49  	// assignment using ChooseSubset.
    50  
    51  	tasks := sets.New[string]("task1", "task2", "task3", "task4", "task5")
    52  
    53  	ret := ChooseSubset(tasks, 2, "my-key1")
    54  	fmt.Println(sets.List(ret))
    55  	// Output: [task3 task4]
    56  }
    57  
    58  func TestBuildHashes(t *testing.T) {
    59  	const target = "a target to remember"
    60  	set := sets.New[string]("a", "b", "c", "e", "f")
    61  
    62  	hd1 := buildHashes(set, target)
    63  	hd2 := buildHashes(set, target)
    64  	t.Log("HashData = ", spew.Sprintf("%+v", hd1))
    65  
    66  	if !cmp.Equal(hd1, hd2, cmp.AllowUnexported(hashData{})) {
    67  		t.Errorf("buildHashe is not consistent: diff(-want,+got):\n%s",
    68  			cmp.Diff(hd1, hd2, cmp.AllowUnexported(hashData{})))
    69  	}
    70  	if !sort.SliceIsSorted(hd1.hashPool, func(i, j int) bool {
    71  		return hd1.hashPool[i] < hd1.hashPool[j]
    72  	}) {
    73  		t.Error("From list is not sorted:", hd1.hashPool)
    74  	}
    75  }
    76  
    77  func TestChooseSubset(t *testing.T) {
    78  	tests := []struct {
    79  		name    string
    80  		from    sets.Set[string]
    81  		target  string
    82  		wantNum int
    83  		want    sets.Set[string]
    84  	}{{
    85  		name:    "return all",
    86  		from:    sets.New[string]("sun", "moon", "mars", "mercury"),
    87  		target:  "a target!",
    88  		wantNum: 4,
    89  		want:    sets.New[string]("sun", "moon", "mars", "mercury"),
    90  	}, {
    91  		name:    "subset 1",
    92  		from:    sets.New[string]("sun", "moon", "mars", "mercury"),
    93  		target:  "a target!",
    94  		wantNum: 2,
    95  		want:    sets.New[string]("mercury", "moon"),
    96  	}, {
    97  		name:    "subset 2",
    98  		from:    sets.New[string]("sun", "moon", "mars", "mercury"),
    99  		target:  "something else entirely",
   100  		wantNum: 2,
   101  		want:    sets.New[string]("mercury", "mars"),
   102  	}, {
   103  		name:    "select 3",
   104  		from:    sets.New[string]("sun", "moon", "mars", "mercury"),
   105  		target:  "something else entirely",
   106  		wantNum: 3,
   107  		want:    sets.New[string]("mars", "mercury", "sun"),
   108  	}}
   109  
   110  	for _, tc := range tests {
   111  		t.Run(tc.name, func(t *testing.T) {
   112  			got := ChooseSubset(tc.from, tc.wantNum, tc.target)
   113  			if !got.Equal(tc.want) {
   114  				t.Errorf("Chose = %v, want = %v, diff(-want,+got):\n%s", got, tc.want, cmp.Diff(tc.want, got))
   115  			}
   116  		})
   117  	}
   118  }
   119  
   120  func TestCollisionHandling(t *testing.T) {
   121  	const (
   122  		key1   = "b08006d4-81f9-42ee-808b-ea18a39cbd83"
   123  		key2   = "c9dc8df4-8c8d-4077-8750-6d2c2113a23b"
   124  		target = "e68a64e1-19d8-4855-9ffa-04f49223a059"
   125  	)
   126  	// Verify baseline, that they collide.
   127  	hasher := fnv.New64a()
   128  	h1 := computeHash([]byte(key1+target), hasher) % universe
   129  	hasher.Reset()
   130  	h2 := computeHash([]byte(key2+target), hasher) % universe
   131  	if h1 != h2 {
   132  		t.Fatalf("Baseline incorrect keys don't collide %d != %d", h1, h2)
   133  	}
   134  	hd := buildHashes(sets.New[string](key1, key2), target)
   135  	if got, want := len(hd.nameLookup), 2; got != want {
   136  		t.Error("Did not resolve collision, only 1 key in the map")
   137  	}
   138  }
   139  
   140  func TestOverlay(t *testing.T) {
   141  	// Execute
   142  	// `go test -run=TestOverlay -count=200`
   143  	// To ensure assignments are still not skewed.
   144  	const (
   145  		sources   = 50
   146  		samples   = 100000
   147  		selection = 10
   148  		want      = samples * selection / sources
   149  		threshold = want / 5 // 20%
   150  	)
   151  	from := sets.New[string]()
   152  	for range sources {
   153  		from.Insert(uuid.NewString())
   154  	}
   155  	freqs := make(map[string]int, sources)
   156  
   157  	for range samples {
   158  		target := uuid.NewString()
   159  		got := ChooseSubset(from, selection, target)
   160  		for k := range got {
   161  			freqs[k]++
   162  		}
   163  	}
   164  
   165  	totalDiff := 0.
   166  	for _, v := range freqs {
   167  		diff := float64(v - want)
   168  		adiff := math.Abs(diff)
   169  		totalDiff += adiff
   170  		if adiff > threshold {
   171  			t.Errorf("Diff for %d is %v, larger than threshold: %d", v, diff, threshold)
   172  		}
   173  	}
   174  	t.Log(totalDiff / float64(len(freqs)))
   175  }
   176  
   177  func BenchmarkSelection(b *testing.B) {
   178  	const maxSet = 200
   179  	from := make([]string, maxSet)
   180  	for i := range maxSet {
   181  		from[i] = uuid.NewString()
   182  	}
   183  	for _, v := range []int{5, 10, 25, 50, 100, 150, maxSet} {
   184  		for _, ss := range []int{1, 5, 10, 15, 20, 25} {
   185  			b.Run(fmt.Sprintf("pool-%d-subset-%d", v, ss), func(b *testing.B) {
   186  				target := uuid.NewString()
   187  				in := sets.New[string](from[:v]...)
   188  				for range b.N {
   189  					ChooseSubset(in, 10, target)
   190  				}
   191  			})
   192  		}
   193  	}
   194  }