├── .gitignore ├── LICENSE ├── README.md ├── bounded ├── bounded.go ├── bounded_test.go └── example_bounded_test.go ├── consistent ├── consistent.go ├── consistent_test.go └── example_consistent_test.go ├── liblb.go ├── murmur └── murmur.go ├── p2c ├── README.md ├── example_p2c_test.go ├── p2c.go └── p2c_test.go ├── r2 ├── README.md ├── example_r2_test.go ├── r2.go └── r2_test.go └── vendor └── github.com └── lafikl └── consistent ├── .gitignore ├── LICENSE ├── README.md ├── consistent.go ├── consistent_test.go └── example_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Khalid Lafi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # liblb 2 | A golang library that implements load balancing algorithms. Still a work in progress! 3 | 4 | https://godoc.org/github.com/lafikl/liblb 5 | -------------------------------------------------------------------------------- /bounded/bounded.go: -------------------------------------------------------------------------------- 1 | // Bounded is Consistent hashing with bounded loads. 2 | // It acheives that by adding a capacity counter on every host, 3 | // and when a host gets picked it, checks its capacity to see if it's below 4 | // the Average Load per Host. 5 | // 6 | // All opertaions in bounded are concurrency-safe. 7 | // 8 | // Average Load Per Host is defined as follows: 9 | // 10 | // (totalLoad/number_of_hosts)*imbalance_constant 11 | // 12 | // totalLoad = sum of all hosts load 13 | // 14 | // load = the number of active requests 15 | // 16 | // imbalance_constant = is the imbalance constant, which is 1.25 in our case 17 | // 18 | // it bounds the load imabalnce to be at most 25% more than (totalLoad/number_of_hosts). 19 | // 20 | // 21 | // For more info: 22 | // https://medium.com/vimeo-engineering-blog/improving-load-balancing-with-a-new-consistent-hashing-algorithm-9f1bd75709ed 23 | // 24 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 25 | package bounded 26 | 27 | import ( 28 | "github.com/lafikl/consistent" 29 | "github.com/lafikl/liblb" 30 | ) 31 | 32 | type bhost struct { 33 | load uint64 34 | weight int 35 | } 36 | 37 | type Bounded struct { 38 | ch *consistent.Consistent 39 | } 40 | 41 | func New(hosts ...string) *Bounded { 42 | c := &Bounded{ 43 | ch: consistent.New(), 44 | } 45 | for _, h := range hosts { 46 | c.Add(h) 47 | } 48 | return c 49 | } 50 | 51 | func (b *Bounded) Add(host string) { 52 | b.ch.Add(host) 53 | } 54 | 55 | func (b *Bounded) Remove(host string) { 56 | b.ch.Remove(host) 57 | } 58 | 59 | // err can be liblb.ErrNoHost if there's no added hosts. 60 | func (b *Bounded) Balance(key string) (host string, err error) { 61 | if len(b.ch.Hosts()) == 0 { 62 | return "", liblb.ErrNoHost 63 | } 64 | 65 | host, err = b.ch.GetLeast(key) 66 | return 67 | } 68 | 69 | // It should be called once a request is assigned to a host, 70 | // obtained from b.Balance. 71 | func (b *Bounded) Inc(host string) { 72 | b.ch.Inc(host) 73 | } 74 | 75 | // should be called when an assigned request to host is finished. 76 | func (b *Bounded) Done(host string) { 77 | b.ch.Done(host) 78 | } 79 | 80 | func (b *Bounded) Loads() map[string]int64 { 81 | return b.ch.GetLoads() 82 | } 83 | 84 | // Max load of a host is (Average Load Per Host*1.25) 85 | func (b *Bounded) MaxLoad() int64 { 86 | return b.ch.MaxLoad() 87 | } 88 | -------------------------------------------------------------------------------- /bounded/bounded_test.go: -------------------------------------------------------------------------------- 1 | package bounded 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNewConsistentBounded(t *testing.T) { 9 | lb := New("127.0.0.1", "192.0.0.1", "88.0.0.1", "10.0.0.1") 10 | 11 | for i := 0; i < 1000; i++ { 12 | host, err := lb.Balance(fmt.Sprintf("hello world %d", i)) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | lb.Inc(host) 17 | } 18 | 19 | loads := lb.Loads() 20 | for k, load := range loads { 21 | if load > lb.MaxLoad() { 22 | t.Fatal(fmt.Sprintf("%s load(%d) > MaxLoad(%d)", k, 23 | load, lb.MaxLoad())) 24 | } 25 | } 26 | for k, load := range loads { 27 | fmt.Printf("%s load(%d) > MaxLoad(%d)\n", k, 28 | load, lb.MaxLoad()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bounded/example_bounded_test.go: -------------------------------------------------------------------------------- 1 | package bounded_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/lafikl/liblb/bounded" 9 | ) 10 | 11 | func Example(t *testing.T) { 12 | hosts := []string{"127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007"} 13 | 14 | lb := bounded.New(hosts...) 15 | // Host load will never exceed (number_of_requests/len(hosts)) by more than 25% 16 | // in this case: 17 | // any host load would be at most: 18 | // ceil((10/3) * 1.25) 19 | for i := 0; i < 10; i++ { 20 | host, err := lb.Balance("hello world") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | // do work for "host" 25 | fmt.Printf("Send request #%d to host %s\n", i, host) 26 | // when the work assign to the host is done 27 | lb.Done(host) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /consistent/consistent.go: -------------------------------------------------------------------------------- 1 | // Consistent uses consistent hashing algorithm to assign work to hosts. 2 | // Its best for the cases when you need affinty, and your hosts come and go. 3 | // When removing a host it gaurantees that only 1/n of items gets reshuffled 4 | // where n is the number of servers. 5 | // 6 | // One of the issues with Consistent Hashing is load imbalance 7 | // when you have hot keys that goes to a single server, 8 | // it's mitigated by using virtual nodes, 9 | // which basically means when adding a host we add n - 20 in our case - replicas of that host. 10 | // 11 | // Beware that Consistent Hashing doesn't provide, 12 | // an upper bound for the load of a host. 13 | // 14 | // If you need such gaurantees see package liblb/bounded. 15 | // 16 | // https://en.wikipedia.org/wiki/Consistent_hashing 17 | package consistent 18 | 19 | import ( 20 | "github.com/lafikl/consistent" 21 | "github.com/lafikl/liblb" 22 | ) 23 | 24 | type Consistent struct { 25 | ch *consistent.Consistent 26 | } 27 | 28 | func New(hosts ...string) *Consistent { 29 | c := &Consistent{ch: consistent.New()} 30 | for _, h := range hosts { 31 | c.ch.Add(h) 32 | } 33 | return c 34 | } 35 | 36 | func (c *Consistent) Add(host string) { 37 | c.ch.Add(host) 38 | } 39 | 40 | func (c *Consistent) Remove(host string) { 41 | c.ch.Remove(host) 42 | } 43 | 44 | func (h *Consistent) Balance(key string) (host string, err error) { 45 | host, err = h.ch.Get(key) 46 | if err != nil { 47 | return "", liblb.ErrNoHost 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /consistent/consistent_test.go: -------------------------------------------------------------------------------- 1 | package consistent 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | func TestNewConsistent(t *testing.T) { 9 | hosts := []string{"127.0.0.1", "94.0.0.1", "88.0.0.1"} 10 | lb := New(hosts...) 11 | loads := map[string]int{} 12 | 13 | for i := 0; i < 100; i++ { 14 | host, err := lb.Balance("hello, world!") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | h, _ := loads[host] 19 | loads[host] = h + 1 20 | } 21 | 22 | // make sure that all requests got to a single host 23 | if len(loads) > 1 { 24 | t.Fatalf("load is not consistent %s\n", loads) 25 | } 26 | 27 | log.Println(loads) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /consistent/example_consistent_test.go: -------------------------------------------------------------------------------- 1 | package consistent_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/lafikl/liblb/consistent" 9 | ) 10 | 11 | func Example(t *testing.T) { 12 | hosts := []string{"127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007"} 13 | 14 | lb := consistent.New(hosts...) 15 | for i := 0; i < 10; i++ { 16 | host, err := lb.Balance(fmt.Sprintf("hello world %d", i)) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | fmt.Printf("Send request #%d to host %s\n", i, host) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /liblb.go: -------------------------------------------------------------------------------- 1 | package liblb 2 | 3 | import "errors" 4 | 5 | type Balancer interface { 6 | New(hosts ...string) 7 | Add(host string) 8 | Remove(host string) 9 | Balance() (string, error) 10 | } 11 | 12 | type KeyedBalancer interface { 13 | New(hosts ...string) 14 | Add(host string) 15 | Remove(host string) 16 | Balance(key string) (string, error) 17 | } 18 | 19 | var ErrNoHost = errors.New("host not found") 20 | -------------------------------------------------------------------------------- /murmur/murmur.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/huichen/murmur 2 | // 3 | // Murmur3 32bit hash function based on 4 | // http://en.wikipedia.org/wiki/MurmurHash 5 | 6 | package murmur 7 | 8 | const ( 9 | c1 = 0xcc9e2d51 10 | c2 = 0x1b873593 11 | c3 = 0x85ebca6b 12 | c4 = 0xc2b2ae35 13 | r1 = 15 14 | r2 = 13 15 | m = 5 16 | n = 0xe6546b64 17 | ) 18 | 19 | var ( 20 | seed = uint32(1) 21 | ) 22 | 23 | func Murmur3(key []byte) (hash uint32) { 24 | hash = seed 25 | iByte := 0 26 | for ; iByte+4 <= len(key); iByte += 4 { 27 | k := uint32(key[iByte]) | uint32(key[iByte+1])<<8 | uint32(key[iByte+2])<<16 | uint32(key[iByte+3])<<24 28 | k *= c1 29 | k = (k << r1) | (k >> (32 - r1)) 30 | k *= c2 31 | hash ^= k 32 | hash = (hash << r2) | (hash >> (32 - r2)) 33 | hash = hash*m + n 34 | } 35 | 36 | var remainingBytes uint32 37 | switch len(key) - iByte { 38 | case 3: 39 | remainingBytes += uint32(key[iByte+2]) << 16 40 | fallthrough 41 | case 2: 42 | remainingBytes += uint32(key[iByte+1]) << 8 43 | fallthrough 44 | case 1: 45 | remainingBytes += uint32(key[iByte]) 46 | remainingBytes *= c1 47 | remainingBytes = (remainingBytes << r1) | (remainingBytes >> (32 - r1)) 48 | remainingBytes = remainingBytes * c2 49 | hash ^= remainingBytes 50 | } 51 | 52 | hash ^= uint32(len(key)) 53 | hash ^= hash >> 16 54 | hash *= c3 55 | hash ^= hash >> 13 56 | hash *= c4 57 | hash ^= hash >> 16 58 | 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /p2c/README.md: -------------------------------------------------------------------------------- 1 | # P2C Balancer 2 | TODO 3 | 4 | 5 | ### Usage Example: 6 | ```go 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "testing" 13 | 14 | "github.com/lafikl/liblb/r2" 15 | ) 16 | 17 | func main() { 18 | lb := p2c.New("127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007") 19 | for i := 0; i < 10; i++ { 20 | host, err := lb.Balance("") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | fmt.Printf("Send request #%d to host %s\n", i, host) 25 | } 26 | } 27 | ``` 28 | 29 | # P2C Balancer 30 | TODO 31 | 32 | 33 | ### Usage Example: 34 | ```go 35 | package main 36 | 37 | import ( 38 | "fmt" 39 | "log" 40 | "testing" 41 | 42 | "github.com/lafikl/liblb/r2" 43 | ) 44 | 45 | func main() { 46 | lb := p2c.New("127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007") 47 | for i := 0; i < 10; i++ { 48 | host, err := lb.Balance("") 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | fmt.Printf("Send request #%d to host %s\n", i, host) 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /p2c/example_p2c_test.go: -------------------------------------------------------------------------------- 1 | package p2c_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/lafikl/liblb/p2c" 9 | ) 10 | 11 | func Example(t *testing.T) { 12 | hosts := []string{"127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007"} 13 | 14 | // Power of Two choices example 15 | lb := p2c.New(hosts...) 16 | for i := 0; i < 10; i++ { 17 | // uses random power of two choices, because the key length == 0 18 | host, err := lb.Balance("") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | // load should be around 33% per host 23 | fmt.Printf("Send request #%d to host %s\n", i, host) 24 | // when the work assign to the host is done 25 | lb.Done(host) 26 | } 27 | 28 | // Partial Key Grouping example 29 | pp2c := p2c.New(hosts...) 30 | for i := 0; i < 10; i++ { 31 | // uses PKG because the key length is > 0 32 | host, err := pp2c.Balance("hello world") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | // traffic should be split between two nodes only 38 | fmt.Printf("Send request #%d to host %s\n", i, host) 39 | // when the work assign to the host is done 40 | pp2c.Done(host) 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /p2c/p2c.go: -------------------------------------------------------------------------------- 1 | // P2C will distribute the traffic by choosing two hosts either via hashing or randomly 2 | // and then pick the least loaded of the two. 3 | // It gaurantees that the max load of a server is ln(ln(n)), 4 | // where n is the number of servers. 5 | // 6 | // All operations in P2C are concurrency-safe. 7 | // 8 | // 9 | // For more info: 10 | // https://brooker.co.za/blog/2012/01/17/two-random.html 11 | // 12 | // http://www.eecs.harvard.edu/~michaelm/postscripts/handbook2001.pdf 13 | // 14 | package p2c 15 | 16 | import ( 17 | "hash/fnv" 18 | "math/rand" 19 | "sync" 20 | "time" 21 | 22 | "github.com/lafikl/liblb" 23 | "github.com/lafikl/liblb/murmur" 24 | ) 25 | 26 | type host struct { 27 | name string 28 | load uint64 29 | } 30 | 31 | type P2C struct { 32 | hosts []*host 33 | rndm *rand.Rand 34 | loadMap map[string]*host 35 | 36 | sync.Mutex 37 | } 38 | 39 | // New returns a new instance of RandomTwoBalancer 40 | func New(hosts ...string) *P2C { 41 | p := &P2C{ 42 | hosts: []*host{}, 43 | loadMap: map[string]*host{}, 44 | rndm: rand.New(rand.NewSource(time.Now().UnixNano())), 45 | } 46 | 47 | for _, h := range hosts { 48 | p.Add(h) 49 | } 50 | 51 | return p 52 | } 53 | 54 | func (p *P2C) Add(hostName string) { 55 | p.Lock() 56 | defer p.Unlock() 57 | 58 | h := &host{name: hostName, load: 0} 59 | p.hosts = append(p.hosts, h) 60 | p.loadMap[hostName] = h 61 | } 62 | 63 | func (p *P2C) Remove(host string) { 64 | p.Lock() 65 | defer p.Unlock() 66 | 67 | _, ok := p.loadMap[host] 68 | if !ok { 69 | return 70 | } 71 | 72 | delete(p.loadMap, host) 73 | 74 | for i, v := range p.hosts { 75 | if v.name == host { 76 | p.hosts = append(p.hosts[:i], p.hosts[i+1:]...) 77 | } 78 | } 79 | } 80 | 81 | func (p *P2C) hash(key string) (string, string) { 82 | h := fnv.New32() 83 | h.Write([]byte(key)) 84 | 85 | n1 := p.hosts[int(h.Sum32())%len(p.hosts)].name 86 | n2 := p.hosts[int(murmur.Murmur3([]byte(key)))%len(p.hosts)].name 87 | 88 | return n1, n2 89 | 90 | } 91 | 92 | // Balance picks two servers either randomly (if no key supplied), or via 93 | // hashing (PKG) if given a key, then it returns the least loaded one between the two. 94 | // 95 | // Partial Key Grouping (PKG) is great for skewed data workloads, which also needs to be 96 | // determinstic in the way of choosing which servers to send requests too. 97 | // https://arxiv.org/pdf/1510.07623.pdf 98 | // the maximum load of a server in PKG at anytime is: 99 | // `max_load-avg_load` 100 | func (p *P2C) Balance(key string) (string, error) { 101 | p.Lock() 102 | defer p.Unlock() 103 | 104 | if len(p.hosts) == 0 { 105 | return "", liblb.ErrNoHost 106 | } 107 | 108 | // chosen host 109 | var host string 110 | 111 | var n1, n2 string 112 | 113 | if len(key) > 0 { 114 | n1, n2 = p.hash(key) 115 | } else { 116 | n1 = p.hosts[p.rndm.Intn(len(p.hosts))].name 117 | n2 = p.hosts[p.rndm.Intn(len(p.hosts))].name 118 | } 119 | 120 | host = n2 121 | 122 | if p.loadMap[n1].load <= p.loadMap[n2].load { 123 | host = n1 124 | } 125 | 126 | p.loadMap[host].load++ 127 | return host, nil 128 | } 129 | 130 | // Decrments the load of the host (if found) by 1 131 | func (p *P2C) Done(host string) { 132 | p.Lock() 133 | defer p.Unlock() 134 | 135 | h, ok := p.loadMap[host] 136 | if !ok { 137 | return 138 | } 139 | if h.load > 0 { 140 | h.load-- 141 | } 142 | } 143 | 144 | // UpdateLoad updates the load of a host 145 | func (p *P2C) UpdateLoad(host string, load uint64) error { 146 | p.Lock() 147 | defer p.Unlock() 148 | 149 | h, ok := p.loadMap[host] 150 | if !ok { 151 | return liblb.ErrNoHost 152 | } 153 | h.load = load 154 | return nil 155 | } 156 | 157 | // Returns the current load of the server, 158 | // or it returns liblb.ErrNoHost if the host doesn't exist. 159 | func (p *P2C) GetLoad(host string) (load uint64, err error) { 160 | p.Lock() 161 | defer p.Unlock() 162 | 163 | h, ok := p.loadMap[host] 164 | if !ok { 165 | return 0, liblb.ErrNoHost 166 | } 167 | return h.load, nil 168 | } 169 | -------------------------------------------------------------------------------- /p2c/p2c_test.go: -------------------------------------------------------------------------------- 1 | package p2c 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "testing" 8 | ) 9 | 10 | func TestNewP2C(t *testing.T) { 11 | var hosts = []string{ 12 | "127.0.0.1", 13 | "225.0.0.1", 14 | "10.0.0.1", 15 | "28.0.0.1", 16 | "88.0.0.1", 17 | } 18 | upperVariance := 1 - math.Log(math.Log(float64(len(hosts)))) 19 | 20 | lb := New() 21 | 22 | for _, host := range hosts { 23 | lb.Add(host) 24 | } 25 | 26 | for i := 0; i < 200; i++ { 27 | lb.Balance("") 28 | } 29 | 30 | for _, host := range hosts { 31 | val, err := lb.GetLoad(host) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | floatVal := float64(val) 36 | fmt.Printf("%s=%d\n", host, val) 37 | 38 | // check for load variance 39 | for _, otherHost := range hosts { 40 | oval, err := lb.GetLoad(otherHost) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | floatOval := float64(oval) 45 | 46 | variance := floatVal / (floatVal + floatOval) 47 | if variance > upperVariance { 48 | t.Fatalf("variance between (%s, %s) is %.2f > %.2f\n", 49 | host, otherHost, variance, upperVariance) 50 | } else { 51 | fmt.Printf("variance between (%s, %s) is %.2f and upper is %.2f\n", 52 | host, otherHost, variance, upperVariance) 53 | } 54 | } 55 | } 56 | } 57 | 58 | func TestNewHP2C(t *testing.T) { 59 | var hosts = []string{ 60 | "127.0.0.1", 61 | "225.0.0.1", 62 | "10.0.0.1", 63 | "28.0.0.1", 64 | "88.0.0.1", 65 | } 66 | 67 | lb := New() 68 | 69 | for _, host := range hosts { 70 | lb.Add(host) 71 | } 72 | 73 | for i := 0; i < 200; i++ { 74 | for j := 0; j < 100; j++ { 75 | lb.Balance("hello, world!") 76 | } 77 | } 78 | 79 | for _, host := range hosts { 80 | val, err := lb.GetLoad(host) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | fmt.Printf("%s=%d\n", host, val) 85 | 86 | } 87 | } 88 | 89 | func TestLongestRun(t *testing.T) { 90 | var hosts = []string{ 91 | "127.0.0.1", 92 | "225.0.0.1", 93 | "10.0.0.1", 94 | "28.0.0.1", 95 | "88.0.0.1", 96 | } 97 | lb := New(hosts...) 98 | 99 | longestRun := map[string]int{} 100 | 101 | currentHost := "" 102 | currentCount := 0 103 | for i := 0; i < 1000; i++ { 104 | // host, err := lb.Balance(fmt.Sprintf("hello, world !", i)) 105 | host, err := lb.Balance("hello, world!") 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | if currentHost != host { 110 | lrun, _ := longestRun[currentHost] 111 | if lrun < currentCount { 112 | longestRun[currentHost] = currentCount 113 | } 114 | currentHost = host 115 | currentCount = 1 116 | continue 117 | } 118 | currentCount++ 119 | } 120 | 121 | log.Println(longestRun) 122 | 123 | for _, host := range hosts { 124 | val, err := lb.GetLoad(host) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | fmt.Printf("%s=%d\n", host, val) 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /r2/README.md: -------------------------------------------------------------------------------- 1 | # Round Robin Balancer 2 | It's one of the simplest if not the simplest load balancing algorithm. 3 | It distribute requests by walking the list of servers and assigning a request to each server in turn. 4 | On the downside Round-Robin assumes that all servers are alike, 5 | and that all requests take the same amount of time, which is obviously not true in practice. 6 | 7 | 8 | ### Usage Example: 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "log" 15 | "testing" 16 | 17 | "github.com/lafikl/liblb/r2" 18 | ) 19 | 20 | func main() { 21 | lb := r2.New("127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007") 22 | for i := 0; i < 10; i++ { 23 | host, err := lb.Balance() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | fmt.Printf("Send request #%d to host %s\n", i, host) 28 | } 29 | } 30 | ``` 31 | 32 | 33 | ## Weighted Round Robin 34 | A variant of Round-Robin that assigns a weight for every host, the weight affects the number of requests that gets sent to the server. 35 | Assume that we have two hosts `A` with weight **1** and `B` with weight **4**, 36 | that means for every single request we send to `A` we send 4 requests to `B`. 37 | In other words, 80% of the requests would go to `B`. 38 | Which you can calculate by yourself applying this formula `host_weight/total_weights`. 39 | 40 | 41 | ### Usage Example: 42 | ```go 43 | package main 44 | 45 | import ( 46 | "fmt" 47 | "log" 48 | "testing" 49 | 50 | "github.com/lafikl/liblb/r2" 51 | ) 52 | 53 | func main() { 54 | // default weight is 1 55 | lb := r2.New("127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007") 56 | // host_weight/total_weights 57 | // this hosts load would be 3/(3+3)=0.5 58 | // meaning that 50% of the requests would go to 127.0.0.1:9000 59 | lb.AddWeight("127.0.0.1:9000", 3) 60 | for i := 0; i < 10; i++ { 61 | host, err := lb.Balance() 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | fmt.Printf("Send request #%d to host %s\n", i, host) 66 | } 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /r2/example_r2_test.go: -------------------------------------------------------------------------------- 1 | package r2_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/lafikl/liblb/r2" 9 | ) 10 | 11 | func Example(t *testing.T) { 12 | lb := r2.New("127.0.0.1:8009", "127.0.0.1:8008", "127.0.0.1:8007") 13 | for i := 0; i < 10; i++ { 14 | host, err := lb.Balance() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | fmt.Printf("Send request #%d to host %s\n", i, host) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /r2/r2.go: -------------------------------------------------------------------------------- 1 | // R2 is a concurrency-safe Round-Robin Balancer. 2 | // Which also supports Weighted Round-Robin. 3 | // 4 | // Round-Robin is a simple and well known algorithm for load balancing. 5 | // 6 | // Here's a simple implmentation of it, if you're not already familiar with it 7 | // 8 | // https://play.golang.org/p/XCMAtKGCaE 9 | package r2 10 | 11 | import ( 12 | "sync" 13 | 14 | "github.com/lafikl/liblb" 15 | ) 16 | 17 | type R2 struct { 18 | i int 19 | hosts []string 20 | 21 | sync.Mutex 22 | } 23 | 24 | func New(hosts ...string) *R2 { 25 | return &R2{i: 0, hosts: hosts} 26 | } 27 | 28 | // Adds a host to the list of hosts, with the weight of the host being 1. 29 | func (rb *R2) Add(host string) { 30 | rb.Lock() 31 | defer rb.Unlock() 32 | 33 | for _, h := range rb.hosts { 34 | if h == host { 35 | return 36 | } 37 | } 38 | rb.hosts = append(rb.hosts, host) 39 | } 40 | 41 | // Weight increases the percentage of requests that get sent to the host 42 | // Which can be calculated as `weight/(total_weights+weight)`. 43 | func (rb *R2) AddWeight(host string, weight int) { 44 | rb.Lock() 45 | defer rb.Unlock() 46 | 47 | for _, h := range rb.hosts { 48 | if h == host { 49 | return 50 | } 51 | } 52 | 53 | for i := 0; i < weight; i++ { 54 | rb.hosts = append(rb.hosts, host) 55 | } 56 | 57 | } 58 | 59 | // Check if host already exist 60 | func (rb *R2) Exists(host string) bool { 61 | rb.Lock() 62 | defer rb.Unlock() 63 | 64 | for _, h := range rb.hosts { 65 | if h == host { 66 | return true 67 | } 68 | } 69 | 70 | return false 71 | } 72 | 73 | func (rb *R2) Remove(host string) { 74 | rb.Lock() 75 | defer rb.Unlock() 76 | 77 | for i, h := range rb.hosts { 78 | if host == h { 79 | rb.hosts = append(rb.hosts[:i], rb.hosts[i+1:]...) 80 | } 81 | } 82 | } 83 | 84 | func (rb *R2) Balance() (string, error) { 85 | rb.Lock() 86 | defer rb.Unlock() 87 | 88 | if len(rb.hosts) == 0 { 89 | return "", liblb.ErrNoHost 90 | } 91 | 92 | host := rb.hosts[rb.i%len(rb.hosts)] 93 | rb.i++ 94 | 95 | return host, nil 96 | } 97 | -------------------------------------------------------------------------------- /r2/r2_test.go: -------------------------------------------------------------------------------- 1 | package r2 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | func TestNewR2(t *testing.T) { 9 | hosts := []string{"127.0.0.1", "94.0.0.1", "88.0.0.1"} 10 | reqPerHost := 100 11 | 12 | lb := New(hosts...) 13 | loads := map[string]uint64{} 14 | 15 | for i := 0; i < reqPerHost*len(hosts); i++ { 16 | host, _ := lb.Balance() 17 | 18 | l, _ := loads[host] 19 | loads[host] = l + 1 20 | } 21 | for h, load := range loads { 22 | if load > uint64(reqPerHost) { 23 | t.Fatalf("host(%s) got overloaded %d>%d\n", h, load, reqPerHost) 24 | } 25 | } 26 | log.Println(loads) 27 | } 28 | 29 | func TestWeightedR2(t *testing.T) { 30 | hosts := []string{"127.0.0.1", "94.0.0.1", "88.0.0.1"} 31 | reqPerHost := 100 32 | 33 | lb := New() 34 | 35 | // in reverse order just to make sure 36 | // that insetion order of hosts doesn't affect anything 37 | for i := len(hosts); i > 0; i-- { 38 | lb.AddWeight(hosts[i-1], i) 39 | } 40 | 41 | loads := map[string]uint64{} 42 | 43 | for i := 0; i < reqPerHost*len(hosts); i++ { 44 | host, _ := lb.Balance() 45 | 46 | l, _ := loads[host] 47 | loads[host] = l + 1 48 | } 49 | 50 | for i, host := range hosts { 51 | if loads[host] > uint64(reqPerHost*(i+1)) { 52 | t.Fatalf("host(%s) got overloaded %d>%d\n", host, loads[host], reqPerHost*i) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vendor/github.com/lafikl/consistent/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /vendor/github.com/lafikl/consistent/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Khalid Lafi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/lafikl/consistent/README.md: -------------------------------------------------------------------------------- 1 | # Package consistent 2 | A Golang implementation of Consistent Hashing and 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 | 8 | 9 | ### Consistent Hashing Example 10 | 11 | ```go 12 | 13 | package main 14 | 15 | import ( 16 | "log" 17 | "github.com/lafikl/consistent" 18 | ) 19 | 20 | func main() { 21 | c := consistent.New() 22 | 23 | // adds the hosts to the ring 24 | c.Add("127.0.0.1:8000") 25 | c.Add("92.0.0.1:8000") 26 | 27 | // Returns the host that owns `key`. 28 | // 29 | // As described in https://en.wikipedia.org/wiki/Consistent_hashing 30 | // 31 | // It returns ErrNoHosts if the ring has no hosts in it. 32 | host, err := c.Get("/app.html") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | log.Println(host) 38 | } 39 | 40 | ``` 41 | 42 | 43 | ### Consistent Hashing With Bounded Loads Example 44 | 45 | ```go 46 | 47 | package main 48 | 49 | import ( 50 | "log" 51 | "github.com/lafikl/consistent" 52 | ) 53 | 54 | func main() { 55 | c := consistent.New() 56 | 57 | // adds the hosts to the ring 58 | c.Add("127.0.0.1:8000") 59 | c.Add("92.0.0.1:8000") 60 | 61 | // It uses Consistent Hashing With Bounded loads 62 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 63 | // to pick the least loaded host that can serve the key 64 | // 65 | // It returns ErrNoHosts if the ring has no hosts in it. 66 | // 67 | host, err := c.GetLeast("/app.html") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | // increases the load of `host`, we have to call it before sending the request 73 | c.Inc(host) 74 | 75 | // send request or do whatever 76 | log.Println("send request to", host) 77 | 78 | // call it when the work is done, to update the load of `host`. 79 | c.Done(host) 80 | 81 | } 82 | 83 | ``` 84 | 85 | 86 | ## Docs 87 | 88 | https://godoc.org/github.com/lafikl/consistent 89 | 90 | 91 | 92 | # License 93 | 94 | ``` 95 | MIT License 96 | 97 | Copyright (c) 2017 Khalid Lafi 98 | 99 | Permission is hereby granted, free of charge, to any person obtaining a copy 100 | of this software and associated documentation files (the "Software"), to deal 101 | in the Software without restriction, including without limitation the rights 102 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 103 | copies of the Software, and to permit persons to whom the Software is 104 | furnished to do so, subject to the following conditions: 105 | 106 | The above copyright notice and this permission notice shall be included in all 107 | copies or substantial portions of the Software. 108 | 109 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 110 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 111 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 112 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 113 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 114 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 115 | SOFTWARE. 116 | 117 | ``` 118 | -------------------------------------------------------------------------------- /vendor/github.com/lafikl/consistent/consistent.go: -------------------------------------------------------------------------------- 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 | "errors" 11 | "fmt" 12 | "hash/fnv" 13 | "math" 14 | "sort" 15 | "sync" 16 | "sync/atomic" 17 | ) 18 | 19 | const replicationFactor = 10 20 | 21 | var ErrNoHosts = errors.New("no hosts added") 22 | 23 | type Host struct { 24 | Name string 25 | Load int64 26 | } 27 | 28 | type Consistent struct { 29 | hosts map[uint64]string 30 | sortedSet []uint64 31 | loadMap map[string]*Host 32 | totalLoad int64 33 | 34 | sync.RWMutex 35 | } 36 | 37 | func New() *Consistent { 38 | return &Consistent{ 39 | hosts: map[uint64]string{}, 40 | sortedSet: []uint64{}, 41 | loadMap: map[string]*Host{}, 42 | } 43 | } 44 | 45 | func (c *Consistent) Add(host string) { 46 | c.Lock() 47 | defer c.Unlock() 48 | 49 | h := c.hash(fmt.Sprintf("%s%d", host, 0)) 50 | if _, ok := c.hosts[h]; ok { 51 | return 52 | } 53 | 54 | c.loadMap[host] = &Host{Name: host, Load: 0} 55 | for i := 0; i < replicationFactor; i++ { 56 | h := c.hash(fmt.Sprintf("%s%d", host, i)) 57 | c.hosts[h] = host 58 | c.sortedSet = append(c.sortedSet, h) 59 | 60 | } 61 | // sort hashes ascendingly 62 | sort.Slice(c.sortedSet, func(i int, j int) bool { 63 | if c.sortedSet[i] < c.sortedSet[j] { 64 | return true 65 | } 66 | return false 67 | }) 68 | } 69 | 70 | // Returns the host that owns `key`. 71 | // 72 | // As described in https://en.wikipedia.org/wiki/Consistent_hashing 73 | // 74 | // It returns ErrNoHosts if the ring has no hosts in it. 75 | func (c *Consistent) Get(key string) (string, error) { 76 | c.RLock() 77 | defer c.RUnlock() 78 | 79 | if len(c.hosts) == 0 { 80 | return "", ErrNoHosts 81 | } 82 | 83 | h := c.hash(key) 84 | idx := c.search(h) 85 | return c.hosts[c.sortedSet[idx]], nil 86 | } 87 | 88 | // It uses Consistent Hashing With Bounded loads 89 | // 90 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 91 | // 92 | // to pick the least loaded host that can serve the key 93 | // 94 | // It returns ErrNoHosts if the ring has no hosts in it. 95 | // 96 | func (c *Consistent) GetLeast(key string) (string, error) { 97 | c.RLock() 98 | defer c.RUnlock() 99 | 100 | if len(c.hosts) == 0 { 101 | return "", ErrNoHosts 102 | } 103 | 104 | h := c.hash(key) 105 | idx := c.search(h) 106 | 107 | i := idx 108 | for { 109 | host := c.hosts[c.sortedSet[i]] 110 | if c.loadOK(host) { 111 | return host, nil 112 | } 113 | i++ 114 | if i >= len(c.hosts) { 115 | i = 0 116 | } 117 | if i == idx-1 { 118 | return c.hosts[c.sortedSet[idx]], nil 119 | 120 | } 121 | } 122 | return c.hosts[c.sortedSet[i]], nil 123 | } 124 | 125 | func (c *Consistent) search(key uint64) int { 126 | idx := sort.Search(len(c.sortedSet), func(i int) bool { 127 | return c.sortedSet[i] >= key 128 | }) 129 | 130 | if idx >= len(c.sortedSet) { 131 | idx = 0 132 | } 133 | return idx 134 | } 135 | 136 | // Sets the load of `host` to the given `load` 137 | func (c *Consistent) UpdateLoad(host string, load int64) { 138 | c.Lock() 139 | defer c.Unlock() 140 | 141 | if _, ok := c.loadMap[host]; !ok { 142 | return 143 | } 144 | c.totalLoad -= c.loadMap[host].Load 145 | c.loadMap[host].Load = load 146 | c.totalLoad += load 147 | } 148 | 149 | // Increments the load of host by 1 150 | // 151 | // should only be used with if you obtained a host with GetLeast 152 | func (c *Consistent) Inc(host string) { 153 | atomic.AddInt64(&c.loadMap[host].Load, 1) 154 | atomic.AddInt64(&c.totalLoad, 1) 155 | 156 | } 157 | 158 | // Decrements the load of host by 1 159 | // 160 | // should only be used with if you obtained a host with GetLeast 161 | func (c *Consistent) Done(host string) { 162 | c.Lock() 163 | defer c.Unlock() 164 | 165 | if _, ok := c.loadMap[host]; !ok { 166 | return 167 | } 168 | atomic.AddInt64(&c.loadMap[host].Load, -1) 169 | atomic.AddInt64(&c.totalLoad, -1) 170 | } 171 | 172 | // Deletes host from the ring 173 | func (c *Consistent) Remove(host string) bool { 174 | c.Lock() 175 | defer c.Unlock() 176 | 177 | for i := 0; i < replicationFactor; i++ { 178 | h := c.hash(fmt.Sprintf("%s%d", host, i)) 179 | delete(c.hosts, h) 180 | c.delSlice(h) 181 | } 182 | return true 183 | } 184 | 185 | // Return the list of hosts in the ring 186 | func (c *Consistent) Hosts() (hosts []string) { 187 | c.RLock() 188 | defer c.RUnlock() 189 | for k, _ := range c.loadMap { 190 | hosts = append(hosts, k) 191 | } 192 | return hosts 193 | } 194 | 195 | // Returns the loads of all the hosts 196 | func (c *Consistent) GetLoads() map[string]int64 { 197 | loads := map[string]int64{} 198 | 199 | for k, v := range c.loadMap { 200 | loads[k] = v.Load 201 | } 202 | return loads 203 | } 204 | 205 | // Returns the maximum load of the single host 206 | // which is: 207 | // (total_load/number_of_hosts)*1.25 208 | // total_load = is the total number of active requests served by hosts 209 | // for more info: 210 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 211 | func (c *Consistent) MaxLoad() int64 { 212 | if c.totalLoad == 0 { 213 | c.totalLoad = 1 214 | } 215 | var avgLoadPerNode float64 216 | avgLoadPerNode = float64(c.totalLoad / int64(len(c.loadMap))) 217 | if avgLoadPerNode == 0 { 218 | avgLoadPerNode = 1 219 | } 220 | avgLoadPerNode = math.Ceil(avgLoadPerNode * 1.25) 221 | return int64(avgLoadPerNode) 222 | } 223 | 224 | func (c *Consistent) loadOK(host string) bool { 225 | // a safety check if someone performed c.Done more than needed 226 | if c.totalLoad < 0 { 227 | c.totalLoad = 0 228 | } 229 | 230 | var avgLoadPerNode float64 231 | avgLoadPerNode = float64((c.totalLoad + 1) / int64(len(c.loadMap))) 232 | if avgLoadPerNode == 0 { 233 | avgLoadPerNode = 1 234 | } 235 | avgLoadPerNode = math.Ceil(avgLoadPerNode * 1.25) 236 | 237 | bhost, ok := c.loadMap[host] 238 | if !ok { 239 | panic(fmt.Sprintf("given host(%s) not in loadsMap", bhost.Name)) 240 | } 241 | 242 | if float64(bhost.Load)+1 <= avgLoadPerNode { 243 | return true 244 | } 245 | 246 | return false 247 | } 248 | 249 | func (c *Consistent) delSlice(val uint64) { 250 | for i := 0; i < len(c.sortedSet); i++ { 251 | if c.sortedSet[i] == val { 252 | c.sortedSet = append(c.sortedSet[:i], c.sortedSet[i+1:]...) 253 | } 254 | } 255 | } 256 | 257 | func (c *Consistent) hash(key string) uint64 { 258 | h := fnv.New64() 259 | h.Write([]byte(key)) 260 | return h.Sum64() 261 | } 262 | -------------------------------------------------------------------------------- /vendor/github.com/lafikl/consistent/consistent_test.go: -------------------------------------------------------------------------------- 1 | package consistent 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAdd(t *testing.T) { 9 | c := New() 10 | 11 | c.Add("127.0.0.1:8000") 12 | if len(c.sortedSet) != replicationFactor { 13 | t.Fatal("vnodes number is incorrect") 14 | } 15 | } 16 | 17 | func TestGet(t *testing.T) { 18 | c := New() 19 | 20 | c.Add("127.0.0.1:8000") 21 | host, err := c.Get("127.0.0.1:8000") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if host != "127.0.0.1:8000" { 27 | t.Fatal("returned host is not what expected") 28 | } 29 | } 30 | 31 | func TestRemove(t *testing.T) { 32 | c := New() 33 | 34 | c.Add("127.0.0.1:8000") 35 | c.Remove("127.0.0.1:8000") 36 | 37 | if len(c.sortedSet) != 0 && len(c.hosts) != 0 { 38 | t.Fatal(("remove is not working")) 39 | } 40 | 41 | } 42 | 43 | func TestGetLeast(t *testing.T) { 44 | c := New() 45 | 46 | c.Add("127.0.0.1:8000") 47 | c.Add("92.0.0.1:8000") 48 | 49 | for i := 0; i < 100; i++ { 50 | host, err := c.GetLeast("92.0.0.1:80001") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | c.Inc(host) 55 | } 56 | 57 | for k, v := range c.GetLoads() { 58 | if v > c.MaxLoad() { 59 | t.Fatalf("host %s is overloaded. %d > %d\n", k, v, c.MaxLoad()) 60 | } 61 | } 62 | fmt.Println("Max load per node", c.MaxLoad()) 63 | fmt.Println(c.GetLoads()) 64 | 65 | } 66 | 67 | func TestIncDone(t *testing.T) { 68 | c := New() 69 | 70 | c.Add("127.0.0.1:8000") 71 | c.Add("92.0.0.1:8000") 72 | 73 | host, err := c.GetLeast("92.0.0.1:80001") 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | c.Inc(host) 79 | if c.loadMap[host].Load != 1 { 80 | t.Fatalf("host %s load should be 1\n") 81 | } 82 | 83 | c.Done(host) 84 | if c.loadMap[host].Load != 0 { 85 | t.Fatalf("host %s load should be 0\n") 86 | } 87 | 88 | } 89 | 90 | func TestHosts(t *testing.T) { 91 | hosts := []string{ 92 | "127.0.0.1:8000", 93 | "92.0.0.1:8000", 94 | } 95 | 96 | c := New() 97 | for _, h := range hosts { 98 | c.Add(h) 99 | } 100 | 101 | addedHosts := c.Hosts() 102 | for _, h := range hosts { 103 | found := false 104 | for _, ah := range addedHosts { 105 | if h == ah { 106 | found = true 107 | break 108 | } 109 | } 110 | if !found { 111 | t.Fatal("missing host", h) 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /vendor/github.com/lafikl/consistent/example_test.go: -------------------------------------------------------------------------------- 1 | package consistent_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/lafikl/consistent" 8 | ) 9 | 10 | func Example_consistent(t *testing.T) { 11 | c := consistent.New() 12 | 13 | // adds the hosts to the ring 14 | c.Add("127.0.0.1:8000") 15 | c.Add("92.0.0.1:8000") 16 | 17 | // Returns the host that owns `key`. 18 | // 19 | // As described in https://en.wikipedia.org/wiki/Consistent_hashing 20 | // 21 | // It returns ErrNoHosts if the ring has no hosts in it. 22 | host, err := c.Get("/app.html") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | log.Println(host) 28 | } 29 | 30 | func Example_bounded() { 31 | c := consistent.New() 32 | 33 | // adds the hosts to the ring 34 | c.Add("127.0.0.1:8000") 35 | c.Add("92.0.0.1:8000") 36 | 37 | // It uses Consistent Hashing With Bounded loads 38 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 39 | // to pick the least loaded host that can serve the key 40 | // 41 | // It returns ErrNoHosts if the ring has no hosts in it. 42 | // 43 | host, err := c.GetLeast("/app.html") 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | // increases the load of `host`, we have to call it before sending the request 48 | c.Inc(host) 49 | // send request or do whatever 50 | log.Println("send request to", host) 51 | // call it when the work is done, to update the load of `host`. 52 | c.Done(host) 53 | } 54 | --------------------------------------------------------------------------------