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  }