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 }