Skip to content

Commit 3b4c83e

Browse files
author
abdulrahman
committed
feature: adding content based routing option to the load balancer
1 parent 8904bea commit 3b4c83e

13 files changed

+425
-10
lines changed

README.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ Configuring Go-LB is a breeze, it supports both JSON and YAML configuration file
3333
"rate_limit_interval_seconds": 1 // default 2
3434
"servers": [
3535
{
36+
"name": "server1",
3637
"url": "http://localhost:8080",
3738
"health_url": "/health"
3839
},
3940
{
41+
"name": "server2",
4042
"url": "http://localhost:8082",
4143
"health_url": "/health-check"
4244
}
@@ -55,16 +57,98 @@ health_check_interval_seconds: 2
5557
rate_limiter_enabled: True
5658
rate_limit_tokens: 10 # default 10
5759
rate_limit_interval_seconds: 10 # default 2
60+
servers:
61+
- name: "server1"
62+
url: "http://localhost:8080"
63+
health_url: "/health"
64+
- name: "server2"
65+
url: "http://localhost:8082"
66+
health_url: "/health-check"
67+
tls_enabled: true # default false
68+
tls_cert_file: "/path/on/container/cert.pem"
69+
tls_key_file: "/path/on/container/key.pem"
70+
```
71+
72+
## Content Based Routing (CBR) Configuration
73+
74+
### JSON
75+
```json
76+
{
77+
"port": "load balancer port",
78+
"strategy": "round_robin | random | least_connections",
79+
"health_check_interval_seconds": 2,
80+
"rate_limiter_enabled": true,
81+
"rate_limit_tokens": 10,
82+
"rate_limit_interval_seconds": 1,
83+
"servers": [
84+
{
85+
"name": "server1",
86+
"url": "http://localhost:8080",
87+
"health_url": "/health"
88+
},
89+
{
90+
"name": "server2",
91+
"url": "http://localhost:8082",
92+
"health_url": "/health-check"
93+
}
94+
],
95+
"tls_enabled": true,
96+
"tls_cert_file": "/path/to/cert.pem",
97+
"tls_key_file": "/path/to/key.pem",
98+
"routing": {
99+
"default_server": "server1",
100+
"rules": [
101+
{
102+
"conditions": [
103+
{
104+
"path_prefix": "/api/v1 (Optional)",
105+
"method": "GET | post | Put (Optional)",
106+
"headers": { // Optional
107+
"MyHeader": "my-value"
108+
}
109+
}
110+
],
111+
"action": {
112+
"route_to": "server2"
113+
}
114+
}
115+
]
116+
}
117+
}
118+
```
119+
120+
### Yaml
121+
```yaml
122+
port: "load balancer port"
123+
strategy: "round_robin | random | least_connections"
124+
health_check_interval_seconds: 2
125+
rate_limiter_enabled: True
126+
rate_limit_tokens: 10
127+
rate_limit_interval_seconds: 10
58128
servers:
59129
- url: "http://localhost:8080"
60130
health_url: "/health"
61131
- url: "http://localhost:8082"
62132
health_url: "/health-check"
63-
tls_enabled: true # default false
133+
tls_enabled: true
64134
tls_cert_file: "/path/on/container/cert.pem"
65135
tls_key_file: "/path/on/container/key.pem"
136+
routing:
137+
default_server: "server1"
138+
rules:
139+
- conditions:
140+
- path_prefix: "/api/v1"
141+
method: "GET"
142+
headers:
143+
MyHeader: "my-value"
144+
action:
145+
route_to: "server2"
66146
```
67147
148+
149+
150+
151+
68152
## Getting Started
69153
70154
### Docker

configs/configs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Configs struct {
1313
Port string `mapstructure:"port" json:"port" yaml:"port"`
1414
LoadBalancerStrategy string `mapstructure:"load_balancer_strategy" json:"load_balancer_strategy" yaml:"load_balancer_strategy"`
1515
Servers []*models.Server `mapstructure:"servers" json:"servers" yaml:"servers"`
16+
Routing models.Routing `mapstructure:"routing" json:"routing" yaml:"routing"`
1617
HealthCheckIntervalSeconds int `mapstructure:"health_check_interval_seconds" json:"health_check_interval_seconds" yaml:"health_check_interval_seconds"`
1718
RateLimiterEnabled bool `mapstructure:"rate_limiter_enabled" json:"rate_limiter_enabled" yaml:"rate_limiter_enabled"`
1819
RateLimitTokens int `mapstructure:"rate_limit_tokens" json:"rate_limit_tokens" yaml:"rate_limit_tokens"`

configs/configs_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ func TestLoadConfigs(t *testing.T) {
5656
HealthUrl: "http://localhost:8081/health",
5757
},
5858
},
59+
Routing: models.Routing{
60+
DefaultServer: "server2",
61+
Rules: models.RouteRules{
62+
{
63+
Conditions: []models.RouteCondition{
64+
{
65+
PathPrefix: "/api/v1",
66+
Method: "GET",
67+
Headers: map[string]string{
68+
"useragent": "Mobile",
69+
},
70+
},
71+
},
72+
Action: models.RouteAction{
73+
RouteTo: "server1",
74+
},
75+
},
76+
},
77+
},
5978
LoadBalancerStrategy: "round_robin",
6079
HealthCheckIntervalSeconds: 3,
6180
RateLimiterEnabled: true,

configs/configs_test.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,14 @@ servers:
1010
health_url: http://localhost:8080/health
1111
- name: server2
1212
url: http://localhost:8081
13-
health_url: http://localhost:8081/health
13+
health_url: http://localhost:8081/health
14+
routing:
15+
rules:
16+
- conditions:
17+
- path_prefix: "/api/v1"
18+
method: "GET"
19+
headers:
20+
useragent: "Mobile"
21+
action:
22+
route_to: server1
23+
default_server: server2

lb/load_balancer.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ type ILoadBalancer interface {
2626
// -----------------------
2727

2828
type loadBalancer struct {
29-
servers []*models.Server
30-
strategy lbs.ILoadBalancerStrategy
29+
servers []*models.Server
30+
strategy lbs.ILoadBalancerStrategy
31+
cbr *models.Routing
32+
cbrEnabled bool
3133

3234
sync.RWMutex
3335
}
@@ -105,13 +107,47 @@ func (l *loadBalancer) Next(request *http.Request) *models.Server {
105107

106108
l.RLock()
107109

110+
if l.cbrEnabled {
111+
return l.getServerFromCbr(request)
112+
}
113+
108114
return l.strategy.Next(request)
109115
}
110116

111-
func NewLoadBalancer(servers []*models.Server, strategy lbs.ILoadBalancerStrategy) ILoadBalancer {
117+
func (l *loadBalancer) getServerFromCbr(req *http.Request) *models.Server {
118+
serverName := l.cbr.Rules.GetRouteTo(&models.RequestProps{
119+
Method: req.Method,
120+
Headers: req.Header,
121+
Path: req.URL.Path,
122+
})
123+
124+
if serverName == "" {
125+
serverName = l.cbr.DefaultServer
126+
}
127+
128+
return l.serverByName(serverName)
129+
}
130+
131+
func (l *loadBalancer) serverByName(name string) *models.Server {
132+
for _, server := range l.servers {
133+
if server.Name == name {
134+
return server
135+
}
136+
}
137+
138+
return nil
139+
}
140+
141+
func NewLoadBalancer(
142+
servers []*models.Server,
143+
cbr *models.Routing,
144+
strategy lbs.ILoadBalancerStrategy,
145+
) ILoadBalancer {
112146
strategy.UpdateServers(servers)
113147
return &loadBalancer{
114-
servers: servers,
115-
strategy: strategy,
148+
servers: servers,
149+
strategy: strategy,
150+
cbr: cbr,
151+
cbrEnabled: cbr != nil && len(cbr.Rules) > 0,
116152
}
117153
}

lb/load_balancer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestLoadBalancer_ServerDown(t *testing.T) {
1818
},
1919
}
2020

21-
lb := NewLoadBalancer(slices.Clone(servers), strategy.NewRoundRobinStrategy()).(*loadBalancer)
21+
lb := NewLoadBalancer(slices.Clone(servers), nil, strategy.NewRoundRobinStrategy()).(*loadBalancer)
2222

2323
assert.Equal(t, len(servers), len(lb.servers))
2424

@@ -36,7 +36,7 @@ func TestLoadBalancer_ServerUp(t *testing.T) {
3636
},
3737
}
3838

39-
lb := NewLoadBalancer(slices.Clone(servers), strategy.NewRoundRobinStrategy()).(*loadBalancer)
39+
lb := NewLoadBalancer(slices.Clone(servers), nil, strategy.NewRoundRobinStrategy()).(*loadBalancer)
4040

4141
assert.Equal(t, len(servers), len(lb.servers))
4242

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func main() {
4242

4343
loadBalancer := lb.NewLoadBalancer(
4444
slices.Clone(cfg.Servers),
45+
&cfg.Routing,
4546
selectedStrategy,
4647
)
4748

models/routing.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package models
2+
3+
import (
4+
"slices"
5+
"strings"
6+
)
7+
8+
type Routing struct {
9+
Rules RouteRules `mapstructure:"rules" json:"rules" yaml:"rules"`
10+
DefaultServer string `mapstructure:"default_server" json:"default_server" yaml:"default_server"`
11+
}
12+
13+
type RequestProps struct {
14+
Path string
15+
Headers map[string][]string
16+
Method string
17+
}
18+
19+
type RouteRule struct {
20+
Conditions []RouteCondition `mapstructure:"conditions" json:"conditions" yaml:"conditions"`
21+
Action RouteAction `mapstructure:"action" json:"action" yaml:"action"`
22+
}
23+
24+
type RouteAction struct {
25+
RouteTo string `mapstructure:"route_to" json:"route_to" yaml:"route_to"`
26+
}
27+
28+
type RouteCondition struct {
29+
PathPrefix string `mapstructure:"path_prefix" json:"path_prefix" yaml:"path_prefix"`
30+
Headers map[string]string `mapstructure:"headers" json:"headers" yaml:"headers"`
31+
Method string `mapstructure:"method" json:"method" yaml:"method"`
32+
}
33+
34+
type RouteRules []RouteRule
35+
36+
func (rc *RouteCondition) DoesMatch(req *RequestProps) bool {
37+
if rc.PathPrefix != "" && !strings.HasPrefix(req.Path, rc.PathPrefix) {
38+
return false
39+
}
40+
if rc.Method != "" && strings.ToLower(rc.Method) != strings.ToLower(req.Method) {
41+
return false
42+
}
43+
for k, v := range rc.Headers {
44+
if !slices.Contains(req.Headers[k], v) {
45+
return false
46+
}
47+
}
48+
return true
49+
}
50+
51+
func (rr *RouteRule) DoesMatch(req *RequestProps) bool {
52+
for _, condition := range rr.Conditions {
53+
if !condition.DoesMatch(req) {
54+
return false
55+
}
56+
}
57+
return true
58+
}
59+
60+
func (rr *RouteRules) GetRouteTo(req *RequestProps) string {
61+
for _, rule := range *rr {
62+
if rule.DoesMatch(req) {
63+
return rule.Action.RouteTo
64+
}
65+
}
66+
67+
return ""
68+
}

0 commit comments

Comments
 (0)