Skip to content

Commit 856ba96

Browse files
authored
Add RANDOM load Add RANDOM load balancing strategy (#590)
* Introduced `RANDOM` strategy in `constants.go` and `loadbalancer.go`. * Updated `gatewayd.yaml` to use the new `RANDOM` strategy as an option.
1 parent 2788266 commit 856ba96

File tree

5 files changed

+151
-1
lines changed

5 files changed

+151
-1
lines changed

config/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,5 @@ const (
130130
// Load balancing strategies.
131131
const (
132132
RoundRobinStrategy = "ROUND_ROBIN"
133+
RANDOMStrategy = "RANDOM"
133134
)

gatewayd.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ servers:
8383
address: 0.0.0.0:15432
8484
loadBalancer:
8585
# Load balancer strategies can be found in config/constants.go
86-
strategy: ROUND_ROBIN
86+
strategy: RANDOM # ROUND_ROBIN, RANDOM
8787
enableTicker: False
8888
tickInterval: 5s # duration
8989
enableTLS: False

network/loadbalancer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ func NewLoadBalancerStrategy(server *Server) (LoadBalancerStrategy, *gerr.Gatewa
1313
switch server.LoadbalancerStrategyName {
1414
case config.RoundRobinStrategy:
1515
return NewRoundRobin(server), nil
16+
case config.RANDOMStrategy:
17+
return NewRandom(server), nil
1618
default:
1719
return nil, gerr.ErrLoadBalancerStrategyNotFound
1820
}

network/random.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package network
2+
3+
import (
4+
"crypto/rand"
5+
"errors"
6+
"fmt"
7+
"math/big"
8+
"sync"
9+
10+
gerr "github.com/gatewayd-io/gatewayd/errors"
11+
)
12+
13+
// Random is a struct that holds a list of proxies and a mutex for thread safety.
14+
type Random struct {
15+
mu sync.Mutex
16+
proxies []IProxy
17+
}
18+
19+
// NewRandom creates a new Random instance with the given server's proxies.
20+
func NewRandom(server *Server) *Random {
21+
return &Random{
22+
proxies: server.Proxies,
23+
}
24+
}
25+
26+
// NextProxy returns a random proxy from the list.
27+
func (r *Random) NextProxy() (IProxy, *gerr.GatewayDError) {
28+
r.mu.Lock()
29+
defer r.mu.Unlock()
30+
31+
proxiesLen := len(r.proxies)
32+
if proxiesLen == 0 {
33+
return nil, gerr.ErrNoProxiesAvailable.Wrap(errors.New("proxy list is empty"))
34+
}
35+
36+
randomIndex, err := randInt(proxiesLen)
37+
if err != nil {
38+
return nil, gerr.ErrNoProxiesAvailable.Wrap(err)
39+
}
40+
41+
return r.proxies[randomIndex], nil
42+
}
43+
44+
// randInt generates a random integer between 0 and max-1 using crypto/rand.
45+
func randInt(max int) (int, error) {
46+
// Generate a secure random number
47+
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
48+
if err != nil {
49+
return 0, fmt.Errorf("failed to generate random integer: %w", err)
50+
}
51+
return int(n.Int64()), nil
52+
}

network/random_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package network
2+
3+
import (
4+
"sync"
5+
"testing"
6+
7+
gerr "github.com/gatewayd-io/gatewayd/errors"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// TestNewRandom verifies that the NewRandom function properly initializes
12+
// a Random object with the provided server and its associated proxies.
13+
// The test ensures that the Random object is not nil and that the number of
14+
// proxies in the Random object matches the number of proxies in the Server.
15+
func TestNewRandom(t *testing.T) {
16+
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
17+
server := &Server{Proxies: proxies}
18+
random := NewRandom(server)
19+
20+
assert.NotNil(t, random)
21+
assert.Equal(t, len(proxies), len(random.proxies))
22+
}
23+
24+
// TestGetNextProxy checks the behavior of the NextProxy method in various
25+
// scenarios, including when proxies are available, when no proxies are available,
26+
// and the randomness of proxy selection.
27+
//
28+
// - The first sub-test confirms that NextProxy returns a valid proxy when available.
29+
// - The second sub-test ensures that an error is returned when there are no proxies available.
30+
// - The third sub-test checks if the proxy selection is random by comparing two subsequent calls.
31+
func TestGetNextProxy(t *testing.T) {
32+
t.Run("Returns a proxy when proxies are available", func(t *testing.T) {
33+
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
34+
server := &Server{Proxies: proxies}
35+
random := NewRandom(server)
36+
37+
proxy, err := random.NextProxy()
38+
39+
assert.Nil(t, err)
40+
assert.Contains(t, proxies, proxy)
41+
})
42+
43+
t.Run("Returns error when no proxies are available", func(t *testing.T) {
44+
server := &Server{Proxies: []IProxy{}}
45+
random := NewRandom(server)
46+
47+
proxy, err := random.NextProxy()
48+
49+
assert.Nil(t, proxy)
50+
assert.Equal(t, gerr.ErrNoProxiesAvailable.Message, err.Message)
51+
})
52+
t.Run("Random selection of proxies", func(t *testing.T) {
53+
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
54+
server := &Server{Proxies: proxies}
55+
random := NewRandom(server)
56+
57+
proxy1, _ := random.NextProxy()
58+
proxy2, _ := random.NextProxy()
59+
60+
assert.Contains(t, proxies, proxy1)
61+
assert.Contains(t, proxies, proxy2)
62+
// It's possible that proxy1 and proxy2 are the same, but if we run this
63+
// test enough times, they should occasionally be different.
64+
})
65+
}
66+
67+
// TestConcurrencySafety ensures that the Random object is safe for concurrent
68+
// use by multiple goroutines. The test launches multiple goroutines that
69+
// concurrently call the NextProxy method. It then verifies that all returned
70+
// proxies are part of the expected set of proxies, ensuring thread safety.
71+
func TestConcurrencySafety(t *testing.T) {
72+
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
73+
server := &Server{Proxies: proxies}
74+
random := NewRandom(server)
75+
76+
var waitGroup sync.WaitGroup
77+
numGoroutines := 100
78+
proxyChan := make(chan IProxy, numGoroutines)
79+
80+
for range numGoroutines {
81+
waitGroup.Add(1)
82+
go func() {
83+
defer waitGroup.Done()
84+
proxy, _ := random.NextProxy()
85+
proxyChan <- proxy
86+
}()
87+
}
88+
89+
waitGroup.Wait()
90+
close(proxyChan)
91+
92+
for proxy := range proxyChan {
93+
assert.Contains(t, proxies, proxy)
94+
}
95+
}

0 commit comments

Comments
 (0)