Skip to content

Commit 307c459

Browse files
Merge pull request #2 from Abdulrahman-Tayara/feature/weighted-round-robin
Feature/weighted round robin
2 parents 21c641b + 648b2cc commit 307c459

File tree

7 files changed

+180
-14
lines changed

7 files changed

+180
-14
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ Configuring Go-LB is a breeze, it supports both JSON and YAML configuration file
4040
{
4141
"name": "server1",
4242
"url": "http://localhost:8080",
43-
"health_url": "/health"
43+
"health_url": "/health",
44+
"weight": 1,
4445
},
4546
{
4647
"name": "server2",
4748
"url": "http://localhost:8082",
48-
"health_url": "/health-check"
49+
"health_url": "/health-check",
50+
"weight": 2
4951
}
5052
],
5153
"tls_enabled": true, // default false
@@ -70,9 +72,11 @@ servers:
7072
- name: "server1"
7173
url: "http://localhost:8080"
7274
health_url: "/health"
75+
weight: 1
7376
- name: "server2"
7477
url: "http://localhost:8082"
7578
health_url: "/health-check"
79+
weight: 2
7680
tls_enabled: true # default false
7781
tls_cert_file: "/path/on/container/cert.pem"
7882
tls_key_file: "/path/on/container/key.pem"
@@ -93,12 +97,14 @@ tls_key_file: "/path/on/container/key.pem"
9397
{
9498
"name": "server1",
9599
"url": "http://localhost:8080",
96-
"health_url": "/health"
100+
"health_url": "/health",
101+
"weight": 1
97102
},
98103
{
99104
"name": "server2",
100105
"url": "http://localhost:8082",
101-
"health_url": "/health-check"
106+
"health_url": "/health-check",
107+
"weight": 2
102108
}
103109
],
104110
"tls_enabled": true,
@@ -137,8 +143,10 @@ rate_limit_interval_seconds: 10
137143
servers:
138144
- url: "http://localhost:8080"
139145
health_url: "/health"
146+
weight: 1
140147
- url: "http://localhost:8082"
141148
health_url: "/health-check"
149+
weight: 2
142150
tls_enabled: true
143151
tls_cert_file: "/path/on/container/cert.pem"
144152
tls_key_file: "/path/on/container/key.pem"

main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"tayara/go-lb/lb"
1111
"tayara/go-lb/ratelimiter/buckettokens"
1212
"tayara/go-lb/strategy"
13+
"tayara/go-lb/utils"
1314
"time"
1415

1516
"github.com/pkg/errors"
@@ -78,10 +79,10 @@ func runHTTPServer(cfg *configs.Configs, handler http.Handler) {
7879
)
7980

