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  }