knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/hash/hash.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 // This file contains the implementation of the subsetting algorithm for 20 // choosing a subset of input values in a consistent manner. 21 22 import ( 23 "bytes" 24 "hash" 25 "hash/fnv" 26 "sort" 27 "strconv" 28 29 "k8s.io/apimachinery/pkg/util/sets" 30 ) 31 32 const ( 33 startSalt = "start-angle-salt" 34 stepSalt = "step-angle-salt" 35 36 // universe represents the possible range of angles [0, universe). 37 // We want to have universe divide total range evenly to reduce bias. 38 universe uint64 = (1 << 11) 39 ) 40 41 // computeAngle returns a uint64 number which represents 42 // a hash built off the given `n` string for consistent selection 43 // algorithm. 44 // We return uint64 here and cast after computing modulo, since 45 // int might 32 bits on 32 platforms and that would trim result. 46 func computeHash(n []byte, h hash.Hash64) uint64 { 47 h.Reset() 48 h.Write(n) 49 return h.Sum64() 50 } 51 52 type hashData struct { 53 // The set of all hashes for fast lookup and to name mapping 54 nameLookup map[uint64]string 55 // Sorted set of hashes for selection algorithm. 56 hashPool []uint64 57 // start angle 58 start uint64 59 // step angle 60 step uint64 61 } 62 63 func (hd *hashData) fromIndexSet(s sets.Set[int]) sets.Set[string] { 64 ret := make(sets.Set[string], len(s)) 65 for v := range s { 66 ret.Insert(hd.nameForHIndex(v)) 67 } 68 return ret 69 } 70 71 func (hd *hashData) nameForHIndex(hi int) string { 72 return hd.nameLookup[hd.hashPool[hi]] 73 } 74 75 func buildHashes(in sets.Set[string], target string) *hashData { 76 // Any one changing this function must execute 77 // `go test -run=TestOverlay -count=200`. 78 // This is to ensure there is no regression in the selection 79 // algorithm. 80 81 // Sorted list to ensure consistent results every time. 82 from := sets.List(in) 83 // Write in two pieces, so we don't allocate temp string which is sum of both. 84 buf := bytes.NewBufferString(target) 85 buf.WriteString(startSalt) 86 hasher := fnv.New64a() 87 hd := &hashData{ 88 nameLookup: make(map[uint64]string, len(from)), 89 hashPool: make([]uint64, len(from)), 90 start: computeHash(buf.Bytes(), hasher) % universe, 91 } 92 buf.Truncate(len(target)) // Discard the angle salt. 93 buf.WriteString(stepSalt) 94 hd.step = computeHash(buf.Bytes(), hasher) % universe 95 96 for i, f := range from { 97 buf.Reset() // This retains the storage. 98 // Make unique sets for every target. 99 buf.WriteString(f) 100 buf.WriteString(target) 101 h := computeHash(buf.Bytes(), hasher) 102 hs := h % universe 103 // Two values slotted to the same bucket. 104 // On average should happen with 1/universe probability. 105 _, ok := hd.nameLookup[hs] 106 for ok { 107 // Feed the hash as salt. 108 buf.WriteString(strconv.FormatUint(h, 16 /*append hex strings for shortness*/)) 109 h = computeHash(buf.Bytes(), hasher) 110 hs = h % universe 111 _, ok = hd.nameLookup[hs] 112 } 113 114 hd.hashPool[i] = hs 115 hd.nameLookup[hs] = f 116 } 117 // Sort for consistent mapping later. 118 sort.Slice(hd.hashPool, func(i, j int) bool { 119 return hd.hashPool[i] < hd.hashPool[j] 120 }) 121 return hd 122 } 123 124 // ChooseSubset consistently chooses n items from `from`, using 125 // `target` as a seed value. 126 // ChooseSubset is an internal function and presumes sanitized inputs. 127 // TODO(vagababov): once initial impl is ready, think about how to cache 128 // the prepared data. 129 func ChooseSubset(from sets.Set[string], n int, target string) sets.Set[string] { 130 if n >= len(from) { 131 return from 132 } 133 134 hashData := buildHashes(from, target) 135 136 // The algorithm for selection does the following: 137 // 0. Select angle to be the start angle 138 // 1. While n candidates are not selected 139 // 2. Find the index for that angle. 140 // 2.1. While that index is already selected pick next index 141 // 3. Advance angle by `step` 142 // 4. Goto 1. 143 selection := sets.New[int]() 144 angle := hashData.start 145 hpl := len(hashData.hashPool) 146 for len(selection) < n { 147 root := sort.Search(hpl, func(i int) bool { 148 return hashData.hashPool[i] >= angle 149 }) 150 // Wrap around. 151 if root == hpl { 152 root = 0 153 } 154 // Already matched this one. Continue to the next index. 155 for selection.Has(root) { 156 root++ 157 if root == hpl { 158 root = 0 159 } 160 } 161 selection.Insert(root) 162 angle = (angle + hashData.step) % universe 163 } 164 165 return hashData.fromIndexSet(selection) 166 }