8081
if cfg.TLSEnabled {
81-
if !IsFileExist(cfg.TLSCertPath) {
82+
if !utils.IsFileExist(cfg.TLSCertPath) {
8283
panic(fmt.Errorf("TLS cert filepath doesn't exist %v", cfg.TLSCertPath))
8384
}
84-
if !IsFileExist(cfg.TLSKeyPath) {
85+
if !utils.IsFileExist(cfg.TLSKeyPath) {
8586
panic(fmt.Errorf("TLS key filepath doesn't exist %v", cfg.TLSKeyPath))
8687
}
8788

models/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Server struct {
1313
Name string `mapstructure:"name" json:"name" yaml:"name"`
1414
Url string `mapstructure:"url" json:"url" yaml:"url"`
1515
HealthUrl string `mapstructure:"health_url" json:"health_url" yaml:"health_url"`
16+
Weight int `mapstructure:"weight" json:"weight" yaml:"weight"`
1617

1718
host string
1819
absoluteHealthUrl string

strategy/weighted_round_robin.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package strategy
2+
3+
import (
4+
"net/http"
5+
"slices"
6+
"sync"
7+
"tayara/go-lb/models"
8+
)
9+
10+
type WeightedRoundRobinStrategy struct {
11+
weightsBucket map[string]int
12+
13+
servers []*models.Server
14+
15+
sync.RWMutex
16+
}
17+
18+
func NewWeightedRoundRobinStrategy() ILoadBalancerStrategy {
19+
return &WeightedRoundRobinStrategy{
20+
weightsBucket: make(map[string]int),
21+
}
22+
}
23+
24+
func (s *WeightedRoundRobinStrategy) Next(request *http.Request) *models.Server {
25+
defer s.RUnlock()
26+
27+
s.RLock()
28+
29+
return s.selectServer(s.servers)
30+
}
31+
32+
func (s *WeightedRoundRobinStrategy) selectServer(servers []*models.Server) *models.Server {
33+
if len(servers) == 0 {
34+
return nil
35+
}
36+
37+
s.ensureWightsBucketValid()
38+
39+
selectedServer := s.findMaxServerWeight()
40+
41+
return selectedServer
42+
}
43+
44+
func (s *WeightedRoundRobinStrategy) findMaxServerWeight() *models.Server {
45+
var selectedServerName string
46+
var maxWeight = -1000
47+
48+
for serverName, tokens := range s.weightsBucket {
49+
if tokens > maxWeight {
50+
maxWeight = tokens
51+
selectedServerName = serverName
52+
}
53+
}
54+
55+
if index := slices.IndexFunc(s.servers, func(s *models.Server) bool {
56+
return s.Name == selectedServerName
57+
}); index >= 0 && index < len(s.servers) {
58+
return s.servers[index]
59+
} else {
60+
return nil
61+
}
62+
}
63+
64+
func (s *WeightedRoundRobinStrategy) ensureWightsBucketValid() {
65+
for _, tokens := range s.weightsBucket {
66+
if tokens > 0 {
67+
return
68+
}
69+
}
70+
71+
s.resetBucket()
72+
}
73+
74+
func (s *WeightedRoundRobinStrategy) UpdateServers(servers []*models.Server) {
75+
defer s.Unlock()
76+
77+
s.Lock()
78+
79+
s.servers = servers
80+
81+
s.resetBucket()
82+
}
83+
84+
func (s *WeightedRoundRobinStrategy) resetBucket() {
85+
clear(s.weightsBucket)
86+
87+
for _, server := range s.servers {
88+
s.weightsBucket[server.Name] = server.Weight
89+
}
90+
}
91+
92+
func (s *WeightedRoundRobinStrategy) RequestServed(server *models.Server, request *http.Request) {
93+
if tokens, ok := s.weightsBucket[server.Name]; ok {
94+
s.weightsBucket[server.Name] = max(tokens-1, 0)
95+
}
96+
}

strategy/weighted_round_robin_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package strategy
2+
3+
import (
4+
"tayara/go-lb/models"
5+
"tayara/go-lb/utils"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestWeightedRoundRobin(t *testing.T) {
12+
strategy := NewWeightedRoundRobinStrategy()
13+
14+
servers := []*models.Server{
15+
{
16+
Name: "Server1",
17+
Url: "http://localhost:8080",
18+
Weight: 2,
19+
},
20+
{
21+
Name: "Server2",
22+
Url: "http://localhost:8081",
23+
Weight: 3,
24+
},
25+
{
26+
Name: "Server3",
27+
Url: "http://localhost:8082",
28+
Weight: 1,
29+
},
30+
}
31+
32+
strategy.UpdateServers(servers)
33+
34+
requestsCount := 6
35+
36+
actualCounts := map[string]int{}
37+
38+
for i := 0; i < requestsCount; i++ {
39+
server := strategy.Next(nil)
40+
41+
actualCounts[server.Name] = utils.GetOrDefault(actualCounts, server.Name, 0) + 1
42+
43+
strategy.RequestServed(server, nil)
44+
}
45+
46+
expectedCounts := map[string]int{
47+
servers[0].Name: 2,
48+
servers[1].Name: 3,
49+
servers[2].Name: 1,
50+
}
51+
52+
assert.Equal(t, expectedCounts, actualCounts)
53+
}

utils.go

Lines changed: 0 additions & 8 deletions
This file was deleted.

utils/utils.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package utils
2+
3+
import "os"
4+
5+
func IsFileExist(filePath string) bool {
6+
_, err := os.Stat(filePath)
7+
return err == nil
8+
}
9+
10+
func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
11+
if v, ok := m[key]; ok {
12+
return v
13+
}
14+
return defaultValue
15+
}

0 commit comments

Comments
 (0)