github.com/argoproj/argo-cd/v3@v3.2.1/controller/sharding/consistent/consistent.go (about) 1 // An implementation of Consistent Hashing and 2 // Consistent Hashing With Bounded Loads. 3 // 4 // https://en.wikipedia.org/wiki/Consistent_hashing 5 // 6 // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 7 package consistent 8 9 import ( 10 "encoding/binary" 11 "errors" 12 "fmt" 13 "math" 14 "sync" 15 "sync/atomic" 16 17 "github.com/google/btree" 18 19 blake2b "github.com/minio/blake2b-simd" 20 ) 21 22 // OptimalExtraCapacityFactor extra factor capacity (1 + ε). The ideal balance 23 // between keeping the shards uniform while also keeping consistency when 24 // changing shard numbers. 25 const OptimalExtraCapacityFactor = 1.25 26 27 var ErrNoHosts = errors.New("no hosts added") 28 29 type Host struct { 30 Name string 31 Load int64 32 } 33 34 type Consistent struct { 35 servers map[uint64]string 36 clients *btree.BTree 37 loadMap map[string]*Host 38 totalLoad int64 39 replicationFactor int 40 lock sync.RWMutex 41 } 42 43 type item struct { 44 value uint64 45 } 46 47 func (i item) Less(than btree.Item) bool { 48 return i.value < than.(item).value 49 } 50 51 func New() *Consistent { 52 return &Consistent{ 53 servers: map[uint64]string{}, 54 clients: btree.New(2), 55 loadMap: map[string]*Host{}, 56 replicationFactor: 1000, 57 } 58 } 59 60 func NewWithReplicationFactor(replicationFactor int) *Consistent { 61 return &Consistent{ 62 servers: map[uint64]string{}, 63 clients: btree.New(2), 64 loadMap: map[string]*Host{}, 65 replicationFactor: replicationFactor, 66 } 67 } 68 69 func (c *Consistent) Add(server string) { 70 c.lock.Lock() 71 defer c.lock.Unlock() 72 73 if _, ok := c.loadMap[server]; ok { 74 return 75 } 76 77 c.loadMap[server] = &Host{Name: server, Load: 0} 78 for i := 0; i < c.replicationFactor; i++ { 79 h := c.hash(fmt.Sprintf("%s%d", server, i)) 80 c.servers[h] = server 81 c.clients.ReplaceOrInsert(item{h}) 82 } 83 } 84 85 // Get returns the server that owns the given client. 86 // As described in https://en.wikipedia.org/wiki/Consistent_hashing 87 // It returns ErrNoHosts if the ring has no servers in it. 88 func (c *Consistent) Get(client string) (string, error) { 89 c.lock.RLock() 90 defer c.lock.RUnlock() 91 92 if c.clients.Len() == 0 { 93 return "", ErrNoHosts 94 } 95 96 h := c.hash(client) 97 var foundItem btree.Item 98 c.clients.AscendGreaterOrEqual(item{h}, func(i btree.Item) bool { 99 foundItem = i 100 return false // stop the iteration 101 }) 102 103 if foundItem == nil { 104 // If no host found, wrap around to the first one. 105 foundItem = c.clients.Min() 106 } 107 108 host := c.servers[foundItem.(item).value] 109 110 return host, nil 111 } 112 113 // GetLeast returns the least loaded host that can serve the key. 114 // It uses Consistent Hashing With Bounded loads. 115 // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 116 // It returns ErrNoHosts if the ring has no hosts in it. 117 func (c *Consistent) GetLeast(client string) (string, error) { 118 c.lock.RLock() 119 defer c.lock.RUnlock() 120 121 if c.clients.Len() == 0 { 122 return "", ErrNoHosts 123 } 124 h := c.hash(client) 125 for { 126 var foundItem btree.Item 127 c.clients.AscendGreaterOrEqual(item{h}, func(bItem btree.Item) bool { 128 if h != bItem.(item).value { 129 foundItem = bItem 130 return false // stop the iteration 131 } 132 return true 133 }) 134 135 if foundItem == nil { 136 // If no host found, wrap around to the first one. 137 foundItem = c.clients.Min() 138 } 139 key := c.clients.Get(foundItem) 140 if key == nil { 141 return client, nil 142 } 143 host := c.servers[key.(item).value] 144 if c.loadOK(host) { 145 return host, nil 146 } 147 h = key.(item).value 148 } 149 } 150 151 // Sets the load of `server` to the given `load` 152 func (c *Consistent) UpdateLoad(server string, load int64) { 153 c.lock.Lock() 154 defer c.lock.Unlock() 155 156 if _, ok := c.loadMap[server]; !ok { 157 return 158 } 159 c.totalLoad -= c.loadMap[server].Load 160 c.loadMap[server].Load = load 161 c.totalLoad += load 162 } 163 164 // Increments the load of host by 1 165 // 166 // should only be used with if you obtained a host with GetLeast 167 func (c *Consistent) Inc(server string) { 168 c.lock.Lock() 169 defer c.lock.Unlock() 170 171 if _, ok := c.loadMap[server]; !ok { 172 return 173 } 174 atomic.AddInt64(&c.loadMap[server].Load, 1) 175 atomic.AddInt64(&c.totalLoad, 1) 176 } 177 178 // Decrements the load of host by 1 179 // 180 // should only be used with if you obtained a host with GetLeast 181 func (c *Consistent) Done(server string) { 182 c.lock.Lock() 183 defer c.lock.Unlock() 184 185 if _, ok := c.loadMap[server]; !ok { 186 return 187 } 188 atomic.AddInt64(&c.loadMap[server].Load, -1) 189 atomic.AddInt64(&c.totalLoad, -1) 190 } 191 192 // Deletes host from the ring 193 func (c *Consistent) Remove(server string) bool { 194 c.lock.Lock() 195 defer c.lock.Unlock() 196 197 for i := 0; i < c.replicationFactor; i++ { 198 h := c.hash(fmt.Sprintf("%s%d", server, i)) 199 delete(c.servers, h) 200 c.delSlice(h) 201 } 202 delete(c.loadMap, server) 203 return true 204 } 205 206 // Return the list of servers in the ring 207 func (c *Consistent) Servers() (servers []string) { 208 c.lock.RLock() 209 defer c.lock.RUnlock() 210 for k := range c.loadMap { 211 servers = append(servers, k) 212 } 213 return servers 214 } 215 216 // Returns the loads of all the hosts 217 func (c *Consistent) GetLoads() map[string]int64 { 218 loads := map[string]int64{} 219 220 for k, v := range c.loadMap { 221 loads[k] = v.Load 222 } 223 return loads 224 } 225 226 // Returns the maximum load of the single host 227 // which is: 228 // (total_load/number_of_hosts)*1.25 229 // total_load = is the total number of active requests served by hosts 230 // for more info: 231 // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 232 func (c *Consistent) MaxLoad() int64 { 233 if c.totalLoad == 0 { 234 c.totalLoad = 1 235 } 236 var avgLoadPerNode float64 237 avgLoadPerNode = float64(c.totalLoad / int64(len(c.loadMap))) 238 if avgLoadPerNode == 0 { 239 avgLoadPerNode = 1 240 } 241 avgLoadPerNode = math.Ceil(avgLoadPerNode * OptimalExtraCapacityFactor) 242 return int64(avgLoadPerNode) 243 } 244 245 func (c *Consistent) loadOK(server string) bool { 246 // a safety check if someone performed c.Done more than needed 247 if c.totalLoad < 0 { 248 c.totalLoad = 0 249 } 250 251 var avgLoadPerNode float64 252 avgLoadPerNode = float64((c.totalLoad + 1) / int64(len(c.loadMap))) 253 if avgLoadPerNode == 0 { 254 avgLoadPerNode = 1 255 } 256 avgLoadPerNode = math.Ceil(avgLoadPerNode * 1.25) 257 258 bserver, ok := c.loadMap[server] 259 if !ok { 260 panic(fmt.Sprintf("given host(%s) not in loadsMap", bserver.Name)) 261 } 262 263 return float64(bserver.Load) < avgLoadPerNode 264 } 265 266 func (c *Consistent) delSlice(val uint64) { 267 c.clients.Delete(item{val}) 268 } 269 270 func (c *Consistent) hash(key string) uint64 { 271 out := blake2b.Sum512([]byte(key)) 272 return binary.LittleEndian.Uint64(out[:]) 273 }