Skip to content

Commit 5f94580

Browse files
Merge pull request #1 from Abdulrahman-Tayara/feature/sticky-session
feature: implement sticky session strategy
2 parents 3b4c83e + 001d710 commit 5f94580

File tree

10 files changed

+134
-7
lines changed

10 files changed

+134
-7
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ Configuring Go-LB is a breeze, it supports both JSON and YAML configuration file
2727
{
2828
"port": "load balancer port",
2929
"strategy": "round_robin | random | least_connections", // default round_robin
30+
"strategy_configs": {
31+
// Sticky session configs
32+
"sticky_session_cookie_name": "example",
33+
"sticky_session_ttl_seconds": 100
34+
},
3035
"health_check_interval_seconds": 2,
3136
"rate_limiter_enabled": true,
3237
"rate_limit_tokens": 10, // default 10
@@ -53,6 +58,10 @@ Configuring Go-LB is a breeze, it supports both JSON and YAML configuration file
5358
```yaml
5459
port: "load balancer port"
5560
strategy: "round_robin | random | least_connections" # default round_robin
61+
strategy_configs:
62+
# Sticky session configs
63+
sticky_session_cookie_name: "example"
64+
sticky_session_ttl_seconds: 100
5665
health_check_interval_seconds: 2
5766
rate_limiter_enabled: True
5867
rate_limit_tokens: 10 # default 10

configs/configs.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package configs
22

33
import (
4-
"github.com/spf13/viper"
54
"tayara/go-lb/models"
5+
"tayara/go-lb/strategy"
6+
7+
"github.com/spf13/viper"
68
)
79

810
const (
@@ -12,6 +14,7 @@ const (
1214
type Configs struct {
1315
Port string `mapstructure:"port" json:"port" yaml:"port"`
1416
LoadBalancerStrategy string `mapstructure:"load_balancer_strategy" json:"load_balancer_strategy" yaml:"load_balancer_strategy"`
17+
StrategyConfigs strategy.Configs `mapstructure:"strategy_configs" json:"strategy_configs" yaml:"strategy_configs"`
1518
Servers []*models.Server `mapstructure:"servers" json:"servers" yaml:"servers"`
1619
Routing models.Routing `mapstructure:"routing" json:"routing" yaml:"routing"`
1720
HealthCheckIntervalSeconds int `mapstructure:"health_check_interval_seconds" json:"health_check_interval_seconds" yaml:"health_check_interval_seconds"`

configs/configs_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package configs
33
import (
44
"reflect"
55
"tayara/go-lb/models"
6+
"tayara/go-lb/strategy"
67
"testing"
78
)
89

@@ -31,7 +32,11 @@ func TestLoadConfigs(t *testing.T) {
3132
HealthUrl: "http://localhost:8080/health",
3233
},
3334
},
34-
LoadBalancerStrategy: "round_robin",
35+
LoadBalancerStrategy: "round_robin",
36+
StrategyConfigs: strategy.Configs{
37+
StickySessionCookieName: "example",
38+
StickySessionTTLSeconds: 100,
39+
},
3540
HealthCheckIntervalSeconds: 5,
3641
RateLimiterEnabled: true,
3742
RateLimitIntervalSeconds: 10,
@@ -75,7 +80,11 @@ func TestLoadConfigs(t *testing.T) {
7580
},
7681
},
7782
},
78-
LoadBalancerStrategy: "round_robin",
83+
LoadBalancerStrategy: "round_robin",
84+
StrategyConfigs: strategy.Configs{
85+
StickySessionCookieName: "example",
86+
StickySessionTTLSeconds: 100,
87+
},
7988
HealthCheckIntervalSeconds: 3,
8089
RateLimiterEnabled: true,
8190
RateLimitIntervalSeconds: 10,

configs/configs_test.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{
22
"port": "9090",
33
"load_balancer_strategy": "round_robin",
4+
"strategy_configs": {
5+
"sticky_session_cookie_name": "example",
6+
"sticky_session_ttl_seconds": 100
7+
},
48
"health_check_interval_seconds": 5,
59
"rate_limiter_enabled": true,
610
"rate_limit_tokens": 10,

configs/configs_test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
port: 8900
22
load_balancer_strategy: "round_robin"
3+
strategy_configs:
4+
sticky_session_cookie_name: "example"
5+
sticky_session_ttl_seconds: 100
36
health_check_interval_seconds: 3
47
rate_limiter_enabled: True
58
rate_limit_tokens: 10

main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package main
33
import (
44
"flag"
55
"fmt"
6-
"github.com/pkg/errors"
7-
"golang.org/x/exp/slog"
86
"net/http"
97
"slices"
108
"tayara/go-lb/configs"
@@ -13,6 +11,9 @@ import (
1311
"tayara/go-lb/ratelimiter/buckettokens"
1412
"tayara/go-lb/strategy"
1513
"time"
14+
15+
"github.com/pkg/errors"
16+
"golang.org/x/exp/slog"
1617
)
1718

1819
var (
@@ -38,7 +39,7 @@ func main() {
3839

3940
slog.Info("configs were loaded", "configs", *cfg)
4041

41-
selectedStrategy := strategy.GetLoadBalancerStrategy(cfg.LoadBalancerStrategy)
42+
selectedStrategy := strategy.GetLoadBalancerStrategy(cfg.LoadBalancerStrategy, cfg.StrategyConfigs)
4243

4344
loadBalancer := lb.NewLoadBalancer(
4445
slices.Clone(cfg.Servers),

strategy/factory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const (
66
LeastConnectionsLoadBalancerStrategy = "least_connections"
77
)
88

9-
func GetLoadBalancerStrategy(strategy string) ILoadBalancerStrategy {
9+
func GetLoadBalancerStrategy(strategy string, cfg Configs) ILoadBalancerStrategy {
1010
switch strategy {
1111
case RoundRobinLoadBalancerStrategy:
1212
return NewRoundRobinStrategy()

strategy/sticky_session.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package strategy
2+
3+
import (
4+
"math/rand"
5+
"net/http"
6+
"tayara/go-lb/models"
7+
"time"
8+
)
9+
10+
var (
11+
defaultSessionName = "lbsession"
12+
defaultTTLSeconds = 300
13+
)
14+
15+
type StickySessionStrategy struct {
16+
cfg Configs
17+
18+
servers []*models.Server
19+
}
20+
21+
func NewStickySessionStrategy(cfg Configs) ILoadBalancerStrategy {
22+
if cfg.StickySessionCookieName == "" {
23+
cfg.StickySessionCookieName = defaultSessionName
24+
}
25+
if cfg.StickySessionTTLSeconds <= 0 {
26+
cfg.StickySessionTTLSeconds = defaultTTLSeconds
27+
}
28+
29+
return &StickySessionStrategy{
30+
cfg: cfg,
31+
}
32+
}
33+
34+
func (s *StickySessionStrategy) Next(request *http.Request) *models.Server {
35+
cookie, err := request.Cookie(s.cfg.StickySessionCookieName)
36+
if err != nil || cookie.Value == "" {
37+
cookieValue := generateSessionID()
38+
cookie.Value = cookieValue
39+
request.AddCookie(&http.Cookie{
40+
Name: s.cfg.StickySessionCookieName,
41+
Value: cookieValue,
42+
Expires: time.Now().Add(time.Second * time.Duration(s.cfg.StickySessionTTLSeconds)),
43+
HttpOnly: true,
44+
})
45+
}
46+
47+
return s.getServer(cookie.Value)
48+
}
49+
50+
func (s *StickySessionStrategy) getServer(sessionId string) *models.Server {
51+
hash := hashSessionToInt(sessionId)
52+
index := hash % len(s.servers)
53+
return s.servers[index]
54+
}
55+
56+
func hashSessionToInt(sessionId string) int {
57+
hash := 0
58+
for _, char := range sessionId {
59+
hash += int(char)
60+
}
61+
return hash
62+
}
63+
64+
func (*StickySessionStrategy) RequestServed(server *models.Server, request *http.Request) {
65+
}
66+
67+
func (*StickySessionStrategy) UpdateServers(servers []*models.Server) {
68+
}
69+
70+
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
71+
72+
func generateSessionID() string {
73+
b := make([]rune, 20)
74+
for i := range b {
75+
b[i] = letterRunes[rand.Intn(len(letterRunes))]
76+
}
77+
return string(b)
78+
}

strategy/sticky_session_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package strategy
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestHasSessionToInt(t *testing.T) {
10+
session := "ABCDEFG"
11+
12+
num := hashSessionToInt(session)
13+
14+
assert.Equal(t, 476, num)
15+
}

strategy/strategy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import (
55
"tayara/go-lb/models"
66
)
77

8+
type Configs struct {
9+
StickySessionCookieName string `mapstructure:"sticky_session_cookie_name" json:"sticky_session_cookie_name" yaml:"sticky_session_cookie_name"`
10+
StickySessionTTLSeconds int `mapstructure:"sticky_session_ttl_seconds" json:"sticky_session_ttl_seconds" yaml:"sticky_session_ttl_seconds"`
11+
}
12+
813
type ILoadBalancerStrategy interface {
914
Next(request *http.Request) *models.Server
1015

0 commit comments

Comments
 (0)